release: v1.21.1#391
Merged
Merged
Conversation
A targeted fix for npm install -g under lerd's node shim. Binaries went into a fnm version-specific bin directory that nothing has on PATH, so npm install -g pm2 succeeded but the resulting pm2 was unreachable from the user's shell. lerd now points npm's prefix at a stable per-user path under ~/.local/share/lerd/node-global/ and writes small wrapper scripts for every global binary into ~/.local/bin/, which is already on PATH because that is where the lerd binary itself lives. Wrappers exec the real bin through fnm exec --using=default so globally installed tools resolve their #!/usr/bin/env node shebang against the fnm-managed default node from any directory. npm uninstall -g cleans the wrapper alongside the package, and files in ~/.local/bin/ that lerd did not write are never touched. The node, npm, and npx shell shims in ~/.local/share/lerd/bin/ now route through the lerd binary first so the new behaviour applies to bare npm install -g and not only to lerd npm install -g, falling back to a direct fnm invocation in containers where the glibc lerd binary cannot exec. The friendly "No Node.js version available via lerd. Run: lerd node:install 22" hint surfaces through both paths. Closes #390.
The first pass landed wrappers in ~/.local/bin, which is only on PATH for users whose distro defaults add it (most XDG-aware Linux distros do) or who set it up by hand. On macOS that directory is not on PATH by default, and Homebrew installs of lerd live at /opt/homebrew/bin/lerd without ever needing ~/.local/bin to exist, so npm install -g pm2 would succeed but pm2 would stay command-not-found. The wrappers now go to config.BinDir() (~/.local/share/lerd/bin/), the directory lerd install already adds to PATH via the user's shell rc on every supported platform. The foreign-file marker check skips the existing node, npm, npx, php, composer, laravel, fnm, and mkcert shims that share the directory, so they are never overwritten by a global of the same name, and the head-only marker check still keeps the cleanup pass from removing native binaries.
composer global require had the same shape as npm install -g before this branch: composer global require psy/psysh succeeded but psysh ended up under ~/.config/composer/vendor/bin/ which is not guaranteed on PATH, especially on macOS Homebrew installs. The flow now mirrors the npm one. A new lerd composer subcommand wraps the existing lerd php composer.phar invocation and, after composer exits, syncs $COMPOSER_HOME/vendor/bin/ to ~/.local/share/lerd/bin/ as small wrapper scripts that exec the real bin through lerd php so #!/usr/bin/env php shebangs always resolve against the FPM container. The shell composer shim delegates to lerd composer first when the lerd binary is reachable, falling back to lerd php composer.phar otherwise. The two syncs coexist in the same target directory because each category uses its own marker substring (lerd-managed npm global shim and lerd-managed composer global shim). A category's cleanup pass only removes wrappers that carry its own marker, so npm and composer sync runs cannot remove each other's files. The hand-written laravel shim that lerd install drops in by default has no marker and so is treated as a foreign file by composer sync, preserving its behaviour for users who relied on it. To make sync run on every composer exit path including failed composer global remove, RunPHP grows a non-exiting sister RunPHPCapture that returns the child exit code instead of calling os.Exit. RunPHP keeps its existing exit semantics for lerd php, sail, and vendor-bin callers, and runComposer uses RunPHPCapture so the sync runs before the parent shell exit code is set. The npm-side sync helper is extracted into a generic shimSync struct in shim_sync.go that takes a marker and a wrapper-body function. syncNodeGlobalBins and the new syncComposerGlobalBins both build on it, so the marker-and-shebang protection that keeps native binaries and foreign shell scripts safe is shared.
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A v1.21.0 follow-up that fixes the long-standing orphan-bin problem for both npm install -g and composer global require.
Reported in #390: npm install -g pm2 finished cleanly but pm2 was unreachable from the shell. The binary landed in ~/.local/share/fnm/node-versions//installation/bin/ which is not on the host PATH and is not stable across node version switches anyway. lerd now points npm's prefix at a per-user path under ~/.local/share/lerd/node-global/ and, after every npm or npx invocation routed through its shim, mirrors that prefix's bin/ to a wrapper script for every executable into ~/.local/share/lerd/bin/. That target directory is the one lerd install already adds to PATH via the user's shell rc on every supported platform, so wrappers are reachable from a fresh shell whether lerd itself came from Homebrew on macOS, curl-pipe on Linux or macOS, or a packaged distro build. Wrappers exec the real bin through fnm exec --using=default so #!/usr/bin/env node shebangs resolve against the fnm-managed default node from any directory, including outside any project.
Composer had the exact same shape. composer global require psy/psysh succeeded but psysh ended up under ~/.config/composer/vendor/bin/, which is also not on PATH for most macOS setups. A new lerd composer subcommand wraps the existing lerd php composer.phar invocation and runs the same sync against composer's vendor/bin into ~/.local/share/lerd/bin/, with wrappers that exec the real bin through lerd php so #!/usr/bin/env php shebangs resolve against the FPM container's PHP. The two syncs coexist in the same target directory because each category uses its own marker substring, so a npm sync run cannot remove a composer wrapper and vice versa. Files without any lerd marker are treated as foreign and never touched, so the existing node, npm, npx, php, composer, laravel, fnm, and mkcert shims that share the directory stay safe.
To keep wrappers in step on every exit path, including a failed composer global remove that leaves the bin dir half-cleaned, RunPHP gains a non-exiting sister RunPHPCapture. runComposer uses the capturing variant, runs sync regardless of composer's exit code, then propagates the code to the parent shell. RunPHP keeps its existing exit semantics for lerd php, sail, and vendor-bin callers, so no other passthrough surface changes.
The node, npm, npx, and composer shell shims now delegate to the lerd binary first so the new behaviour applies to bare npm install -g and composer global require, not only to lerd npm and lerd composer. They fall back to the original direct-fnm and direct-composer.phar paths in containers where the glibc lerd binary cannot exec.
A small head-only marker check in the sync's cleanup pass requires every candidate for removal to start with #! before being inspected, which keeps a Go binary that happens to carry a marker substring as a string constant (the lerd binary itself is the load-bearing example here) from ever being removed during cleanup.
Upgrade path is already covered by the existing lerd update flow, which re-execs the new binary with install --from-update after the binary swap, so addShellShims rewrites every shim with the new templates. Out-of-band package upgrades (Homebrew, AUR) require the usual lerd install post-upgrade step the same way they always have.
Closes #390.