What Happens
tool_shadow_path (host-setup/linux/install-tools.sh:890-897) resolves a tool with type -P and
returns the result whenever it is absolute and is not $BIN_DIR/$name:
resolved=$(type -P "$name" 2>/dev/null || true)
[[ $resolved == /* && $resolved != "$BIN_DIR/$name" ]] && printf '%s' "$resolved"
type -P answers with whatever PATH resolves to at that moment. When the managed copy at
$BIN_DIR/$name does not exist, PATH can only resolve to some other copy, so this returns one
unconditionally, whether or not that copy sits ahead of $BIN_DIR. The function's name says shadow
and both callers read it as one, but the ordering that would make it a shadow is never tested.
Two callers act on the result, and each states something the check did not establish:
tool_note, line 944. With $BIN_DIR/$tool absent it takes the else branch and reports
<path> is installed outside <BIN_DIR> and keeps answering once the managed copy is installed.
That is a claim about how PATH will resolve after an install, and nothing measured it. Where the
other copy sits behind $BIN_DIR, it is false.
tool_unshadow, line 1036. It reaches dpkg-query -S and warns
<path> belongs to a distro package and stays, put <BIN_DIR> ahead of it on PATH instead, which
is advice to fix a PATH order that is already correct on any host whose PATH puts /usr/local/bin
before /usr/bin, the Debian and Ubuntu default. The comment directly above it, line 1034, says
this case is "found only when PATH puts it ahead of $BIN_DIR" -- which is precisely what
tool_shadow_path does not establish.
The Property That Triggers It
A tool in the jq | uv | git-restore-mtime arm where both hold:
$BIN_DIR/<tool> does not exist yet, and
- another copy exists in a directory PATH places behind
$BIN_DIR.
A distro package under /usr/bin is the ordinary case, because the default PATH on Debian and
Ubuntu puts /usr/local/bin first. uv under ~/.local/bin is the case where the same code path
produces a true statement, since that directory really does precede $BIN_DIR, and removing the
copy really is the fix. That is why this has not surfaced: the arm's three tools split across both
states, and only one of them is reported wrongly.
Repro
Constructed, with no managed copy installed, and using a placeholder tool name so nothing on the
host is touched:
BIN_DIR=/usr/local/bin # as install-tools.sh sets it, line 15
mkdir -p /tmp/behind
printf '#!/bin/sh\necho behind\n' > /tmp/behind/widget && chmod +x /tmp/behind/widget
# $BIN_DIR holds no widget, and /tmp/behind sits *after* $BIN_DIR on PATH.
PATH="$BIN_DIR:/tmp/behind:$PATH" type -P widget
# -> /tmp/behind/widget
# Absolute, and != $BIN_DIR/widget, so tool_shadow_path returns it and the caller reports that it
# "keeps answering once the managed copy is installed". It would not: the moment $BIN_DIR/widget
# exists, $BIN_DIR precedes /tmp/behind and the managed copy answers.
The same two lines run with PATH="/tmp/behind:$BIN_DIR:$PATH" are the genuine shadow, and
tool_shadow_path cannot tell the two apart.
Suggested Fix
Have tool_shadow_path establish the ordering it names rather than inferring it from a resolution
that cannot distinguish the two states: walk PATH and return a copy only where its directory
precedes $BIN_DIR. That splits the conflated cases into the three the callers actually need.
- A copy ahead of
$BIN_DIR: a real shadow. tool_note and tool_unshadow keep today's behavior.
- A copy behind
$BIN_DIR, with no managed copy: not a shadow. Worth no note at all, since
installing the managed copy is itself the resolution.
- A copy behind
$BIN_DIR, with a managed copy present: already nothing, and stays nothing.
Worth checking install-tools.ps1 for the same conflation before settling the shape, since the two
sides are meant to agree on what a shadow is.
What Happens
tool_shadow_path(host-setup/linux/install-tools.sh:890-897) resolves a tool withtype -Pandreturns the result whenever it is absolute and is not
$BIN_DIR/$name:type -Panswers with whatever PATH resolves to at that moment. When the managed copy at$BIN_DIR/$namedoes not exist, PATH can only resolve to some other copy, so this returns oneunconditionally, whether or not that copy sits ahead of
$BIN_DIR. The function's name says shadowand both callers read it as one, but the ordering that would make it a shadow is never tested.
Two callers act on the result, and each states something the check did not establish:
tool_note, line 944. With$BIN_DIR/$toolabsent it takes theelsebranch and reports<path> is installed outside <BIN_DIR> and keeps answering once the managed copy is installed.That is a claim about how PATH will resolve after an install, and nothing measured it. Where the
other copy sits behind
$BIN_DIR, it is false.tool_unshadow, line 1036. It reachesdpkg-query -Sand warns<path> belongs to a distro package and stays, put <BIN_DIR> ahead of it on PATH instead, whichis advice to fix a PATH order that is already correct on any host whose PATH puts
/usr/local/binbefore
/usr/bin, the Debian and Ubuntu default. The comment directly above it, line 1034, saysthis case is "found only when PATH puts it ahead of
$BIN_DIR" -- which is precisely whattool_shadow_pathdoes not establish.The Property That Triggers It
A tool in the
jq | uv | git-restore-mtimearm where both hold:$BIN_DIR/<tool>does not exist yet, and$BIN_DIR.A distro package under
/usr/binis the ordinary case, because the default PATH on Debian andUbuntu puts
/usr/local/binfirst.uvunder~/.local/binis the case where the same code pathproduces a true statement, since that directory really does precede
$BIN_DIR, and removing thecopy really is the fix. That is why this has not surfaced: the arm's three tools split across both
states, and only one of them is reported wrongly.
Repro
Constructed, with no managed copy installed, and using a placeholder tool name so nothing on the
host is touched:
The same two lines run with
PATH="/tmp/behind:$BIN_DIR:$PATH"are the genuine shadow, andtool_shadow_pathcannot tell the two apart.Suggested Fix
Have
tool_shadow_pathestablish the ordering it names rather than inferring it from a resolutionthat cannot distinguish the two states: walk PATH and return a copy only where its directory
precedes
$BIN_DIR. That splits the conflated cases into the three the callers actually need.$BIN_DIR: a real shadow.tool_noteandtool_unshadowkeep today's behavior.$BIN_DIR, with no managed copy: not a shadow. Worth no note at all, sinceinstalling the managed copy is itself the resolution.
$BIN_DIR, with a managed copy present: already nothing, and stays nothing.Worth checking
install-tools.ps1for the same conflation before settling the shape, since the twosides are meant to agree on what a shadow is.