Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

User Guide

HPM is a Rust-based package manager for SideFX Houdini. It manages both HPM packages (Houdini tools, HDAs, scripts, shelf tools, toolbars, etc.) and the Python dependencies those packages need, and it produces the Houdini package.json files that let Houdini find them at launch.

This guide covers installation, the command surface, the hpm.toml manifest, global configuration, and troubleshooting.

Table of contents

  1. Install
  2. First package
  3. Registries
  4. Command reference
  5. The hpm.toml manifest
  6. Global configuration
  7. Storage layout
  8. Houdini integration
  9. Output formats and automation
  10. Troubleshooting

Install

Prerequisites

  • SideFX Houdini 20.5, 21.x, or 22.x (for integration — HPM itself runs fine without it).
  • Rust 1.85+ if building from source.
  • Git (optional; used by hpm init --vcs git).

From a pre-built binary

Download the binary for your platform from the latest release and put it on your PATH. Verify with:

hpm --version

From source

git clone https://github.com/3db-dk/hpm.git
cd hpm
cargo build --release
# binary: target/release/hpm

Shell completions

# Bash (add to ~/.bashrc)
eval "$(hpm completions bash)"

# Zsh (add to ~/.zshrc)
eval "$(hpm completions zsh)"

# Fish
hpm completions fish | source

# PowerShell (add to $PROFILE)
hpm completions powershell | Out-String | Invoke-Expression

Supported shells: bash, zsh, fish, powershell, elvish.

First package

hpm init my-first-package --description "My first Houdini package"
cd my-first-package

The standard template creates this layout:

my-first-package/
├── hpm.toml           # HPM manifest
├── README.md
├── .gitignore
├── package.json       # Houdini-native package file
├── otls/              # HDAs / .otl files
├── python/            # Python modules (python/__init__.py is pre-created)
├── scripts/           # Shelf tools and script hooks
├── presets/           # Node presets
├── config/            # Configuration files
└── tests/             # Test files

Pass --bare for just hpm.toml — use this when you have a custom layout or are wrapping an existing codebase.

A typical workflow

hpm init my-tools                        # 1. scaffold
hpm add some-creator/utility-nodes@1.0.0 # 2. add deps
hpm add local-tools --path ../local-tools
hpm install                              # 3. resolve & install
hpm check                                # 4. sanity check
hpm list --tree                          # 5. inspect

Then wire Houdini up — see Houdini integration.

Registries

HPM resolves package names through one or more registries. A registry is either an HTTP API endpoint or a Git repository serving a Cargo-style index. Without at least one registry configured, hpm search and hpm add <name>@<version> have nowhere to look.

# API registry (auto-detected by URL)
hpm registry add https://api.tumbletrove.com/v1/registry --name tumbletrove

# Git-index registry (auto-detected by .git suffix or host)
hpm registry add https://github.com/studio/hpm-packages.git --name studio --type git

hpm registry list
hpm registry update      # refresh caches
hpm registry remove studio

Global registries live in ~/.hpm/config.toml under [[registries]]. A project can also declare registries in its own hpm.toml under [[registries]] — handy for studios that want each project to pin the registries it resolves against. See [[registries]] below.

A dependency can target a specific registry:

[dependencies]
studio-tools = { version = "1.0.0", registry = "studio" }

Without registry, HPM resolves through every configured registry in order.

Command reference

Global options

Available on every command:

OptionDescription
-v, --verboseIncrease verbosity (repeat for more detail).
-q, --quietSuppress non-error output.
--color <when>auto, always, or never.
--output <format>human (default), json, json-lines, json-compact.
-C, --directory <dir>Run as if invoked from <dir>.

hpm init

Create a new HPM package.

hpm init [OPTIONS] [NAME]

Options

FlagDefaultDescription
--description <text>Package description.
--author <name>git config user.* if setAuthor ("Name <email>").
--version <v>0.1.0Initial version.
--license <id>MITLicense identifier.
--houdini <range>^21[compat].houdini Cargo-style range (e.g. "^21", ">=20.5, <22", ">=21"). Default is bounded to a single Houdini major — see [compat] for why.
--bareoffSkip standard directories and generated files; create only hpm.toml.
--vcs <vcs>gitgit or none.

Examples

hpm init my-tools
hpm init --bare minimal
hpm init advanced-tools \
  --description "Advanced geometry tools" \
  --author "Artist <artist@studio.com>" \
  --license Apache-2.0 \
  --houdini ">=20.5, <22"

hpm add

Add one or more dependencies to hpm.toml.

hpm add [OPTIONS] <PACKAGE>...

<PACKAGE> is either a bare name (resolved from registries at install time) or name@version. name uses the creator/slug form.

Options

FlagDescription
--path <dir>Add as a local path dependency (only valid with a single package). Path dependencies install into a _dev/ subtree of the global packages dir so they never overwrite a registry install at the same (slug, version).
--linkFor path dependencies, install as a symlink (Unix) or NTFS junction (Windows) instead of copying. Working-tree edits become visible to a live Houdini session without re-running hpm install. Requires --path. Packages that declare native [compat].platforms (a DSO/HDK build, not universal) are installed as a copy even with --link: a linked native binary can’t be rebuilt in place while a Houdini session has it loaded (Windows LNK1104), and a mapped DSO needs a relaunch to reload anyway.
-m, --manifest <path>Path to the manifest to modify (hpm.toml or containing dir). Defaults to cwd.
--optionalMark all added dependencies as optional.
--registry <name>Resolve from this configured registry only, and record registry = "<name>" in hpm.toml so later installs and updates keep using it. Errors if the name is not configured. Not valid with --path.

Examples

hpm add studio/utility-nodes@1.0.0
hpm add studio/a@1.0.0 studio/b@2.0.0
hpm add local-tools --path ../local-tools
hpm add local-tools --path ../local-tools --link  # live edits → Houdini
hpm add studio/visualize@1.0.0 --optional
hpm add studio/lib@1.0.0 -m /path/to/project
hpm add studio/internal@1.0.0 --registry studio   # pin to one registry

hpm remove

Remove a dependency from hpm.toml. This does not delete package files from ~/.hpm/packages/ — run hpm clean for that.

hpm remove [OPTIONS] <PACKAGE>
FlagDescription
-m, --manifest <path>Manifest to modify.

hpm install

Resolve and install every dependency declared in hpm.toml.

hpm install [OPTIONS]

Install does the following:

  1. Loads hpm.toml.
  2. If hpm.lock exists, verifies cached packages against stored checksums and warns if the lock is older than 90 days.
  3. Resolves HPM dependencies through configured registries and downloads anything missing to ~/.hpm/packages/.
  4. Collects Python dependencies from the root manifest and every installed dependency’s manifest, downloads a managed CPython matching the lower bound of the root manifest’s [compat].houdini to ~/.hpm/uv-python/ (no-op if already present), resolves them with the bundled uv, and installs them into a content-addressable venv in ~/.hpm/venvs/<hash>/.
  5. Writes one Houdini manifest per installed dependency to <project>/.hpm/packages/{creator}.{slug}.json.
  6. Writes or updates hpm.lock.

Options

FlagDescription
-m, --manifest <path>Path to hpm.toml (or its containing directory).
--frozen-lockfileFail if hpm.lock is missing or would need to change. Use in CI.

hpm update

Update dependencies to their latest compatible versions.

hpm update [OPTIONS] [PACKAGES]...

With no packages, updates all. With specific packages, updates only those.

FlagDescription
-m, --manifest <path>Manifest to operate on.
--dry-runPrint the proposed plan without applying it.
-y, --yesSkip the confirmation prompt.

Examples

hpm update --dry-run
hpm update
hpm update studio/geometry-tools
hpm update --yes --output json      # CI-friendly

hpm list

Display installed dependencies and their metadata.

hpm list [OPTIONS]
FlagDescription
-m, --manifest <path>Manifest to read.
--treeRender dependencies as a tree.

hpm check

Validate hpm.toml and the surrounding project.

hpm check

Check runs:

  • hpm.toml exists, parses, and passes manifest validation (scoped creator/slug path, semver version, [compat].houdini parseable, [stage] and [[operators]] per-platform consistency with [compat].platforms).
  • Generated Houdini package.json serializes cleanly.
  • Soft warnings for: missing description, missing authors, missing keywords, missing [compat].houdini, missing README or license file, missing .gitignore when a .git directory is present, packages larger than 100 MB, and individual files larger than 10 MB.

Check is advisory — warnings do not fail the command.

hpm run

Execute a script defined in the manifest’s [scripts] table.

hpm run <SCRIPT> [-- ARGS...]
ArgumentDescription
<SCRIPT>Name of the entry under [scripts].
ARGS...Trailing arguments forwarded to the script verbatim, after shell-quoting.

Behaviour:

  • Looks up the named entry; if its cmd is a conditional list, the first variant whose when.os matches the host wins. Plain entries always match.
  • Sets HPM_PACKAGE_ROOT to the manifest directory and runs the command from that directory through the host shell (sh -c on Unix, cmd /C on Windows).
  • For table-form entries with python or requirements, materializes a uv-managed venv at ~/.hpm/venvs/<hash>/, prepends its bin/ (or Scripts/ on Windows) to PATH, and sets VIRTUAL_ENV. Two scripts whose python + requirements resolve to the same closure share one venv on disk.
  • For entries with package-env = true, runs inside the package’s full resolved environment instead: the merged venv across the project and its installed dependencies, with every package’s python/ on PYTHONPATH. Requires hpm install to have been run.
  • The script’s exit code becomes hpm’s exit code, so hpm run is safe to chain in CI or wrap in a Houdini hook.

Example

[scripts.tt_setup]
cmd          = "python scripts/tt_setup.py"
python       = "3.11"
requirements = ["PySide6>=6.6"]
hpm run tt_setup

Search every configured registry for packages matching a query.

hpm search <QUERY>

If no registries are configured, HPM prints instructions to add one.

hpm build

Materialise the install image into a directory. Runs [stage].prepack scripts (compile DSO, collapse expanded HDAs, etc.), then copies workspace files into the output directory using the same include/exclude/place rules hpm pack would apply. The result is what a registry consumer’s install would look like.

hpm build [OPTIONS]

hpm build is a one-shot CLI verb — it copies files and exits, with no background watcher. The output directory is yours to manage; common patterns:

  • Single workstation, one Houdini at a time: leave the default [stage].output_dir (dist/), point Houdini at it, rerun hpm build whenever you want a refresh.
  • Multiple Houdini sessions in parallel: pass --output <tmpdir> per session and have each session’s HOUDINI_PACKAGE_PATH reference its own staging directory. Avoids cross-session DSO lock conflicts.

Options

FlagDescription
-m, --manifest <path>Path to hpm.toml or containing dir. Defaults to cwd.
-o, --output <dir>Override [stage].output_dir. Relative paths resolve against the manifest dir; absolute paths are used verbatim.
--platform <id>Target platform. Defaults to host when [compat].platforms is declared. Required when host is not in the declared list.
--profile <name>Build profile. Defaults to release. Selects the matching [stage.profile.<name>] table (if any) and is exposed to prepack scripts as HPM_BUILD_PROFILE.
--houdini-majors <list>Houdini major versions this build targets, space-separated (e.g. --houdini-majors "21 22"). Forwarded verbatim to prepack scripts as HPM_HOUDINI_MAJORS. Omit to leave it unset.
--no-prepackSkip [stage].prepack scripts. Use in CI when build steps already ran out-of-band.
--no-cleanKeep existing output-dir contents instead of wiping first.

Prepack environment. In addition to HPM_PACKAGE_ROOT, prepack scripts (and any [scripts] they invoke) see these build-context variables:

  • HPM_BUILD_PROFILE — the selected --profile (default release). A single prepack script can branch on it, e.g. cmake --build --config $HPM_BUILD_PROFILE, instead of maintaining one script per build type.

  • HPM_PLATFORM — the resolved target platform (e.g. macos-aarch64), set whenever the package declares [compat].platforms.

  • HPM_HOUDINI_MAJORS — the set of Houdini major versions this build should target, space-separated (e.g. 21 22, or just 22). A package that builds one native artifact per Houdini major (a compiled USD resolver, say) reads it to restrict the matrix — build every declared major when it is unset or empty, otherwise only the listed ones. hpm forwards the value verbatim and does not interpret it.

    hpm build sets it from --houdini-majors; CI passes the release matrix that way instead of exporting an ad-hoc, unprefixed variable. When the flag is omitted, an HPM_HOUDINI_MAJORS already present in the environment passes through unchanged, so an out-of-process producer (e.g. a desktop launcher that discovers the machine’s installed majors) can set it and an explicit --houdini-majors still overrides. Unset in both = the package’s full declared matrix.

HPM_BUILD_PROFILE, HPM_PLATFORM, and HPM_HOUDINI_MAJORS are set only for the hpm build prepack path; a standalone hpm run <script> does not carry build-profile, platform, or Houdini-major context.

Workflow notes — live editing and DSO rebuild

These are user-level concerns; HPM doesn’t model them in the manifest:

  • HDA editing. Edits made inside Houdini save back to whatever path Houdini loaded the HDA from. If you load from dist/otls/foo.hda (collapsed by hpm build), saves go into the build output and get clobbered on the next hpm build. If you want round-trip editing, point Houdini at an unstaged expanded HDA dir during dev, and run hpm build only when you want to produce the publishable form.
  • DSO rebuild while Houdini is loaded. On Windows, a loaded .dll is locked. With --output <tmpdir-A> for session A and --output <tmpdir-B> for session B, your cmake --build (writing to build/<plat>/) doesn’t hit either lock, and a fresh hpm build --output <tmpdir-C> writes to a third location — you only hit the lock when you try to rebuild into a directory a live Houdini still has loaded. The typical workflow is one temp dir per Houdini lifetime, thrown away on Houdini close.

hpm pack

Build a distributable archive from the current package.

hpm pack [OPTIONS]

Pack runs hpm check first, then:

  1. Auto-generates a Houdini-native {slug}.json inside the archive unless the user has provided one. This file follows Houdini’s own package format, so the archive is usable by Houdini even without HPM.
  2. Filters files by [stage] (per-platform place rules and include/exclude globs) when the manifest declares [compat].platforms.
  3. Produces a .zip archive plus a SHA-256 checksum.
  4. If a signing key is supplied, produces an Ed25519 signature over the archive bytes and emits a keyId.
  5. Builds a searchable asset index from the manifest’s [[operators]] declarations and includes it in --json output (and warns if a declared source file is missing from the archive).

Options

FlagDescription
--key <path>Ed25519 PKCS#8 PEM private key. Overrides HPM_SIGNING_KEY.
--output <dir>Output directory. Defaults to the current directory.
--jsonEmit the result as JSON (useful in CI).
--platform <id>Override host-platform detection. Valid: linux-x86_64, linux-aarch64, macos-x86_64, macos-aarch64, windows-x86_64, windows-aarch64, universal. Only legal when [compat].platforms is declared.
--verify-assetsFail the pack (and delete the archive) if any [[operators]] source is missing from the produced archive, instead of just warning.

Asset index in --json output

--json adds an assets array alongside the existing checksum/signature fields. Each entry describes one bundled operator; None fields are omitted. HDA-vs-DSO is carried by kind (hda_operator / dso_operator).

{
  "archive": "studio-rbd-tools-1.0.0.zip",
  "sha256": "…",
  "signature": null,
  "key_id": null,
  "platform": null,
  "assets": [
    {
      "kind": "hda_operator",
      "type_name": "studio::rbd_configure::2.0",
      "label": "RBD Configure",
      "category": "Sop",
      "namespace": "studio",
      "op_version": "2.0",
      "tab_submenu": "Studio/Dynamics",
      "icon": "SOP_rbd",
      "source_file": "otls/rbd.hda"
    },
    {
      "kind": "dso_operator",
      "type_name": "studio::fast_scatter",
      "label": "Fast Scatter",
      "category": "Sop",
      "namespace": "studio",
      "source_file": "dso/scatter.so"
    }
  ]
}

The index is built from declarations, not by parsing the bundled files — see [[operators]] for why and how to declare them.

Signing key resolution order

  1. --key <path> (CLI flag).
  2. HPM_SIGNING_KEY environment variable. If its value starts with -----BEGIN, it’s treated as inline PEM; otherwise it’s a path.
  3. [signing].key_path in ~/.hpm/config.toml.

Generating a signing key

openssl genpkey -algorithm ed25519 -out signing.pem
openssl pkey -in signing.pem -pubout -out signing.pub.pem

Keep signing.pem secret. Publish signing.pub.pem so consumers can verify. See Security for the wire format.

hpm audit

Run a security audit on the current project.

hpm audit [OPTIONS]
FlagDescription
-m, --manifest <path>Manifest to audit.

Audit checks:

  • HTTP URLs — flags any url = ... dependency whose URL is http:// rather than https://.
  • Lock file presence — warns if hpm.lock is missing.
  • Lock file staleness — warns if hpm.lock is older than 90 days.
  • Checksum verification — verifies every cached package in ~/.hpm/packages/ matches the checksum stored in hpm.lock.

See Security for more.

hpm global

Install a package into Houdini’s user preferences, so it loads in every session of one Houdini version without a project or launcher wiring.

hpm global add    <PACKAGE> --houdini <X.Y> [--registry <name>]
hpm global list             --houdini <X.Y>
hpm global remove <PACKAGE> --houdini <X.Y>

<PACKAGE> is creator/slug or creator/slug@version; a bare name resolves to the highest non-yanked version. Resolution goes through the configured registries exactly like hpm add, including the --registry pin.

--houdini <X.Y> is required and is not guessed. hpm does not discover Houdini installations, and the preferences directory is per-version, so a default would risk writing where your Houdini never looks. A full build string is accepted (21.0.729) and truncated to its major.minor line.

The manifest is written as hpm-<creator>.<slug>.json into:

PlatformDirectory
Linux~/houdiniX.Y/packages/
macOS~/Library/Preferences/houdini/X.Y/packages/
Windows%USERPROFILE%\Documents\houdiniX.Y\packages\

HOUDINI_USER_PREF_DIR takes precedence when set, with __HVER__ expanded to X.Y, matching Houdini’s own behaviour.

That directory is not hpm’s. It holds files from SideFX, other tools, and you. Unlike a project install — which owns <project>/.hpm/packages/ and sweeps anything unrecognized out of it — hpm global never scans that directory to decide what to delete. Every removal is driven off the ledger at ~/.hpm/global/houdini-<X.Y>.json and touches only files hpm recorded writing. Files you put there by hand are left alone.

A package whose [compat].houdini does not include the target version is rejected before anything is written. Installing it anyway would emit a manifest whose enable expression is false, and Houdini would ignore it in silence.

hpm global remove deletes the manifest but leaves the package in the store, matching hpm remove. Run hpm clean to reclaim the space.

There is no hpm global update yet: with no manifest to rewrite, updating is hpm global add at the new version, which replaces the entry in place.

Examples

hpm global add studio/utility-nodes --houdini 21.0
hpm global add studio/utility-nodes@1.2.0 --houdini 21.0 --registry studio
hpm global list --houdini 21.0
hpm global remove studio/utility-nodes --houdini 21.0

Only public packages resolve today: the standalone CLI has nowhere to store credentials, so it always resolves anonymously. A private package fails as “not found”.

hpm clean

Remove orphaned packages, dev (path-dep) installs, and/or venvs that no active project depends on.

Globally installed packages are roots here too — they belong to no project, so without the global ledger they would look unreferenced and be deleted out from under a manifest Houdini still loads. If a ledger cannot be read, hpm clean refuses to run rather than risk that.

hpm clean [OPTIONS]
FlagDescription
-n, --dry-runPrint what would be removed, without touching anything.
-y, --yesSkip confirmation.
--python-onlyClean only orphaned venvs.
--comprehensiveClean packages, dev installs, and venvs.

HPM identifies active projects via [projects] in ~/.hpm/config.toml (explicit_paths plus recursive search_roots). Three classes of artifact are considered:

  • Registry/URL packages in ~/.hpm/packages/<creator>/<slug>@<version>/: preserved if reachable from any active project’s dependency graph.
  • Dev installs in ~/.hpm/packages/_dev/<creator>/<slug>@<version>/ (created by { path = "..." } deps, copy or link mode): preserved if any active project’s path-dep source manifest reports that (slug, version). Entries are listed as _dev/<slug>@<version> so the source of each is obvious. Link installs are unlinked safely — never followed. A copy is content-addressed as _dev/<slug>@<version>/<source-hash>/, so each rebuild leaves a new directory behind; clean reclaims the superseded ones for a still-used package, keeping only the current build. Run it with your Houdini sessions closed — a copy still in use is skipped rather than removed.
  • Python venvs under ~/.hpm/venvs/: preserved if any kept package declares matching [python_dependencies]. Removed only when --python-only or --comprehensive is set.

A project whose path-dep source can’t be read (workspace moved or deleted) logs a warning and doesn’t block cleanup of other dev installs; re-run hpm install after fixing the path to reinstate anything that was swept.

hpm registry

Manage registries in ~/.hpm/config.toml. See Registries.

hpm registry add <URL> [--name <alias>] [--type api|git] [--if-not-exists]
hpm registry list
hpm registry remove <NAME>
hpm registry update

If --type is omitted, HPM infers it: URLs ending in .git or hosted on github.com / gitea.* are treated as git; everything else is api.

Pass --if-not-exists to make add idempotent: if a registry with the same name already exists it is left unchanged and the command exits 0, instead of erroring. Useful in provisioning scripts.

hpm completions

Emit shell completion scripts — see Install.

The hpm.toml manifest

A minimal manifest:

[package]
path = "my-studio/my-tools"
name = "My Tools"
version = "1.0.0"

All sections, in the order they appear in practice:

[package]

FieldRequiredDescription
pathyesScoped identifier, creator/slug. Both segments must be kebab-case (lowercase letters, digits, hyphens). Example: tumblehead/tumble-rig. hpm init defaults the creator to local, producing local/<name> — change it before publishing.
nameyesFreeform display name.
versionyesSemantic version per semver.org. major.minor.patch is required; pre-release identifiers (1.0.0-alpha.1, 1.0.0-rc.2) and build metadata (1.0.0+build.5) are accepted.
descriptionnoShort description.
authorsnoList of "Name <email>" strings.
licensenoLicense identifier (e.g. MIT, Apache-2.0).
readmenoPath to README, relative to the package. Defaults to README.md for init-generated packages.
homepagenoProject homepage URL.
repositorynoRepository URL.
documentationnoDocumentation URL.
keywordsnoList of keywords for discovery.
categoriesnoList of categories.

[compat]

Target-environment compatibility for the package. Two axes:

  • houdini — a Cargo-style version requirement string.
  • platforms — the native platforms this package supports. Omit (or use ["universal"]) for pure-data / pure-Python packages; list the platforms the package ships binaries for otherwise.
[compat]
houdini = "^21"                                # default — Houdini 21.x only
# houdini = ">=20.5, <22"                       # explicit range
# houdini = "~21.5"                             # tilde: >=21.5, <21.6
# houdini = "21"                                # bare = caret = ^21
# houdini = ">=20.5"                            # unbounded above — only safe for pure-data
platforms = ["linux-x86_64", "macos-aarch64"]   # omit for pure-data

The supported operators are =, >=, >, <=, <, ^, ~, and the bare-version shorthand (aliases caret). Multiple comparators combine with and when separated by commas. The same grammar is reused inside [runtime] conditional values (when = { houdini = "^21" }).

The lower bound of [compat].houdini on the root manifest drives the bundled Python version. A dependency package’s range is a compatibility floor only and does not influence the venv ABI. See Python guide.

Why the default is bounded above

Houdini’s binary compatibility doesn’t survive a major-version bump. A DSO compiled against the Houdini 21 SDK won’t load in Houdini 22; some Python module signatures shift between majors too. The init template defaults to houdini = "^21" (Houdini 21.x only) for that reason — authors who ship binaries get a safe starting point, and authors of pure-data / pure-Python packages can widen the range explicitly after testing on the next major.

hpm check warns when [compat].platforms is non-empty but [compat].houdini is unbounded above (e.g. ">=21"). That catches the failure mode where a native-binary package installs cleanly on a newer Houdini and then crashes at load.

[dependencies]

HPM package dependencies. Keys are scoped creator/slug paths. Values take one of four shapes:

[dependencies]

# 1. Registry, version-only (shorthand)
"studio/utility-nodes" = "1.0.0"

# 2. Registry with options
"studio/material-library" = { version = "2.0.0", optional = true }
"studio/internal-tool" = { version = "1.0.0", registry = "studio" }

# 3. Direct URL (pre-built archive — ZIP or gzipped tar both accepted)
"studio/prebuilt" = { url = "https://pkg.example.com/prebuilt-1.0.0.zip", version = "1.0.0" }

# 4. Local path (development)
local-tools = { path = "../local-tools" }
local-tools = { path = "../local-tools", optional = true }

# 5. Local path, installed as a symlink/junction (live edits)
#    Working-tree edits become visible to a live Houdini session without
#    re-running `hpm install`. Otherwise identical to (4): same _dev/ namespace
#    isolation, no effect on registry installs at the same coordinate.
local-tools = { path = "../local-tools", link = true }

A registry = "<name>" key pins the dependency to one configured registry. The pin is authoritative: resolution, install, and hpm update all consult only that registry, and pinning a registry that is not configured is an error rather than a silent fallback to the rest of the set. Without the key, a dependency resolves across every configured registry in order, first match wins. Add a pin from the command line with hpm add <package> --registry <name>.

The lock file (hpm.lock) records the resolved version and SHA-256 checksum for each dependency, so subsequent installs are reproducible and tamper-evident.

[python_dependencies]

Python packages, installed through the bundled uv into a shared venv:

[python_dependencies]

# Version constraint shorthand
numpy = ">=1.20.0"

# Detailed form
requests = { version = ">=2.25.0", extras = ["security", "socks"] }
matplotlib = { version = "^3.5.0", optional = true }

Version constraints use PEP 440 syntax (same as pip/uv). See Python guide for the Houdini→Python version mapping and venv sharing behavior.

[runtime]

Environment variables to set when Houdini loads the package. The key is the variable name; the value is a { method, value } pair:

[runtime]
MY_PLUGIN_ROOT = { method = "set", value = "$HPM_PACKAGE_ROOT/config" }
HOUDINI_TOOLBAR_PATH = { method = "prepend", value = "$HPM_PACKAGE_ROOT/toolbar" }
HOUDINI_AUDIT_LOG = { method = "append", value = "$HPM_PACKAGE_ROOT/logs/audit" }
MethodEffect
setReplace the variable.
prependPrepend to the existing variable (Houdini picks the platform separator).
appendAppend to the existing variable.

Use $HPM_PACKAGE_ROOT to refer to the installed package directory. HPM merges these entries with its built-in PYTHONPATH and HOUDINI_SCRIPT_PATH entries when generating the Houdini manifest.

Note on the emitted form: in the generated Houdini package.json, prepend and append are written as single-element JSON lists. The list form matters for them — Houdini only honors method on a custom variable when the variable’s first definition uses a list value; with a flat string every later entry silently overwrites the variable regardless of its declared method.

set is emitted as a bare flat string instead (Houdini’s package system has no set method). This is deliberate: a path-registered variable that Houdini seeds itself — OCIO, PYTHONPATH, HOUDINI_*_PATH — is always merge-able, so a list-form replace appends onto Houdini’s seed (you’d get builtin;project, which breaks OCIO) instead of replacing it. A flat string overwrites the seed cleanly and load-order-independent, which is exactly what set promises. (A genuinely conditional set value — a { when, set } list — can’t be a flat string, so it falls back to the list replace form; gate the whole package on the condition, or set such a path variable flat, if you need the overwrite there.)

Required env vars

A package can declare an env var as required without giving it a value. Any project that depends on the package must then supply the value in its own [runtime] section in hpm.toml. hpm install (and project sync) errors out otherwise — the package isn’t launchable without it.

# In the package's hpm.toml
[runtime]
PROJECT_ASSETS = { method = "set", required = true }
# In the consuming project's hpm.toml
[runtime]
PROJECT_ASSETS = { method = "set", value = "/mnt/studio/assets" }

required = true may be combined with a value; the value then acts as a default and the project override becomes optional. Without a value, the entry is a hard placeholder.

A consuming project can also override any package-declared env var by re-declaring the same key in its own [runtime]. How the project entry combines with the package’s depends on the project entry’s method:

Project methodResult
setReplaces every package contribution wholesale — only the project’s value survives.
prepend / appendExtends them — each declaring package’s own entry stays in place, and the project’s value merges in once, after all of them.

So a project append/prepend adds to a package-provided value (e.g. extending a package’s PYTHONPATH) rather than clobbering it. Use set when you genuinely want to replace what the packages contribute.

Mechanically, project sync writes the project’s [runtime] entries into a dedicated ~hpm-project-overrides.json in the project packages dir. Houdini processes package files in byte-wise filename order and ~ sorts after every slug character, so the overrides file always applies last — after every per-package file — and each override is applied exactly once, no matter how many packages declare the same variable. This also means a project [runtime] entry takes effect even when no package declares the variable. Since a project-level entry has no owning package, $HPM_PACKAGE_ROOT is undefined there (sync warns if a project entry references it).

Conditional values

value accepts either a flat string or an ordered list of { when, set } variants. The variants are selected against four axes; the first match wins per the rules below.

[runtime.PXR_PLUGINPATH_NAME]
method = "prepend"
value = [
  { when = { houdini = "^21" }, set = "$HPM_PACKAGE_ROOT/resolver/houdini21/r" },
  { when = { houdini = "^22" }, set = "$HPM_PACKAGE_ROOT/resolver/houdini22/r" },
]
FieldFormEvaluated byCompiles to
houdiniCargo-style req: "^21", "~21.5", ">=21, <22.5", "21" (alias for ^21)Houdini at startuphoudini_version >= 'X' and houdini_version < 'Y'
os"linux", "macos", "windows"Houdini at startuphoudini_os == '<os>'
python"3.11", "python3.10", etc.Houdini at startuphoudini_python == 'python<v>'
install_source"dev" (path dependency) or "registry" (registry/URL install)hpm at install timefiltered out before emission

The first three axes lower into Houdini’s package.json expression form per https://www.sidefx.com/docs/houdini/ref/plugins.html. install_source is install-time evaluated by hpm — variants gated to a non-matching install source are dropped before the Houdini package.json is written, so a "dev" branch never ships to a registry consumer’s manifest and a "registry" branch never fires in the dev’s own Houdini.

All present axes combine with and within a single when. Order matters: the first matching branch wins. (Houdini itself applies every matching element of a conditional value array, so hpm compiles each emitted branch to exclude all earlier branches’ conditions — first-match is guaranteed by the emitted expressions, and anything after an always-true branch is dropped as unreachable.) An empty when = {} is the always-true fallback and should appear last. $HPM_PACKAGE_ROOT is substituted in each branch, just like in flat values; any other $VAR (e.g. $HOUDINI_MAJOR_RELEASE) passes through verbatim so Houdini’s own variable expansion handles it.

Malformed selectors fail at manifest validation time, so authors find them before publish, not at install.

HDK plugin pattern (dev-only paths)

The canonical use of install_source is HDK plugin development: a build-tree path that must reach the dev’s own Houdini but never leak to a registry consumer. Express this with a single [runtime] entry whose dev variant points at build/ and whose fallback points at the staged artifact:

[runtime.HOUDINI_DSO_PATH]
method = "prepend"
value = [
  # While developing the package locally:
  { when = { install_source = "dev", os = "windows" }, set = "$HPM_PACKAGE_ROOT/build/Release" },
  { when = { install_source = "dev", os = "linux"   }, set = "$HPM_PACKAGE_ROOT/build/lib" },
  { when = { install_source = "dev", os = "macos"   }, set = "$HPM_PACKAGE_ROOT/build/lib" },
  # What ships in the published archive:
  { when = {}, set = "$HPM_PACKAGE_ROOT/dso" },
]

When this package is consumed via { path = "..." }, the dev variant fires and points Houdini at the live build directory. When it ships through a registry, hpm filters the dev variants out at install time, the fallback fires, and Houdini sees only the published dso/ location. If you want the variable to disappear entirely for non-dev consumers, omit the fallback branch — an entry with no surviving variants is not emitted.

When a key appears in both the project and the package, the project entry’s method decides whether it replaces or combines:

  • set — the project override replaces the package’s [runtime] entry (with its surviving variants).
  • prepend / append — the package’s [runtime] entry (with surviving variants) is emitted in the package’s own file, and the project’s override merges in after every package file via the project overrides manifest (see [runtime] above).

[stage]

Defines how the install image is derived from the workspace. [stage] governs both hpm pack (which streams an archive directly from the workspace, applying these rules) and — when present — hpm build (which materialises the same image into output_dir on disk so a path-dep consumer can pick it up live).

[stage]
# Where `hpm build` materialises the install image. Default: "dist".
output_dir = "dist"

# Scripts (named entries from [scripts]) to run before staging. Fail-fast.
prepack = ["build-dso"]

# Gitignore-style globs applied on top of .gitignore and .hpmignore.
# Empty `include` means "everything not excluded". Always-excluded: .git/, .hpm/.
include = ["python/**", "otls/**", "config/**", "LICENSE", "README.md"]
exclude = ["src/**", "build/**", "tests/**"]

# Per-platform place rules. The platform key must appear in [compat].platforms.
[stage.platform.linux-x86_64]
place = [{ from = "build/linux/*.so", to = "dso/" }]

[stage.platform.macos-aarch64]
place = [{ from = "build/macos/*.dylib", to = "dso/" }]

[stage.platform.windows-x86_64]
place = [{ from = "build/win/*.dll", to = "dso/" }]

Platforms. Valid platform identifiers (TumbleTrove API build platform enum verbatim): linux-x86_64, linux-aarch64, macos-x86_64, macos-aarch64, windows-x86_64, windows-aarch64, universal. Use universal for OS-agnostic content (pure-Python / data). Declare the platforms you ship under [compat].platforms; each per-platform [stage.platform.<plat>] table must reference a platform listed there.

Place rules. Each rule has a from glob (workspace-relative) and a to path (archive-relative). If to ends with / it’s a directory; the file’s basename is appended. Otherwise to is the literal archive path (use when relocating a single file under a renamed name). Both from and to use forward slashes regardless of host OS.

Per-platform packing semantics. When packing for --platform <X>:

  • A path matched by [stage.platform.X].place[*].from is included at the rewritten to path. The target’s claim wins over other platforms’.
  • A path matched only by [stage.platform.Y].place[*].from (some Y != X) is excluded.
  • A path matched by no place rule and not covered by [stage].exclude is included as common content at its workspace-relative path. (If [stage].include is non-empty, common content is restricted to paths matching one of those globs.)

This means listing the same from glob under every platform is a valid way to declare “this content ships in every per-platform archive” (e.g. a shared resolver path).

Build vs pack. hpm pack reads [stage] directly and streams the archive from the workspace — useful in CI where you build immediately before packing. hpm build runs prepack scripts and materialises the same install image into output_dir on disk, so a path-dep consumer working in another project can pick it up live (with link = true, edits flow through without re-running hpm install).

Build profiles — [stage.profile.<name>]

Build profiles are a second axis orthogonal to platform, for packages whose native binaries can be built more than one way — typically an optimized release (the default) and a symbol-bearing debug variant for attaching a debugger to an HDK plugin.

hpm build --profile <name> always exposes the name to prepack scripts as HPM_BUILD_PROFILE (see Prepack environment above). When a matching [stage.profile.<name>] table is declared, its overrides layer onto the base [stage] so the profile build is reproducible from the manifest:

[stage]
prepack = ["build-dso"]

[stage.profile.debug]
# Replaces the base prepack list when non-empty.
prepack = ["build-dso-debug"]

# Per-platform place rules here are appended to the base platform rules —
# e.g. ship Windows PDBs alongside the DSO only in the debug profile.
[stage.profile.debug.platform.windows-x86_64]
place = [{ from = "build/Debug/*.pdb", to = "dso/windows-x86_64/" }]

Merge semantics (base [stage] + selected profile):

  • prepack — the profile’s list replaces the base when non-empty; otherwise the base prepack runs.
  • include / exclude — profile entries are appended to the base globs.
  • platform.<plat>.place — profile place rules are appended to the matching base platform entry (a new platform key is created if absent). Each [stage.profile.<name>.platform.<plat>] key must appear in [compat].platforms, same as the base [stage.platform.<plat>] tables.

The default profile is release; you only need a [stage.profile.release] table if you want to customize the default. A --profile <name> that names no declared table is allowed — the env var is still set — but hpm build prints a warning so a typo is visible. hpm pack is unaffected by --profile; profile selection is a build-time concern only.

[[registries]]

Per-project registries. Same shape as the global version in ~/.hpm/config.toml:

[[registries]]
name = "tumbletrove"
url = "https://api.tumbletrove.com/v1/registry"
type = "api"

[[registries]]
name = "studio"
url = "https://github.com/studio/hpm-packages.git"
type = "git"

Project registries are additive to global registries.

[scripts]

Named scripts for the package. Run them with hpm run <name> [args...], which sets HPM_PACKAGE_ROOT to the manifest directory and forwards trailing arguments to the script.

[scripts]
build = "python scripts/build.py"
test = "python -m pytest tests/"

Display metadata (label and description)

The table form accepts two optional, purely informational fields for tools that present scripts to people (menus, buttons, tooltips):

[scripts.launch]
cmd         = "python -m mytool.ui"
label       = "Launch My Tool"
description = "Open the main UI"

label is a human-readable display name and description is a one-line summary. HPM itself never acts on either — hpm run ignores them, and hpm check only echoes them back so you can confirm they parsed. They are consumer-agnostic metadata: any frontend that surfaces a package’s scripts may use them (falling back to the entry key when label is absent), but nothing in HPM treats a labelled script as special. Both are omitted from round-tripped manifests when unset.

Per-host variation

Scripts whose command differs per OS use a conditional cmd value — the same when-grammar [runtime] uses, restricted to the os axis:

[scripts]
build = "cargo build"                        # runs on any host

[scripts.register]
cmd = [
  { when = { os = "windows" }, set = "\"$HPM_PACKAGE_ROOT/plugin/bin/tool.exe\" register" },
  { when = { os = "macos"   }, set = "\"$HPM_PACKAGE_ROOT/plugin/bin/tool\" register" },
  { when = { os = "linux"   }, set = "\"$HPM_PACKAGE_ROOT/plugin/bin/tool\" register" },
]

hpm run picks the first variant whose when.os matches the host. Add an empty when = {} branch as a last entry to declare a fallback that matches any host the explicit branches missed. A script with no matching variant on the current host is treated as absent — hpm run errors with a message that the script only matches other platforms.

Only the os axis is meaningful for scripts: HPM has no Houdini-version or Python context at hpm run time, and install_source is irrelevant because scripts run against the dev’s workspace, not an install. Setting any non-os axis in a script when is rejected at manifest validation time.

Per-script Python environments

A script that needs a pinned Python interpreter or extra packages can opt into a uv-managed virtual environment by switching from the shorthand string to the table form:

[scripts.tt_setup]
cmd          = "python scripts/tt_setup.py"
python       = "3.11"
requirements = ["PySide6>=6.6"]

hpm run tt_setup then resolves requirements through the same uv pipeline that backs [python_dependencies], materialises a venv at ~/.hpm/venvs/<hash>/, prepends its bin/ (or Scripts/ on Windows) to PATH, and sets VIRTUAL_ENV so python in the command resolves to the pinned interpreter. Two scripts whose python + requirements resolve to the same closure share one venv on disk. Plain-string entries keep their prior behaviour and execute against whatever python is on PATH.

Conditional cmd + venv hints compose:

[scripts.regen]
cmd = [
  { when = { os = "windows" }, set = "python scripts\\regen.py" },
  { when = {},                  set = "python scripts/regen.py" },
]
python       = "3.11"
requirements = ["pyyaml"]

Both python and requirements are optional in the table form; omitting both yields a regular script with no venv overhead.

Running inside the package environment

A per-script venv is intentionally minimal — it installs only the script’s own requirements and does not expose the package’s [python_dependencies] or make the package’s python/ modules importable. That’s right for self-contained tooling, but the wrong shape for code that ships in the package and needs to import it (a render-farm task, a CLI the package exposes).

Set package-env = true to run the script inside the package’s full resolved environment instead:

[scripts.farm]
cmd         = "python -m tumblepipe.farm"
package-env = true

hpm run farm then builds the same environment hpm install materialises for Houdini, but for this out-of-Houdini process:

  • a merged uv venv resolved from [python_dependencies] across the project and its installed hpm dependencies (so a dependency’s Python packages are present too);
  • every involved package’s python/ directory on PYTHONPATH — the running package first, then each dependency, then the venv’s site-packages;
  • the interpreter pinned to the project’s Houdini-mapped CPython (a per-script python here is ignored); any requirements on the entry are layered on top of the package environment.

This is read-only: the dependency set comes from the existing hpm.lock plus the global package store, so it never fetches packages or rewrites generated Houdini manifests. Because the venv is content-addressable, when hpm install already built it the run reuses it without re-resolving.

Run hpm install first so the project is locked and its dependencies are present; otherwise hpm run errors and points you there. Consumers like the Deadline render-farm plugin can then delegate the whole venv/deps/PYTHONPATH dance to hpm run <script> instead of reproducing it.

The table form also accepts plain inline-table syntax:

[scripts]
tt_setup = { cmd = "python scripts/tt_setup.py", python = "3.11", requirements = ["PySide6>=6.6"] }

Consumers resolve scripts through PackageManifest::script_for(name) (or resolved_scripts()) which returns the [ScriptEntry] verbatim; call ScriptEntry::resolve_cmd(host_os) to pick the right variant.

[[operators]]

Declares the operators (node types) the package bundles, so hpm pack can emit a searchable asset index that lets a registry offer node-level search (e.g. “find the package that ships an rbd_configure SOP”) without ever downloading or opening the archive.

[[operators]]
kind        = "hda"                          # "hda" or "dso"
type_name   = "studio::rbd_configure::2.0"
label       = "RBD Configure"
category    = "Sop"
tab_submenu = "Studio/Dynamics"
icon        = "SOP_rbd"
source      = "otls/rbd.hda"

[[operators]]
kind      = "dso"
type_name = "studio::fast_scatter"
label     = "Fast Scatter"
category  = "Sop"
source    = "dso/scatter.so"
FieldRequiredDescription
kindyeshda (defined by an HDA/OTL digital asset) or dso (registered by a compiled plugin shipped as a DSO, i.e. authored against the HDK).
type_nameyesNamespaced operator type name, e.g. studio::rbd_configure::2.0. hpm derives namespace and op_version from it where it can.
categoryyesOperator table / network category: Sop, Object, Dop, Lop, Top, Vop, Rop, Driver, …
labelnoTAB-menu display name.
tab_submenunoTAB submenu path, e.g. Studio/Dynamics.
iconnoIcon identifier, e.g. SOP_rbd.
sourcenoWhere the operator’s file lives in the produced package (after [stage] placement) — either a single archive-relative path or a per-platform table (see below). When set, hpm pack checks it against the produced archive.

Per-platform source (multi-platform DSOs)

A single HDA ships at the same path in every archive, so one source string works. But a compiled operator’s binary differs per platform (.so / .dll / .dylib, often under dso/<platform>/), so its source is a table keyed by platform — mirroring [stage.platform.*]. The keys must appear in [compat].platforms:

[[operators]]
kind      = "dso"
type_name = "studio::fast_scatter"
label     = "Fast Scatter"
category  = "Sop"
source    = { linux-x86_64 = "dso/linux-x86_64/scatter.so", macos-aarch64 = "dso/macos-aarch64/scatter.dylib", windows-x86_64 = "dso/windows-x86_64/scatter.dll" }

Each per-platform hpm pack resolves source to the entry for the platform it targets and emits that concrete path. An operator whose table does not list the platform being packed is omitted from that platform’s index — its file isn’t in that package. (Omit source entirely to index an operator by name/category with no path and no check.)

Verifying sources at pack time

By default hpm pack warns when a declared source is missing from the produced archive. Pass --verify-assets to make it a hard error instead (and delete the invalid archive) — recommended in CI so a package never publishes an index advertising a file it doesn’t ship. The check runs against the built archive, so build compiled artifacts before packing.

Why declarations rather than reading the files? The HDA container format is officially undocumented and can change between Houdini versions, and a compiled HDK plugin (DSO) does not expose its operator names offline at all — they are C++ constructor arguments that only materialise inside Houdini. A declaration in hpm.toml is stable, version-proof, and a single source of truth. The author already knows exactly what they ship.

Platform and Houdini-version filtering come for free from elsewhere in the manifest: a DSO’s architecture is implied by the per-platform archive it ships in ([compat].platforms + [stage]), and the supported Houdini range is [compat].houdini. See hpm pack for the emitted index shape.

Global configuration

HPM reads ~/.hpm/config.toml if it exists, then <cwd>/.hpm/config.toml (project override) if it exists. Layering is presence-aware: each file overrides exactly the values it sets, so a project config that omits a section (or a single key) leaves the user-level value in place, and anything no file sets falls back to the built-in default. Setting only storage.home_dir re-derives the cache/packages/registry directories under it unless they are set explicitly. A malformed config file is a hard error, not a silent fallback to defaults.

[install]
path = "packages/hpm"        # relative install path inside projects
parallel_downloads = 8

[storage]
home_dir = "/Users/me/.hpm"                # default: $HOME/.hpm
cache_dir = "/Users/me/.hpm/cache"
packages_dir = "/Users/me/.hpm/packages"
registry_cache_dir = "/Users/me/.hpm/registry"

[projects]
explicit_paths = [
    "/Users/me/studio/pipeline",
]
search_roots = [
    "/Users/me/houdini-projects",
]
max_search_depth = 3
ignore_patterns = [".git", ".hg", ".svn", "node_modules", "backup", "archive", ".cache", "temp", "tmp"]

[[registries]]
name = "tumbletrove"
url = "https://api.tumbletrove.com/v1/registry"
type = "api"

[signing]
key_path = "/Users/me/.hpm/signing.pem"    # fallback for `hpm pack`

What each section controls

[install]

  • path, parallel_downloadsaccepted but currently inert. Both deserialize and validate, but no code outside hpm-config reads them. The per-project package directory is hardcoded to <project>/.hpm/packages (Config::project_paths), so overriding path has no effect. Left in the schema for compatibility; do not rely on either.

[storage]

  • home_dir — HPM’s root on disk. Default $HOME/.hpm on every platform (Linux, macOS, Windows). All other storage paths derive from this by default.
  • cache_dir, packages_dir, registry_cache_dir — override individual subdirectories without moving the whole root.

[projects] — drives hpm clean’s orphan detection.

  • explicit_paths — absolute project paths that are always considered active.
  • search_roots — directories scanned recursively for active projects.
  • max_search_depth — recursion limit for search_roots (default 3).
  • ignore_patterns — directory names/prefixes to skip during scanning. Matches full names or prefixes, not globs.

[[registries]] — list of registries. Same shape as [[registries]] in hpm.toml.

[signing]

  • key_path — fallback Ed25519 PKCS#8 PEM path for hpm pack when neither --key nor HPM_SIGNING_KEY is set.

Storage layout

HPM stores everything under ~/.hpm/ on every supported platform. Use [storage] to change the root or individual subdirectories.

~/.hpm/
├── config.toml                      # global configuration (optional)
├── packages/                        # extracted packages (global dedupe)
│   └── creator/
│       └── slug@1.0.0/
├── venvs/                           # content-addressable Python venvs
│   └── a1b2c3d4e5f6/
│       ├── pyvenv.cfg
│       ├── lib/python3.11/site-packages/   # Lib\site-packages on Windows
│       └── metadata.json
├── global/                          # `hpm global` ledgers, one per Houdini version
│   └── houdini-21.0.json            # what hpm installed into Houdini's user prefs
├── cache/                           # download cache
├── registry/                        # registry index caches (one dir per registry)
├── tools/                           # bundled uv binary
├── uv-cache/                        # isolated uv cache (never touches your system uv)
├── uv-config/                       # isolated uv config
├── uv-python/                       # managed CPython installs (downloaded by uv on first launch)
└── logs/                            # operational logs

Per-project layout:

<project>/
├── hpm.toml
├── hpm.lock                         # pinned versions + checksums
├── .hpm/
│   ├── config.toml                  # project-level overrides (optional)
│   └── packages/                    # Houdini manifests, one per dependency
│       ├── studio.utility-nodes.json
│       └── studio.material-library.json
└── (your package sources)

Houdini integration

hpm install writes one Houdini package.json per dependency into <project>/.hpm/packages/{creator}.{slug}.json. Each file points hpath at the absolute location of the extracted package in ~/.hpm/packages/ and, for packages that declare [python_dependencies], prepends the shared venv’s site-packages onto PYTHONPATH:

{
  "hpath": ["/Users/me/.hpm/packages/studio/utility-nodes@1.0.0"],
  "env": [
    {
      "PYTHONPATH": {
        "method": "prepend",
        "value": ["/Users/me/.hpm/venvs/a1b2c3d4e5f60718/lib/python3.11/site-packages"]
      }
    }
  ],
  "enable": "houdini_version >= '20.5'"
}

method: "prepend" delegates path-separator handling to Houdini, so the same manifest works on Unix (:) and Windows (;) without HPM embedding an OS-specific joiner.

To make Houdini pick these up, add <project>/.hpm/packages to HOUDINI_PACKAGE_PATH. For a studio-wide setup, set it in the shell or in your DCC launcher; for a one-off project, set it when launching Houdini:

HOUDINI_PACKAGE_PATH="$PWD/.hpm/packages:$HOUDINI_PACKAGE_PATH" houdini

hpath points directly at the extracted package root, so Houdini auto-discovers its convention subdirectories (otls/, desktop/, toolbar/, python_panels/, viewer_states/, python3.11libs/pythonrc.py, keymaps/, …).

Project installs vs. global installs

The two differ only in where the manifest lands and what may be deleted around it:

hpm install (project)hpm global add
Manifest location<project>/.hpm/packages/Houdini’s user prefs packages/
Loaded whenHoudini is pointed at the project via HOUDINI_PACKAGE_PATHEvery session of that Houdini version
Version scopingenable expression onlyenable plus a per-version directory
Reproducible via hpm.lockYesNo — global installs are user state, not project state
Unrecognized files in the target dirSwept; hpm owns that directoryLeft alone; hpm owns only files it recorded

Use a project install for anything a team needs to reproduce. Use a global install for tools you want in your own everyday Houdini.

Output formats and automation

All commands emit human-readable output by default. The --output global flag selects a machine-readable format instead:

FormatWhen to use
humanDefault. Colored, styled for terminal use.
jsonPretty-printed JSON.
json-linesOne JSON object per line. Good for streaming and log ingestion.
json-compactSingle-line JSON. Minimal bandwidth.

Structured output is supported by list, check, update, search, and pack. Commands whose output is inherently interactive (init, add, remove, install, build, audit, run, clean, registry, global, completions) reject a non-human --output with a clear error rather than silently ignoring it.

Errors in any machine-readable format are also emitted as JSON, with fields success, error, error_type, and elapsed_ms.

A typical CI recipe:

set -e
hpm install --frozen-lockfile                 # fail if lock is stale
hpm audit                                       # warn on security issues
hpm pack --json                                 # produce archive + manifest

hpm update --dry-run --output json is useful for nightly jobs that want to detect available updates without applying them.

Troubleshooting

Package not found

Error: Package error: Package 'studio/foo' not found

Check hpm registry list — if it’s empty, add one with hpm registry add. If registries are configured, run hpm registry update and try again.

Dependency failed to resolve on install

hpm install reports the specific cause in the error chain. The two most common forms:

Package error: Failed to sync project dependencies: Cannot install foo 1.2.3:
no package registries are configured. Run 'hpm registry add <url>' to add one.
Package error: Failed to sync project dependencies: Failed to resolve foo 1.2.3
from registry: Version '1.2.3' of package 'foo' not found in registry

The first means no registry is configured — add one with hpm registry add. The second means the package or version isn’t in any configured registry: confirm the name (registries index by creator/slug, not the bare slug) and the version, then hpm registry update. Add -vv for the same detail plus a usage hint.

Houdini version mapping failed

Error: No Python version mapping for Houdini 23; supported versions are 20.5+, 21, 22.
Houdini 19.x (Python 3.7) and 20.0-20.4 (Python 3.9) are past EOL.

An unsupported [compat].houdini lower bound is a hard error rather than a silent fallback. Update hpm-core::python::collection if you need to add a new major, or set the range to a supported lower bound.

Checksum mismatch at install time

Error: Package integrity check failed: ...

Means the cached package in ~/.hpm/packages/ no longer matches the checksum recorded in hpm.lock. Either someone tampered with the cache, or the cache predates a lock-file rewrite. Remove the offending directory under ~/.hpm/packages/ and run hpm install again.

Lock file is stale

Lock file is 120 days old. Consider running 'hpm update' to check for newer versions.

Advisory warning. HPM will still install; hpm update refreshes the lock.

--frozen-lockfile fails in CI

The lock file either doesn’t exist yet or would need to change. If this is a fresh project, run hpm install locally, commit hpm.lock, and retry. If the project already has a lock and CI still fails, a dependency’s resolution has drifted — review the diff from hpm update --dry-run.

Python packages aren’t importable inside Houdini

Check that:

  1. HOUDINI_PACKAGE_PATH includes <project>/.hpm/packages.
  2. The generated .hpm/packages/{creator}.{slug}.json has a PYTHONPATH entry for the offending package.
  3. The venv directory it points to exists and contains site-packages/. If it doesn’t, remove the venv directory and re-run hpm install to rebuild it — venvs are a content-addressed cache and are always safe to delete.

Restart Houdini after any change to HOUDINI_PACKAGE_PATH.

Debug logging

RUST_LOG=debug hpm install
RUST_LOG=hpm_core=debug,hpm_core::python=trace hpm install    # per-module

Resetting state

# per-project reset
rm -rf .hpm/ hpm.lock
hpm install

# global reset (last resort)
rm -rf ~/.hpm/
hpm install                  # will re-download everything

Getting help