Files
distrobox-images/houdini/CLAUDE.md
2026-09-12 13:19:16 +02:00

13 KiB

Houdini image — design notes and debugging history

Context for whoever (Claude) touches this Dockerfile next. Simon doesn't need to read this — it's implementation reasoning, not usage docs (those are in README.md).

Why these choices

  • Base image + install command copy SideFX's own docker/Ubuntu/Dockerfile (shipped inside the Houdini tarball under docker/Ubuntu/), not guessed. That reference folder has been deleted from this repo once its contents were folded in here — it only ever served as a source to copy from, and keeping it around invited drift between two copies of the same info.
  • Ubuntu 22.04, not Rocky/RHEL. The tarball is gccXX.X-tagged (here gcc14.2), meaning the runtime needs a libstdc++ new enough for that build. Ubuntu's base image keeps libstdc++6 updated via regular package updates; RHEL-family distros pin their system toolchain for the whole OS lifetime, so a Rocky 9 base risks a GLIBCXX_x version mismatch depending on which Houdini build you're on. This was a real, not hypothetical, concern with the 22.0.429/gcc14.2 build tested here.
  • --auto-install --accept-EULA <date>, no --make-dir, no --install-license. An earlier draft of this Dockerfile invented an --install-license <path> flag that doesn't exist, based on scripted- install docs that actually describe a different tool (houdini_installer, the newer credential-based launcher — irrelevant here, since it needs network + stored SideFX credentials at build time, which is worse for a private-registry-baked-tarball workflow). The flags actually used are copy-pasted from SideFX's own Dockerfile.
  • EULA date is a plain YYYY-MM-DD, no SideFX- prefix. The SideFX-YYYY-MM-DD form belongs to houdini_installer's config file, not to houdini.install. Mixing the two up silently breaks --accept-EULA matching. Value as of writing: 2021-10-13 (from https://www.sidefx.com/legal/license-agreement/ — SideFX can update this without notice, it's not pinned/checked anywhere).
  • /opt/hfs symlink is created explicitly. houdini.install's default behavior only creates a major.minor symlink (/opt/hfs22.0 here), not a generic /opt/hfs. The Dockerfile globs for the installed hfs[0-9]* dir and symlinks it to /opt/hfs itself, so houdini-run and the /etc/profile.d snippet don't need to know the exact version.

SideFX Labs — baked in, not installed at runtime

Simon hit No such file or directory using Houdini's own in-app package installer for Labs. Root cause (confirmed, not guessed): this image never installed ca-certificates, so any HTTPS download from inside the container fails TLS verification — reproduced directly with wget against github.com inside a test build (wget exit 5, "cannot verify github.com's certificate"). The in-app installer's No such file or directory is almost certainly it shelling out to git (also never installed) for the same underlying reason: no working way to fetch anything over HTTPS. wget was already installed and used fine elsewhere in this Dockerfile, so the gap was specifically the missing CA bundle, not a missing downloader. Fixed by adding ca-certificates to the apt-get install list (base stage).

Rather than also install git and instruct Simon to run the in-app installer, Labs is fetched and baked in at build time:

  • Downloaded from a GitHub release tag (https://github.com/sideeffects/SideFXLabs/archive/refs/tags/<tag>.tar.gz), pinned via ARG SIDEFXLABS_VERSION, same pattern as HOUDINI_EULA_DATE — unpinned upstream, pick the newest tag whose target_commitish is Development and whose bundled SideFXLabs.json enable range covers this image's Houdini version. Checked via the GitHub releases API at writing time: tag 22.0.440 declares "enable": "houdini_version >= '22.0' and houdini_version < '22.5'", which covers this image's 22.0.429.
  • Extracted straight into $HFS/packages/SideFXLabs22.0/, with the vendored SideFXLabs.json copied up to $HFS/packages/SideFXLabs22.0.json unmodified. This location was not a guess: $HFS/packages is where Houdini's own shipped optional packages already live (confirmed by listing the real installed tree in a test build — apex, kinefx, ocio, sculpt, shotbuilder, texturepaint all follow the exact same <name>.json + <name>/ pattern there). It's also documented as a package search path independent of $HOME (https://www.sidefx.com/docs/houdini/ref/plugins.html), which matters specifically because distrobox bind-mounts the host's $HOME over whatever the image ships under $HOUDINI_USER_PREF_DIR — anything baked in there would be invisible at runtime. No edits to the vendored JSON were needed: its default "$HOUDINI_PACKAGE_PATH/SideFXLabs22.0" already resolves correctly, since HOUDINI_PACKAGE_PATH just means "the directory this json file itself lives in".
  • Verified end-to-end in a real build: ca-certificates fix lets wget fetch the release tarball; extracted tree has 452 .hda files under otls/; $HFS/packages/SideFXLabs22.0.json present and well-formed. Not verified against a real license + GUI launch (Apprentice activation needs a real SideFX login, out of scope for this check) — the Labs menu actually appearing in a running Houdini session is unconfirmed, though the mechanism matches Houdini's own shipped packages exactly.

Runtime library list

SideFX's own package list in docker/Ubuntu/Dockerfile is built for headless hython/sesinetd use (their docker-compose.yml only ever runs hython or sesinetd as separate services, never the GUI). Running the actual houdinifx GUI needed 4 more libs, each found by ldd-checking the real binaries/plugins in a test build and confirmed missing before adding:

  • libegl1 — EGL dispatch, houdinifx-bin linked against it directly.
  • libatomic1 — houdinifx-bin/hython-bin both needed it.
  • libxkbfile1 — same.
  • libxcb-cursor0 — this one took the longest to find. Qt6's xcb platform plugin (libqxcb.so, under /opt/hfs<ver>/dsolib/Qt_plugins/platforms/) hard-depends on it (a known Qt6 change from Qt5, where cursor theming moved from Xcursor to xcb-cursor). Without it, Houdini fails at Qt platform-plugin init with a generic "no Qt platform plugin could be initialized" — nothing points at the actual missing symbol unless you ldd the plugin directly.

If a future Houdini/Qt version adds new failures here, the fastest diagnosis path is: ldd every .so under dsolib/Qt_plugins/ recursively (see git history of this file / session transcript for the exact one-liner), not guessing from the vague top-level Qt error message.

houdini-run strict-mode compatibility

houdini_setup_bash (sourced by houdini-run to set up $HFS/PATH/etc.) is not written for set -euo pipefail:

  • It references variables that are legitimately unset in some code paths (set -u would abort).
  • It does things like d=$(which java) expecting a silent empty result on failure (set -e would abort on the failed which).

Fix: wrap the source ./houdini_setup call with set +euo pipefail / set -euo pipefail around it, rather than trying to patch the vendor script.

NixOS / distrobox host environment leakage

Distrobox forwards the host shell's environment into the container by design (needed for DISPLAY, WAYLAND_DISPLAY, XDG_*, D-Bus, etc. to work at all for GUI apps). On a NixOS host that also sets its own LD_LIBRARY_PATH (observed here pointing at a Nix-store alsa-lib built against a much newer glibc than Ubuntu 22.04 ships), that leaks into the container and crashes Houdini's binaries at dynamic-link time (GLIBC_ABI_GNU2_TLS not found, etc.) — a library meant for the host's libc, loaded into a binary linked against the container's older libc.

houdini_setup_bash only ever prepends to LD_LIBRARY_PATH if it's already set — it never resets it. So houdini-run explicitly unset LD_LIBRARY_PATH before sourcing it, discarding whatever leaked in from the host and letting Houdini build its own from scratch. This is safe on any host, not just NixOS — it just happens to matter there.

This class of problem (host env leaking into the container and colliding with container-local binaries) is worth checking first if a new, unexplained crash shows up after this image has otherwise worked before.

sesinetd / Apprentice licensing — the two real bugs

Both now handled automatically inside houdini-run (not just documented as manual steps), found via /var/log/sidefx/sesinetd.log after a genuinely confusing symptom in the GUI:

  1. License file permissions. sesinetd drops privileges to www-data after being started as root via sudo. The license file bind-mounted from the host (~/.houdini-apprentice/licenses:/usr/lib/sesi/licenses) is created with touch as the host user, so it's not writable by www-data inside the container. Result: activation's network round-trip to SideFX succeeds fine (curl-level success), but writing the resulting license locally fails — surfaced in the GUI as the deeply misleading "Success (curl error 0)" error dialog that just loops back to the install-license prompt with no indication anything is actually wrong. The real error only shows up in /var/log/sidefx/sesinetd.log: [FATAL - Licensing] [Manager] - Failed to open license file. Fix: houdini-run now chmod 666s the license file before starting sesinetd, every time, unconditionally.
  2. Stuck self-signed TLS cert. If cert generation for sesinetd's HTTPS listener gets interrupted (observed here across our own repeated container kill/restart cycles during testing), it leaves 0-byte /usr/lib/sesi/ssl/auth.cert / auth.priv behind. On next start, sesinetd tries to read those, fails (Bad certificate chain: 'PEM lib'), and — rather than regenerating — spins retrying at ~100% CPU forever, never actually finishing startup or opening port 1715. No error surfaces in the GUI for this one at all; the only symptom is a pegged CPU core and a license dialog that never succeeds. Fix: houdini-run deletes those two files first if either is 0 bytes.

Also confirmed, contra a plausible-sounding but wrong worry: SideFX's own docker README warns that regular (non-floating) licenses need special "container enabled" entitlements to run sesinetd in Docker, working around it with network_mode: host in their own docker-compose.yml. That warning is about Docker's default isolated network namespace confusing the node-lock check. Distrobox shares the host's network namespace by default (no --unshare-netns), which is the same situation as SideFX's own workaround — so this doesn't apply here, and was confirmed by an actual successful Apprentice activation.

Confidence table (as of the last real end-to-end test)

Everything below was confirmed with an actual build + an actual distrobox create + houdini-run launch on a real AMD iGPU (Vega, raven) host, not just read from docs.

Area Confidence Note
Base image + install command High Matches SideFX's own Dockerfile; built end to end against the real 22.0.429/gcc14.2 tarball.
/opt/hfs symlink High Confirmed by test build.
EULA date value Medium 2021-10-13 as of writing; unpinned, SideFX can change it.
Licensing works under distrobox's shared netns High Confirmed with a real Apprentice activation.
Runtime library list High Vendor's list + 4 libs added, all confirmed via ldd and an actual successful launch.
houdini-run strict-mode compatibility High Confirmed end to end.
sesinetd license-file-permission and stuck-cert fixes High Both bugs actually hit during testing, root-caused via /var/log/sidefx/sesinetd.log, fixed in houdini-run, confirmed working after the fix.
distrobox default hostname Medium Pinned explicitly (houdini-apprentice) rather than trusting the default, since Apprentice node-locks to hostname too.
GUI rendering via XWayland (Hyprland host) High Confirmed real AMD hardware acceleration (glxinfo: direct rendering, AMD Radeon Vega renderer, not llvmpipe). First launch was slow (container's first-run package install + cold start); later launches fluid.
AMD OpenCL (sims) Low Not set up; needs ROCm/rusticl. GL/Vulkan viewport is unaffected.
NVIDIA variant Untested Builds fine (same base, no extra packages), but never run against real NVIDIA hardware — only the amd target has been used end to end.
SideFX Labs baked into $HFS/packages Medium Build-time fetch + extraction confirmed (452 .hdas, well-formed json), and the target path matches Houdini's own shipped packages exactly — but never confirmed against a real license + GUI launch that the Labs menu actually appears.

Repo-wide conventions

See the top-level ../README.md for the multi-software repo convention (one self-contained folder per app, shared registry setup notes) — that file is meant for Simon to read; this one and the top-level ../CLAUDE.md (if one exists) are for future-Claude context.