Skip to content

feat: run activated binaries through direct links - #630

Merged
alecthomas merged 1 commit into
masterfrom
aat/direct-binary-links
Oct 20, 2026
Merged

alecthomas merged 1 commit into
masterfrom
aat/direct-binary-links

Conversation

@alecthomas

Copy link
Copy Markdown
Collaborator

Every command in an activated Hermit shell goes through a bin/ stub, which starts a Hermit process to resolve the package, check for updates, and set up the environment before running the real binary. That costs roughly 150ms per invocation (measured with just --version: ~0.15–0.18s through the stub versus under 0.01s for the binary itself), which adds up quickly in builds and scripts that call tools repeatedly.

This change makes activation build a per-environment directory of symlinks pointing straight at unpacked package binaries, and puts it on PATH directly ahead of bin/. Anything without a link falls through to its stub, so behaviour is unchanged for those binaries.

  • Location. <state>/environments/<env-name>-<env-path-hash>/<links-hash>. The directory name is a hash of the links themselves, so packages whose contents change under the same reference (channels) still get a new directory.
  • Only safe packages are linked. Packages keep their stubs when the stub does work at execution time: packages that are not unpacked yet (the stub downloads them on first use), packages with on exec triggers, runtime dependencies, or a bundled Hermit environment, and all packages from inherited parent environments (their stubs apply the parent's own variables, which the child's shell variables may override).
  • Link directories never change once created. A shell's PATH entry therefore always matches the package set its environment variables came from. A package change creates a new directory, and the existing prompt hooks switch the shell over at the next prompt.
  • Old directories are kept until hermit clean. Deleting them on package changes would break running shells and processes: bash caches resolved command paths and fails with "No such file or directory" rather than searching PATH again. hermit clean --bin removes the current environment's directories, and --packages/--transient remove all of them.
  • Side effects stay in activation. hermit activate and hermit env --activate create the directory. hermit env --ops only computes its path. Activation also checks linked channel packages for updates, because their stubs no longer run to do it.
  • zsh first-prompt hook. update_hermit_path now includes the new HERMIT_LINK_FARM entry, so it stays directly ahead of bin/.

Known gaps:

  • A package unpacked by its stub's first run is not linked until the shell re-activates, which happens on a new shell or when bin/ changes.
  • Commands on the same line as a package change, such as hermit upgrade foo && foo, still use the previous links until the next prompt. That matches how other packages' environment variables already behave.
  • When linked channel packages are due for an update check, activation now makes that network request.

Tests: unit tests in linkfarm_test.go cover which packages get linked, that EnvOps has no side effects, that an upgrade leaves the old directory unchanged, and both clean options. New integration tests, run under bash and zsh, cover:

  • running through the link directory, including upgrade, deactivation and clean
  • packages that are not unpacked falling back to stubs
  • on exec trigger packages keeping their stubs
  • channel updates being picked up at activation

The two zsh PATH-order tests were updated for the new entry. Fish is not covered locally.

🤖 Generated with Claude Code

Each bin/ stub starts a Hermit process before running the real binary,
adding roughly 150ms to every command in an activated shell.

Activation now builds a per-environment directory of symlinks straight
to the unpacked package binaries and puts it on PATH just ahead of
bin/. Anything not linked falls through to its stub. Packages that
need exec-time work keep their stubs: unpacked-on-first-use packages,
exec triggers, runtime dependencies, bundled Hermit environments, and
parent environments, whose stubs apply the parent's own variables.

Link directories are named by a hash of their links and never change,
so a shell's PATH always matches the package set its variables came
from. They are only deleted by "hermit clean", because bash caches
command paths and fails rather than searching PATH again. Activation
also checks linked channel packages for updates, as their stubs no
longer run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alecthomas
alecthomas marked this pull request as ready for review October 20, 2026 06:05
@alecthomas
alecthomas requested a review from jvmakine October 20, 2026 06:06
@alecthomas
alecthomas merged commit 396cc5f into master Oct 20, 2026
8 checks passed
@alecthomas
alecthomas deleted the aat/direct-binary-links branch October 20, 2026 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant