diff --git a/docs/NPM_DISTRIBUTION.md b/docs/NPM_DISTRIBUTION.md index 53c54a8..3d76548 100644 --- a/docs/NPM_DISTRIBUTION.md +++ b/docs/NPM_DISTRIBUTION.md @@ -56,7 +56,8 @@ The binary goes in the package's `bin/`, or, when that is not writable (a root-owned `sudo npm install -g` whose postinstall npm 12 skipped), in a per-user cache: `$XDG_CACHE_HOME` (if absolute), else `~/.cache` (`~/Library/Caches` on macOS), under `//-/`. -The launcher looks in both. A +The launcher looks in both. With no absolute cache root (an empty or relative +`HOME`) there is no fallback, so nothing under the current directory is run. A termination signal during that first-run download stops the installer too, and it removes its partial file. diff --git a/scripts/install.js b/scripts/install.js index 126e985..433359a 100755 --- a/scripts/install.js +++ b/scripts/install.js @@ -23,22 +23,37 @@ const binaryName = process.platform === "win32" ? `${nativeBinary}.exe` : native // (a root-owned global install whose postinstall npm 12 skipped), in a // per-user cache. scripts/idx.js looks in both, in this order. const packageBinaryPath = path.join(packageRoot, "bin", binaryName); -// A relative XDG_CACHE_HOME is invalid per the XDG spec, and would make the -// launcher run a binary from the current directory: ignore it. -const xdgCacheHome = process.env.XDG_CACHE_HOME; -const cacheBinaryPath = path.join( - xdgCacheHome && path.isAbsolute(xdgCacheHome) - ? xdgCacheHome - : process.platform === "darwin" - ? path.join(os.homedir(), "Library", "Caches") - : path.join(os.homedir(), ".cache"), - packageName, - packageManifest.version, - // A home directory shared across machines must not mix architectures. - `${process.platform}-${process.arch}`, - binaryName, -); -const binaryLocations = [packageBinaryPath, cacheBinaryPath]; +// The cache root must be absolute: a relative one (an XDG_CACHE_HOME that the +// XDG spec calls invalid, or an empty or relative HOME) would make the +// launcher run a binary from the current directory. Without one there is no +// cache fallback. +function cacheRoot() { + const xdgCacheHome = process.env.XDG_CACHE_HOME; + if (xdgCacheHome && path.isAbsolute(xdgCacheHome)) { + return xdgCacheHome; + } + const home = os.homedir(); + if (!home || !path.isAbsolute(home)) { + return null; + } + return process.platform === "darwin" + ? path.join(home, "Library", "Caches") + : path.join(home, ".cache"); +} +const cacheDirectory = cacheRoot(); +const binaryLocations = [packageBinaryPath]; +if (cacheDirectory) { + binaryLocations.push( + path.join( + cacheDirectory, + packageName, + packageManifest.version, + // A home directory shared across machines must not mix architectures. + `${process.platform}-${process.arch}`, + binaryName, + ), + ); +} let binaryPath = packageBinaryPath; const githubRepository = "0xrsydn/idx-cli"; const maxRedirects = 5; diff --git a/scripts/npm-smoke.sh b/scripts/npm-smoke.sh index 69c5161..73d172f 100755 --- a/scripts/npm-smoke.sh +++ b/scripts/npm-smoke.sh @@ -106,6 +106,17 @@ npm_config_cache="$npm_cache" npm install --prefix "$read_only_dir" "${smoke_dir chmod -R a-w "${read_only_dir}/node_modules/idx-mcp" XDG_CACHE_HOME="${smoke_dir}/user-cache" IDX_BINARY_URL="${repo_root}/target/release/idx-mcp" \ "${read_only_dir}/node_modules/.bin/idx-mcp" --help > "${smoke_dir}/read-only-help.txt" +# Without an absolute cache root (empty HOME) there is no cache fallback, so a +# binary planted under the current directory is never run. +planted_dir="${smoke_dir}/planted" +planted="${planted_dir}/.cache/idx-mcp/$(node -p 'require("./npm/idx-mcp/package.json").version')/$(node -p 'process.platform + "-" + process.arch')/idx-mcp" +mkdir -p "$(dirname "$planted")" +printf '#!/bin/sh\necho PLANTED\n' > "$planted" +chmod +x "$planted" +if (cd "$planted_dir" && HOME='' XDG_CACHE_HOME='' "${read_only_dir}/node_modules/.bin/idx-mcp" --help 2>&1) | grep -q PLANTED; then + echo "npm smoke: the launcher ran a binary from the current directory" >&2 + exit 1 +fi chmod -R u+w "${read_only_dir}/node_modules/idx-mcp" cached=("${smoke_dir}"/user-cache/idx-mcp/*/*/idx-mcp) if ! head -n 1 "${smoke_dir}/read-only-help.txt" | grep -q "MCP server" || [[ ! -x "${cached[0]}" ]]; then