fix(installer): persist the bootstrapped Node on PATH, and stop duplicating PATH lines
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Two defects found by the second unattended greenfield run on canary (VMID 1125,
rolled back to its greenfield snapshot first).
1. ensure_node() exported the Mosaic-managed Node for the installer process and
nothing wrote it down. The install finished rc=0, put $PREFIX/bin in
~/.profile, and the next login shell found `mosaic` and then died on
env: 'node': No such file or directory
The CLI is a Node script, so a CLI on PATH without its runtime is a
successful install that produces a broken command. persist_node_on_path()
now writes the runtime's bin dir to the same profile, from both the
fresh-install and the already-installed-but-not-on-PATH branches.
2. The 'is it already in a shell rc file' guard was a single
`grep -qslF "$dir" "${rc_files[@]}"` over four paths, most of which do
not exist on a clean host. Handing grep a missing file makes the exit status
implementation-defined: GNU grep 3.11 returns 0 when -q already matched an
earlier file, ugrep 7.5 returns 2 for the missing one regardless. On the 2
path the caller reads 'not present yet' and appends another PATH line, so
every re-install grew the profile. Measured: 3 runs produced 3 duplicate
entries; with the fix, 1.
path_entry_exists() now tests each file for existence and greps it on its
own, so the result does not depend on the grep implementation.
The profile-writing body is factored into persist_on_path(), shared by the CLI
prefix and the Node runtime, since both now need identical treatment.
Verified in a scratch $HOME: fresh write, idempotent across three runs, zsh
routes to .zshenv, an unwritable profile warns and survives set -e, and an
already-on-PATH prefix is a no-op that creates no file. Falsified by restoring
the multi-file grep: duplicates return.
This commit is contained in:
+67
-23
@@ -309,52 +309,84 @@ require_cmd() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Persist $PREFIX/bin on PATH instead of only warning about it.
|
||||
# True if any shell rc file already puts $1 on PATH.
|
||||
#
|
||||
# The warning it replaces was the last step of an otherwise successful install,
|
||||
# so the installer reported success and left `mosaic: command not found` — an
|
||||
# unattended install had no operator to read the advice and act on it.
|
||||
# Each file is tested for existence first and grepped one at a time, rather than
|
||||
# handed to a single `grep -qs ... "${rc_files[@]}"`. Handing grep a missing file
|
||||
# makes the exit status implementation-defined: GNU grep 3.11 returns 0 when -q
|
||||
# matched an earlier file, ugrep 7.5 returns 2 for the missing one regardless.
|
||||
# On the 2 path the caller reads "not present yet" and appends a duplicate PATH
|
||||
# line on every single install.
|
||||
path_entry_exists() {
|
||||
local dir="$1" rc_file
|
||||
for rc_file in "$HOME/.profile" "$HOME/.zshenv" "$HOME/.zshrc" "$HOME/.bashrc"; do
|
||||
if [[ -f "$rc_file" ]] && grep -qF "$dir" "$rc_file"; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Append `export PATH="$1:$PATH"` to the shell profile so $1 survives this
|
||||
# process. An `export` here reaches only the installer; every directory the
|
||||
# install leaves behind has to be written down somewhere a later shell reads.
|
||||
#
|
||||
# Deliberately NOT ~/.bashrc: Debian's default .bashrc returns early for
|
||||
# non-interactive shells, so a PATH line appended to the bottom of it is
|
||||
# unreachable to `bash -lc`, to systemd units, and to every agent seat — the
|
||||
# exact consumers that need the CLI. ~/.profile is read by login shells and
|
||||
# Debian's .profile sources .bashrc for interactive ones, so a single line there
|
||||
# reaches both. For zsh the always-sourced file is .zshenv, not .zshrc.
|
||||
ensure_prefix_on_path() {
|
||||
if [[ ":$PATH:" == *":$PREFIX/bin:"* ]]; then
|
||||
return
|
||||
# exact consumers that need these binaries. ~/.profile is read by login shells
|
||||
# and Debian's .profile sources .bashrc for interactive ones, so a single line
|
||||
# there reaches both. For zsh the always-sourced file is .zshenv, not .zshrc.
|
||||
#
|
||||
# $1 = directory to add, $2 = label for the comment line.
|
||||
# Returns 1 (having warned) if the profile could not be written.
|
||||
persist_on_path() {
|
||||
local dir="$1" label="$2" profile
|
||||
|
||||
if path_entry_exists "$dir"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local profile rc_files=("$HOME/.profile" "$HOME/.zshenv" "$HOME/.zshrc" "$HOME/.bashrc")
|
||||
if [[ -n "${ZSH_VERSION:-}" ]] || [[ "$(basename "${SHELL:-}")" == "zsh" ]]; then
|
||||
profile="$HOME/.zshenv"
|
||||
else
|
||||
profile="$HOME/.profile"
|
||||
fi
|
||||
|
||||
if grep -qslF "$PREFIX/bin" "${rc_files[@]}" 2>/dev/null; then
|
||||
warn "$PREFIX/bin is in your shell profile but not in this shell"
|
||||
dim " Run: export PATH=\"$PREFIX/bin:\$PATH\" (or start a new login shell)"
|
||||
return
|
||||
fi
|
||||
|
||||
# Probe writability in a subshell. A redirection failure on a special built-in
|
||||
# aborts the shell it runs in, so it has to be a child; and the redirection on
|
||||
# the subshell is what silences the "Permission denied" the shell would
|
||||
# otherwise print ahead of our own message.
|
||||
if ! ( : >>"$profile" ) 2>/dev/null; then
|
||||
warn "$PREFIX/bin is not on your PATH and $profile could not be written"
|
||||
dim " Add to your shell rc: export PATH=\"$PREFIX/bin:\$PATH\""
|
||||
return
|
||||
warn "$dir is not on your PATH and $profile could not be written"
|
||||
dim " Add to your shell rc: export PATH=\"$dir:\$PATH\""
|
||||
return 1
|
||||
fi
|
||||
|
||||
{
|
||||
echo ""
|
||||
echo "# Mosaic CLI"
|
||||
echo "export PATH=\"$PREFIX/bin:\$PATH\""
|
||||
echo "# $label"
|
||||
echo "export PATH=\"$dir:\$PATH\""
|
||||
} >>"$profile"
|
||||
ok "Added $PREFIX/bin to PATH in $profile"
|
||||
ok "Added $dir to PATH in $profile"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Persist $PREFIX/bin on PATH instead of only warning about it.
|
||||
#
|
||||
# The warning it replaces was the last step of an otherwise successful install,
|
||||
# so the installer reported success and left `mosaic: command not found` — an
|
||||
# unattended install had no operator to read the advice and act on it.
|
||||
ensure_prefix_on_path() {
|
||||
if [[ ":$PATH:" == *":$PREFIX/bin:"* ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if path_entry_exists "$PREFIX/bin"; then
|
||||
warn "$PREFIX/bin is in your shell profile but not in this shell"
|
||||
elif ! persist_on_path "$PREFIX/bin" "Mosaic CLI"; then
|
||||
return
|
||||
fi
|
||||
dim " Run: export PATH=\"$PREFIX/bin:\$PATH\" (or start a new login shell)"
|
||||
}
|
||||
|
||||
@@ -670,6 +702,16 @@ install_node() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# Make the Mosaic-managed Node reachable from the next shell as well as this
|
||||
# one. Measured on a greenfield canary run: without this the install finished
|
||||
# rc=0, wrote $PREFIX/bin to ~/.profile, and the next login shell found `mosaic`
|
||||
# and then died on `env: 'node': No such file or directory` — the CLI is a Node
|
||||
# script, so a CLI on PATH without its runtime is a successful install that
|
||||
# produces a broken command.
|
||||
persist_node_on_path() {
|
||||
persist_on_path "$NODE_ROOT/current/bin" "Mosaic-managed Node.js" || true
|
||||
}
|
||||
|
||||
ensure_node() {
|
||||
if command -v node &>/dev/null && node_is_suitable node; then
|
||||
return 0
|
||||
@@ -678,6 +720,7 @@ ensure_node() {
|
||||
# A previous run may have installed one that is not on this shell's PATH.
|
||||
if node_is_suitable "$NODE_ROOT/current/bin/node"; then
|
||||
export PATH="$NODE_ROOT/current/bin:$PATH"
|
||||
persist_node_on_path
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -705,6 +748,7 @@ ensure_node() {
|
||||
fail "Could not bootstrap Node.js. Install Node.js >= $NODE_MIN_MAJOR and re-run."
|
||||
exit 1
|
||||
fi
|
||||
persist_node_on_path
|
||||
}
|
||||
|
||||
# ─── preflight ────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user