Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,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
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -2457,6 +2472,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
Expand Down
110 changes: 110 additions & 0 deletions test/install-preflight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4213,3 +4213,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/);
});
});
Loading