From 8524bd9c522f2e7e9946b1cc91fefc8aa1dd52b9 Mon Sep 17 00:00:00 2001 From: latenighthackathon Date: Fri, 29 May 2026 04:33:18 +0000 Subject: [PATCH] fix(install): fail fast on missing binutils at preflight (#4415) scripts/install-openshell.sh uses `strings` (from binutils) to verify the OpenShell CLI binary carries the credential-rewrite endpoints. That check only ran during OpenShell install/verification, late in the installer, so on a host without binutils the installer ran for ~5 minutes (Node.js install, repo clone, npm install, tsc build, OpenShell download + checksum) before aborting at the final verification step. Reported on a clean Ubuntu 24.04 host in #4415. Add an ensure_openshell_build_deps preflight in main(), right after ensure_docker and before any clone/build/download work, that fails fast with an actionable message (Debian/Ubuntu: sudo apt-get install -y binutils) when `strings` is absent. The check is skipped when NEMOCLAW_DEFER_OPENSHELL_INSTALL=1, since that flag postpones all OpenShell work to a later phase where install-openshell.sh runs the same check itself, leaving the pre-upgrade backup flow unaffected. Tests: two cases in test/install-preflight.test.ts assert the installer fails fast with the binutils guidance and never reaches the OpenShell install/clone when `strings` is missing, and that the preflight stays silent under NEMOCLAW_DEFER_OPENSHELL_INSTALL=1. Full install-preflight suite passes (101/101) in the nemoclaw-test container. Signed-off-by: latenighthackathon --- scripts/install.sh | 16 +++++ test/install-preflight.test.ts | 110 +++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/scripts/install.sh b/scripts/install.sh index c0ae3aa871..907c4efa36 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1157,6 +1157,21 @@ ensure_supported_runtime() { info "Runtime OK: Node.js ${node_version}, npm ${npm_version}" } +# Fail fast when a host dependency that scripts/install-openshell.sh relies on +# is missing, before any clone/build/download work. install-openshell.sh uses +# `strings` (binutils) to confirm the OpenShell CLI binary carries the +# credential-rewrite endpoints; without it the install ran for ~5 minutes +# (Node.js, clone, npm install, tsc build, OpenShell download + checksum) +# only to abort at the final verification step (#4415). Skip when the OpenShell +# install is deferred: that flag postpones all OpenShell work to a later phase +# where install-openshell.sh runs the same `strings` check itself. +ensure_openshell_build_deps() { + if truthy_env "${NEMOCLAW_DEFER_OPENSHELL_INSTALL:-}"; then + return 0 + fi + command_exists strings || error "'strings' (from binutils) is required to install and verify OpenShell. Install it first (Debian/Ubuntu: sudo apt-get install -y binutils) and re-run the installer." +} + # --------------------------------------------------------------------------- # 1. Node.js # --------------------------------------------------------------------------- @@ -2444,6 +2459,7 @@ main() { preflight_usage_notice_prompt ensure_docker + ensure_openshell_build_deps # Offer express install on supported platforms (DGX Spark / Station / WSL). # Runs AFTER the third-party notice so the user has explicitly accepted the diff --git a/test/install-preflight.test.ts b/test/install-preflight.test.ts index 1ce521dbfa..7f3c1c8268 100644 --- a/test/install-preflight.test.ts +++ b/test/install-preflight.test.ts @@ -4163,3 +4163,113 @@ sys.exit(exit_code) expect(phases).toBe(""); }); }); + +// --------------------------------------------------------------------------- +// Build-dependency preflight (#4415): missing binutils/`strings` should fail +// fast at preflight, before any clone/build/download work, instead of ~5 +// minutes in at OpenShell verification. +// --------------------------------------------------------------------------- + +/** + * Like buildIsolatedSystemPath but lets the caller exclude additional binary + * names (in addition to node/npm/npx). Used to simulate a host that is missing + * `strings` (binutils) while keeping the rest of coreutils available. + */ +function buildSystemPathExcluding(extra: readonly string[]): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-preflight-nodep-")); + const EXCLUDE = new Set(["node", "npm", "npx", ...extra]); + for (const sysDir of ["/usr/bin", "/bin"]) { + if (!fs.existsSync(sysDir)) continue; + for (const name of fs.readdirSync(sysDir)) { + if (EXCLUDE.has(name)) continue; + try { + fs.symlinkSync(path.join(sysDir, name), path.join(dir, name)); + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err; + } + } + } + return dir; +} + +/** docker stub whose `info` always succeeds, so ensure_docker passes. */ +function writeDockerOkStub(fakeBin: string) { + writeExecutable( + path.join(fakeBin, "docker"), + `#!/usr/bin/env bash +if [ "$1" = "info" ]; then exit 0; fi +exit 0 +`, + ); + writeExecutable( + path.join(fakeBin, "systemctl"), + `#!/usr/bin/env bash +if [ "$1" = "is-active" ] && [ "$2" = "docker" ]; then echo "active"; exit 0; fi +exit 0 +`, + ); +} + +describe("installer build-dependency preflight (#4415)", { timeout: 30_000 }, () => { + it("fails fast at preflight when binutils (strings) is missing, before any clone/build", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-install-no-strings-")); + const fakeBin = path.join(tmp, "bin"); + fs.mkdirSync(fakeBin); + writeNodeStub(fakeBin); + writeDockerOkStub(fakeBin); + const noStringsPath = buildSystemPathExcluding(["strings"]); + + const result = spawnSync("bash", [INSTALLER], { + cwd: path.join(import.meta.dirname, ".."), + encoding: "utf-8", + env: { + ...process.env, + HOME: tmp, + PATH: `${fakeBin}:${noStringsPath}`, + NEMOCLAW_NON_INTERACTIVE: "1", + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1", + }, + }); + + const output = `${result.stdout}${result.stderr}`; + expect(result.status).not.toBe(0); + expect(output).toMatch(/'strings' \(from binutils\) is required/); + expect(output).toMatch(/sudo apt-get install -y binutils/); + // Fail-fast guarantee: never reached the OpenShell install/verify or the + // CLI build, which is the ~5-minutes-in failure point the issue reports. + expect(output).not.toMatch(/Installing OpenShell/); + expect(output).not.toMatch(/Cloning into/); + }); + + it("does not fire the binutils preflight when OpenShell install is deferred", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-install-no-strings-deferred-")); + const fakeBin = path.join(tmp, "bin"); + fs.mkdirSync(fakeBin); + writeNodeStub(fakeBin); + // npm stub that fails fast on install, so the run stops shortly AFTER the + // (skipped) binutils preflight rather than doing real work. The assertion + // only cares that our binutils error never fires under DEFER. + writeNpmStub(fakeBin, 'echo "npm stub stop" >&2; exit 91'); + writeDockerOkStub(fakeBin); + const noStringsPath = buildSystemPathExcluding(["strings"]); + + const result = spawnSync("bash", [INSTALLER], { + cwd: path.join(import.meta.dirname, ".."), + encoding: "utf-8", + env: { + ...process.env, + HOME: tmp, + PATH: `${fakeBin}:${noStringsPath}`, + NEMOCLAW_NON_INTERACTIVE: "1", + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1", + NEMOCLAW_DEFER_OPENSHELL_INSTALL: "1", + NPM_PREFIX: path.join(tmp, "prefix"), + }, + }); + + const output = `${result.stdout}${result.stderr}`; + // The deferred path postpones all OpenShell work (and its own strings + // check) to a later phase, so the early preflight must stay silent. + expect(output).not.toMatch(/'strings' \(from binutils\) is required/); + }); +});