From 0e837734da039c4b45fcdeb69d9dba692c94ac21 Mon Sep 17 00:00:00 2001 From: Michiel De Smet Date: Mon, 18 May 2026 20:09:04 +0800 Subject: [PATCH 1/9] fix: embed altimate-core in standalone binary and rename to altimate (match npm primary) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The curl install at the repo root produced a binary that crashed on first run with `Cannot find module '@altimateai/altimate-core'`. Root cause: script/build.ts marked altimate-core as `external` (NAPI native modules can't live inside Bun's single-file bunfs), and the release archive shipped only the raw Bun binary — no companion node_modules, no NODE_PATH-aware wrapper. The CI smoke tests hid the bug by pre-setting NODE_PATH against the developer checkout before invoking the binary. The fix has two halves: 1. Self-contained binary (build-level fix) - Drop `@altimateai/altimate-core` from requiredExternals. - In the per-target build loop, stage a copy of the loader package and replace its NAPI-RS dispatcher with a one-line shim: module.exports = require('./altimate-core..node') Co-locate the target's `.node` file. Add a Bun.build onResolve plugin that redirects `@altimateai/altimate-core` to the staged shim. Bun statically sees a single require() and embeds that one .node into bunfs. Result: ~192 MB self-contained binary, no companion files. - Ensure every altimate-core NAPI prebuild is installed before the target loop via `bun install --os=* --cpu=*`. - Drop targets without an altimate-core prebuild from allTargets: linux-*-musl and win32-arm64. release.yml's build matrix updated to match. Re-add if/when altimate-core publishes those prebuilds. - CI smoke tests now run the binary with NODE_PATH explicitly cleared (`env -u NODE_PATH ...`). This turns the smoke test into a real regression guard for the curl-install class of bug. 2. Rename the curl-install binary altimate-code → altimate The npm package already exposes `altimate` as the primary bin and `altimate-code` as the alias (see packages/opencode/package.json bin block, and the postinstall banner). The curl install was the odd one out, installing only `altimate-code`. Align it with the npm contract: the standalone install puts `altimate` on $PATH. Drop the alias from the curl-install path — users who want `altimate-code` get it from npm or Homebrew. - `install`: APP=altimate (was altimate-code). Cascades to archive filename `altimate-.{zip,tar.gz}`, INSTALL_DIR `~/.altimate/bin`, `mv $tmp_dir/altimate $INSTALL_DIR`, and every log line / tmp-basename / check_version reference. Final getting-started block matches upstream opencode's shape (cd / altimate). Branding-only diff against anomalyco/opencode's install is now 6 non-branding lines: the file-marker line, 4 ASCII banner lines (removed), and one cosmetic getting-started line. - `script/build.ts`: archive step renames `@altimateai/altimate-code-` → `altimate-` and packs exactly one file (the renamed `altimate` binary), not the sourcemaps and not the `altimate-code` alias. The `altimate-code` copy stays inside the platform package dir because the npm wrapper (`packages/opencode/bin/altimate`) looks for `bin/altimate-code` to locate the platform binary — that flow is unchanged. - `script/publish.ts`: Homebrew + AUR formula URLs and the sha256sum paths swap to the new `altimate-` archive names. The brew formula still installs the binary as `altimate` and creates the `altimate-code` alias inside the Cellar — Homebrew users keep both names. - Tests (`branding.test.ts`, `brew-formula.test.ts`, `smoke-test-binary.test.ts`) updated to match the new binary name and archive prefix. Things that intentionally did NOT change: GitHub repo URL `AltimateAI/altimate-code`, npm package name `altimate-code`, the npm wrapper package's `bin` block. Verification on darwin-arm64: $ ls packages/opencode/dist/*.zip altimate-darwin-arm64.zip # was altimate-code-darwin-arm64.zip $ unzip -l packages/opencode/dist/altimate-darwin-arm64.zip 192306704 altimate # single file, no altimate-code # Regression test: NODE_PATH unset, from /tmp $ cd /tmp && env -u NODE_PATH .../bin/altimate --version 0.0.0-test # E2E install via --binary $ HOME=$SCRATCH bash ./install --binary .../bin/altimate $ ls $SCRATCH/.altimate/bin altimate $ env -u NODE_PATH $SCRATCH/.altimate/bin/altimate --version 0.0.0-test # Install diff vs upstream opencode — non-branding lines $ diff -u <(curl -s https://raw.githubusercontent.com/anomalyco/opencode/dev/install) install \ | grep -E '^[-+]' | grep -vE 'opencode|altimate|anomalyco|AltimateAI|OpenCode|Altimate' --- /tmp/upstream-install ... -echo -e "${MUTED} ${NC} ▄ " -echo -e "${MUTED}█▀▀█ █▀▀█ █▀▀█ █▀▀▄ ${NC}█▀▀▀ █▀▀█ █▀▀█ █▀▀█" -echo -e "${MUTED}█░░█ █░░█ █▀▀▀ █░░█ ${NC}█░░░ █░░█ █░░█ █▀▀▀" -echo -e "${MUTED}▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ${NC}▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀" +echo -e "${MUTED}To start:${NC}" # → file marker + ASCII banner removal + cosmetic "To start:" line. $ bun test --timeout 60000 test/branding/ test/install/ 400 pass / 0 fail Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release.yml | 32 +-- install | 36 ++-- packages/opencode/script/build.ts | 196 +++++++++++++++--- packages/opencode/script/publish.ts | 23 +- .../opencode/test/branding/branding.test.ts | 14 +- .../test/install/brew-formula.test.ts | 24 +-- .../test/install/smoke-test-binary.test.ts | 37 ++-- 7 files changed, 252 insertions(+), 110 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75bbc915cd..51253af7f5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,19 +59,17 @@ jobs: matrix: include: # Each target builds ONE binary in parallel (~5 min each) - # Index matches allTargets array in build.ts + # Index matches allTargets array in build.ts. Targets without a + # @altimateai/altimate-core NAPI prebuild (linux-*-musl, win32-arm64) + # are intentionally excluded — see allTargets in build.ts. - { index: 0, name: "linux-arm64" } - { index: 1, name: "linux-x64" } - { index: 2, name: "linux-x64-baseline" } - - { index: 3, name: "linux-arm64-musl" } - - { index: 4, name: "linux-x64-musl" } - - { index: 5, name: "linux-x64-baseline-musl" } - - { index: 6, name: "darwin-arm64" } - - { index: 7, name: "darwin-x64" } - - { index: 8, name: "darwin-x64-baseline" } - - { index: 9, name: "win32-arm64" } - - { index: 10, name: "win32-x64" } - - { index: 11, name: "win32-x64-baseline" } + - { index: 3, name: "darwin-arm64" } + - { index: 4, name: "darwin-x64" } + - { index: 5, name: "darwin-x64-baseline" } + - { index: 6, name: "win32-x64" } + - { index: 7, name: "win32-x64-baseline" } steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -113,10 +111,12 @@ jobs: fi chmod +x "$BINARY" - # Set NODE_PATH so the binary can resolve external NAPI modules - # (mirrors what the npm bin wrapper does at runtime) - NODE_PATH="$(pwd)/packages/opencode/node_modules:$(pwd)/node_modules" "$BINARY" --version - echo "Smoke test passed: binary starts and prints version" + # Run the binary with NO pre-set NODE_PATH. The Bun-compiled binary + # now embeds @altimateai/altimate-core's NAPI .node into bunfs, so + # it must start standalone. This is the regression gate for the + # v0.7.x curl-install crash (Cannot find module '@altimateai/altimate-core'). + env -u NODE_PATH "$BINARY" --version + echo "Smoke test passed: standalone binary starts without external NODE_PATH" - name: Upload build artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 @@ -262,7 +262,9 @@ jobs: exit 1 else chmod +x "$BINARY" - NODE_PATH="$(pwd)/packages/opencode/node_modules:$(pwd)/node_modules" "$BINARY" --version + # No NODE_PATH — the binary embeds altimate-core itself. + # This is the gate that would have caught the v0.7.x missing-module bug. + env -u NODE_PATH "$BINARY" --version echo "Pre-publish smoke test passed" fi diff --git a/install b/install index 1f425ed049..588cf01f83 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash set -euo pipefail -APP=altimate-code +APP=altimate MUTED='\033[0;2m' RED='\033[0;31m' @@ -22,7 +22,7 @@ Options: Examples: curl -fsSL https://altimate.ai/install | bash curl -fsSL https://altimate.ai/install | bash -s -- --version 1.0.180 - ./install --binary /path/to/altimate-code + ./install --binary /path/to/altimate EOF } @@ -65,7 +65,7 @@ while [[ $# -gt 0 ]]; do esac done -INSTALL_DIR=$HOME/.altimate-code/bin +INSTALL_DIR=$HOME/.altimate/bin mkdir -p "$INSTALL_DIR" # If --binary is provided, skip all download/detection logic @@ -219,11 +219,11 @@ print_message() { } check_version() { - if command -v altimate-code >/dev/null 2>&1; then - altimate_code_path=$(which altimate-code) + if command -v altimate >/dev/null 2>&1; then + altimate_path=$(which altimate) ## Check the installed version - installed_version=$(altimate-code --version 2>/dev/null || echo "") + installed_version=$(altimate --version 2>/dev/null || echo "") if [[ "$installed_version" != "$specific_version" ]]; then print_message info "${MUTED}Installed version: ${NC}$installed_version." @@ -275,7 +275,7 @@ download_with_progress() { fi local tmp_dir=${TMPDIR:-/tmp} - local basename="${tmp_dir}/altimate_code_install_$$" + local basename="${tmp_dir}/altimate_install_$$" local tracefile="${basename}.trace" rm -f "$tracefile" @@ -325,8 +325,8 @@ download_with_progress() { } download_and_install() { - print_message info "\n${MUTED}Installing ${NC}altimate-code ${MUTED}version: ${NC}$specific_version" - local tmp_dir="${TMPDIR:-/tmp}/altimate_code_install_$$" + print_message info "\n${MUTED}Installing ${NC}altimate ${MUTED}version: ${NC}$specific_version" + local tmp_dir="${TMPDIR:-/tmp}/altimate_install_$$" mkdir -p "$tmp_dir" if [[ "$os" == "windows" ]] || ! [ -t 2 ] || ! download_with_progress "$url" "$tmp_dir/$filename"; then @@ -340,15 +340,15 @@ download_and_install() { unzip -q "$tmp_dir/$filename" -d "$tmp_dir" fi - mv "$tmp_dir/altimate-code" "$INSTALL_DIR" - chmod 755 "${INSTALL_DIR}/altimate-code" + mv "$tmp_dir/altimate" "$INSTALL_DIR" + chmod 755 "${INSTALL_DIR}/altimate" rm -rf "$tmp_dir" } install_from_binary() { - print_message info "\n${MUTED}Installing ${NC}altimate-code ${MUTED}from: ${NC}$binary_path" - cp "$binary_path" "${INSTALL_DIR}/altimate-code" - chmod 755 "${INSTALL_DIR}/altimate-code" + print_message info "\n${MUTED}Installing ${NC}altimate ${MUTED}from: ${NC}$binary_path" + cp "$binary_path" "${INSTALL_DIR}/altimate" + chmod 755 "${INSTALL_DIR}/altimate" } if [ -n "$binary_path" ]; then @@ -366,9 +366,9 @@ add_to_path() { if grep -Fxq "$command" "$config_file"; then print_message info "Command already exists in $config_file, skipping write." elif [[ -w $config_file ]]; then - echo -e "\n# altimate-code" >> "$config_file" + echo -e "\n# altimate" >> "$config_file" echo "$command" >> "$config_file" - print_message info "${MUTED}Successfully added ${NC}altimate-code ${MUTED}to \$PATH in ${NC}$config_file" + print_message info "${MUTED}Successfully added ${NC}altimate ${MUTED}to \$PATH in ${NC}$config_file" else print_message warning "Manually add the directory to $config_file (or similar):" print_message info " $command" @@ -448,8 +448,8 @@ echo -e "" echo -e "" echo -e "${MUTED}To start:${NC}" echo -e "" -echo -e "cd ${MUTED}# Open your project directory${NC}" -echo -e "altimate-code ${MUTED}# Launch the interactive TUI${NC}" +echo -e "cd ${MUTED}# Open directory${NC}" +echo -e "altimate ${MUTED}# Run command${NC}" echo -e "" echo -e "${MUTED}For more information visit ${NC}https://altimate.ai" echo -e "" diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index e9cc135b17..3de1554562 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -89,6 +89,17 @@ const singleFlag = process.argv.includes("--single") const baselineFlag = process.argv.includes("--baseline") const skipInstall = process.argv.includes("--skip-install") +// Build targets are limited to the platforms for which @altimateai/altimate-core +// publishes a NAPI prebuild (see +// https://www.npmjs.com/package/@altimateai/altimate-core?activeTab=dependencies). +// Each per-target build embeds that prebuild's .node file directly into the Bun +// single-file executable so the release archive ships a single self-contained +// binary — no companion node_modules, no NODE_PATH wrapper. +// +// Combinations with no altimate-core prebuild are intentionally excluded: +// • linux-*-musl (no @altimateai/altimate-core-linux-*-musl) +// • win32-arm64 (no @altimateai/altimate-core-win32-arm64-msvc) +// If/when altimate-core ships prebuilds for those, add them back here. const allTargets: { os: string arch: "arm64" | "x64" @@ -108,22 +119,6 @@ const allTargets: { arch: "x64", avx2: false, }, - { - os: "linux", - arch: "arm64", - abi: "musl", - }, - { - os: "linux", - arch: "x64", - abi: "musl", - }, - { - os: "linux", - arch: "x64", - abi: "musl", - avx2: false, - }, { os: "darwin", arch: "arm64", @@ -137,10 +132,6 @@ const allTargets: { arch: "x64", avx2: false, }, - { - os: "win32", - arch: "arm64", - }, { os: "win32", arch: "x64", @@ -194,12 +185,14 @@ const targets = targetIndexFlag !== undefined await $`rm -rf dist` // Packages excluded from the compiled binary — must be resolvable from -// node_modules at runtime. Split into required (must ship with the wrapper -// package) and optional (user installs on demand). -const requiredExternals = [ - // NAPI native module — cannot be embedded in Bun single-file executable. - "@altimateai/altimate-core", -] +// node_modules at runtime. +// +// NOTE: @altimateai/altimate-core is intentionally NOT external. We replace +// its NAPI-RS loader with a one-line shim per target (see below) so Bun +// statically sees a single `require('./altimate-core..node')` and +// embeds that one .node file into bunfs. This keeps the binary self-contained +// without bloating it with 5 platforms' worth of native addons. +const requiredExternals: string[] = [] const optionalExternals = [ // Database drivers — native addons, users install on demand per warehouse "pg", "snowflake-sdk", "@google-cloud/bigquery", "@databricks/sql", @@ -212,6 +205,76 @@ const binaries: Record = {} if (!skipInstall) { await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}` await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}` + // Ensure every @altimateai/altimate-core platform prebuild is resolvable in + // node_modules. Each per-target build below picks one and embeds its .node + // file into the Bun binary. + await $`bun install --os="*" --cpu="*" @altimateai/altimate-core@${pkg.dependencies["@altimateai/altimate-core"]}` +} + +// Map a build target to the altimate-core NAPI prebuild package name and the +// matching `.node` file name. The mapping mirrors the lines in altimate-core's +// NAPI-RS-generated loader (e.g. `require('./altimate-core.linux-x64-gnu.node')`). +// Baseline variants share the same prebuild as their non-baseline counterpart — +// "baseline" is a Bun-binary distinction, not a NAPI one. +function altimateCorePlatformFor(item: { os: string; arch: "arm64" | "x64"; abi?: "musl" }): { + pkg: string + nodeFile: string + platformTag: string +} { + if (item.abi === "musl") { + throw new Error(`No @altimateai/altimate-core prebuild for linux-${item.arch}-musl; this target should not be in allTargets.`) + } + if (item.os === "darwin") { + const tag = `darwin-${item.arch}` + return { pkg: `@altimateai/altimate-core-${tag}`, nodeFile: `altimate-core.${tag}.node`, platformTag: tag } + } + if (item.os === "linux") { + const tag = `linux-${item.arch}-gnu` + return { pkg: `@altimateai/altimate-core-${tag}`, nodeFile: `altimate-core.${tag}.node`, platformTag: tag } + } + if (item.os === "win32") { + if (item.arch === "x64") { + const tag = "win32-x64-msvc" + return { pkg: `@altimateai/altimate-core-${tag}`, nodeFile: `altimate-core.${tag}.node`, platformTag: tag } + } + throw new Error(`No @altimateai/altimate-core prebuild for win32-${item.arch}; this target should not be in allTargets.`) + } + throw new Error(`Unsupported build target: ${item.os}-${item.arch}`) +} + +// Resolve the loader package once up-front. Real path (not the bun symlink in +// node_modules/.bun) — we copy from this for each per-target staging dir. +const altimateCoreLoaderPkgJson = fileURLToPath(import.meta.resolve("@altimateai/altimate-core/package.json")) +const altimateCoreLoaderDir = fs.realpathSync(path.dirname(altimateCoreLoaderPkgJson)) +const altimateCoreVersion = pkg.dependencies["@altimateai/altimate-core"] + +// Locate the on-disk dir for an @altimateai/altimate-core- NAPI +// prebuild. bun installs each optional platform package in its own `.bun/` +// store entry, not next to the loader, so import.meta.resolve doesn't find it +// from the workspace. Walk up from the loader dir looking for the surrounding +// `.bun` store, then look up the platform package by its conventional name. +function locatePlatformPackageDir(pkgName: string): string { + // bun's flat layout: `@scope+name@version` — only the `/` becomes `+`, the + // leading `@` of the scope stays. + const flatName = pkgName.replace("/", "+") + let current = altimateCoreLoaderDir + for (;;) { + // Match every layout we walk through: + // • current === `.bun` → check current + // • current === node_modules → check current/.bun + // • current === workspace root above node_modules → check current/node_modules/.bun + for (const prefix of [current, path.join(current, ".bun"), path.join(current, "node_modules", ".bun")]) { + const candidate = path.join(prefix, `${flatName}@${altimateCoreVersion}`, "node_modules", pkgName) + if (fs.existsSync(candidate)) return fs.realpathSync(candidate) + } + const parent = path.dirname(current) + if (parent === current) break + current = parent + } + throw new Error( + `Could not locate ${pkgName}@${altimateCoreVersion} on disk under any .bun store; ` + + `ensure 'bun install --os=* --cpu=*' has fetched it.`, + ) } for (const item of targets) { const name = [ @@ -235,10 +298,66 @@ for (const item of targets) { const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/" const workerRelativePath = path.relative(dir, parserWorker).replaceAll("\\", "/") + // ------------------------------------------------------------------------- + // Stage a per-target copy of @altimateai/altimate-core so we can embed the + // target's NAPI prebuild into the Bun single-file executable. + // + // The upstream NAPI-RS loader (index.js) dispatches at runtime across every + // supported platform — referencing each `./altimate-core..node` + // and `@altimateai/altimate-core-` from `require()`. If we hand + // that loader to Bun.build as-is, Bun statically resolves every branch and + // either bloats the binary with 5 platforms' worth of .node files or fails + // when a non-target platform package isn't on disk. + // + // Instead we replace the loader with a one-line shim: + // + // module.exports = require('./altimate-core..node') + // + // and drop the matching .node file next to it. Bun sees a single static + // require() and embeds that one .node into bunfs. Result: self-contained + // ~80–100 MB binary, no companion files, no NODE_PATH. + // ------------------------------------------------------------------------- + const platform = altimateCorePlatformFor(item) + const platformPkgDir = locatePlatformPackageDir(platform.pkg) + const platformNodeSrc = path.join(platformPkgDir, platform.nodeFile) + if (!fs.existsSync(platformNodeSrc)) { + throw new Error(`Expected NAPI prebuild not found: ${platformNodeSrc}. Did 'bun install --os=* --cpu=*' run?`) + } + + const stagedAltimateCoreDir = path.join(dir, "dist", name, ".altimate-core-staged", "@altimateai", "altimate-core") + await $`mkdir -p ${stagedAltimateCoreDir}` + // Keep index.d.ts + package.json so typecheck and resolution stay happy. + fs.copyFileSync(path.join(altimateCoreLoaderDir, "package.json"), path.join(stagedAltimateCoreDir, "package.json")) + if (fs.existsSync(path.join(altimateCoreLoaderDir, "index.d.ts"))) { + fs.copyFileSync(path.join(altimateCoreLoaderDir, "index.d.ts"), path.join(stagedAltimateCoreDir, "index.d.ts")) + } + // The shim — single static require() of the target's .node file. + fs.writeFileSync( + path.join(stagedAltimateCoreDir, "index.js"), + `// Generated by packages/opencode/script/build.ts for ${name}.\n` + + `// Replaces the multi-platform NAPI-RS loader so Bun embeds exactly one .node.\n` + + `module.exports = require('./${platform.nodeFile}')\n`, + ) + // The actual native binding, co-located so the shim's relative require() resolves. + fs.copyFileSync(platformNodeSrc, path.join(stagedAltimateCoreDir, platform.nodeFile)) + + // Bun.build plugin: rewrite @altimateai/altimate-core imports to the staged + // shim. Without this, Bun resolves the import via the workspace + // node_modules and we'd be back to the full multi-platform loader. + const stagedShimAbs = path.join(stagedAltimateCoreDir, "index.js") + const altimateCoreResolverPlugin = { + name: "altimate-core-staged-resolver", + setup(build: any) { + build.onResolve({ filter: /^@altimateai\/altimate-core$/ }, () => ({ + path: stagedShimAbs, + })) + }, + } + await Bun.build({ conditions: ["browser"], tsconfig: "./tsconfig.json", - plugins: [solidPlugin], + plugins: [solidPlugin, altimateCoreResolverPlugin], sourcemap: "external", // IMPORTANT: Without code splitting, Bun inlines dynamic import() targets // into the main chunk. Any external require() in those targets will fail @@ -269,9 +388,15 @@ for (const item of targets) { }, }) - // Create backward-compatible altimate-code alias - // Use hard copy instead of symlink — npm publish and Docker COPY can strip symlinks, - // causing "Binary not found" in Verdaccio sanity tests. + // Staging dir is no longer needed once Bun has embedded the shim + .node. + await $`rm -rf dist/${name}/.altimate-core-staged` + + // Create backward-compatible altimate-code alias inside the platform package. + // The npm wrapper (`packages/opencode/bin/altimate`) looks for `bin/altimate-code` + // (or .exe) when locating the platform binary, so this must exist for the + // `npm i -g` flow. The release archive below ships only `altimate`. + // Use a hard copy instead of a symlink — npm publish and Docker COPY can + // strip symlinks, causing "Binary not found" in Verdaccio sanity tests. if (item.os === "win32") { await $`cp dist/${name}/bin/altimate.exe dist/${name}/bin/altimate-code.exe`.nothrow() } else { @@ -327,12 +452,17 @@ for (const item of targets) { if (Script.release) { for (const key of Object.keys(binaries)) { - const archiveName = key.replace(/^@altimateai\//, "") + // Archive name maps the platform package name (`@altimateai/altimate-code-`) + // to a standalone-archive prefix (`altimate-`). The curl-install + // script (`install` at repo root) expects this prefix and unpacks a single + // binary named `altimate` — matching the primary npm bin entry. + const archiveName = key.replace(/^@altimateai\/altimate-code-/, "altimate-") const archivePath = path.resolve("dist", archiveName) + const binaryName = key.includes("win32") ? "altimate.exe" : "altimate" if (key.includes("linux")) { - await $`tar -czf ${archivePath}.tar.gz *`.cwd(`dist/${key}/bin`) + await $`tar -czf ${archivePath}.tar.gz ${binaryName}`.cwd(`dist/${key}/bin`) } else { - await $`zip -r ${archivePath}.zip *`.cwd(`dist/${key}/bin`) + await $`zip ${archivePath}.zip ${binaryName}`.cwd(`dist/${key}/bin`) } } } diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts index 516fdda868..dd44c49c12 100755 --- a/packages/opencode/script/publish.ts +++ b/packages/opencode/script/publish.ts @@ -201,13 +201,14 @@ try { // registries if (!Script.preview) { - // Calculate SHA values - const arm64Sha = await $`sha256sum ./dist/altimate-code-linux-arm64.tar.gz | cut -d' ' -f1` + // Calculate SHA values. Archive names follow the standalone-binary scheme + // (`altimate-.{tar.gz,zip}`) — see build.ts archive step. + const arm64Sha = await $`sha256sum ./dist/altimate-linux-arm64.tar.gz | cut -d' ' -f1` .text() .then((x) => x.trim()) - const x64Sha = await $`sha256sum ./dist/altimate-code-linux-x64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim()) - const macX64Sha = await $`sha256sum ./dist/altimate-code-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim()) - const macArm64Sha = await $`sha256sum ./dist/altimate-code-darwin-arm64.zip | cut -d' ' -f1` + const x64Sha = await $`sha256sum ./dist/altimate-linux-x64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim()) + const macX64Sha = await $`sha256sum ./dist/altimate-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim()) + const macArm64Sha = await $`sha256sum ./dist/altimate-darwin-arm64.zip | cut -d' ' -f1` .text() .then((x) => x.trim()) @@ -231,10 +232,10 @@ if (!Script.preview) { "conflicts=('altimate-code')", "depends=('ripgrep')", "", - `source_aarch64=("\${pkgname}_\${pkgver}_aarch64.tar.gz::https://github.com/AltimateAI/altimate-code/releases/download/v\${pkgver}\${_subver}/altimate-code-linux-arm64.tar.gz")`, + `source_aarch64=("\${pkgname}_\${pkgver}_aarch64.tar.gz::https://github.com/AltimateAI/altimate-code/releases/download/v\${pkgver}\${_subver}/altimate-linux-arm64.tar.gz")`, `sha256sums_aarch64=('${arm64Sha}')`, - `source_x86_64=("\${pkgname}_\${pkgver}_x86_64.tar.gz::https://github.com/AltimateAI/altimate-code/releases/download/v\${pkgver}\${_subver}/altimate-code-linux-x64.tar.gz")`, + `source_x86_64=("\${pkgname}_\${pkgver}_x86_64.tar.gz::https://github.com/AltimateAI/altimate-code/releases/download/v\${pkgver}\${_subver}/altimate-linux-x64.tar.gz")`, `sha256sums_x86_64=('${x64Sha}')`, "", "package() {", @@ -280,7 +281,7 @@ if (!Script.preview) { "", " on_macos do", " if Hardware::CPU.intel?", - ` url "https://github.com/AltimateAI/altimate-code/releases/download/v${Script.version}/altimate-code-darwin-x64.zip"`, + ` url "https://github.com/AltimateAI/altimate-code/releases/download/v${Script.version}/altimate-darwin-x64.zip"`, ` sha256 "${macX64Sha}"`, "", " def install", @@ -289,7 +290,7 @@ if (!Script.preview) { " end", " end", " if Hardware::CPU.arm?", - ` url "https://github.com/AltimateAI/altimate-code/releases/download/v${Script.version}/altimate-code-darwin-arm64.zip"`, + ` url "https://github.com/AltimateAI/altimate-code/releases/download/v${Script.version}/altimate-darwin-arm64.zip"`, ` sha256 "${macArm64Sha}"`, "", " def install", @@ -301,7 +302,7 @@ if (!Script.preview) { "", " on_linux do", " if Hardware::CPU.intel? and Hardware::CPU.is_64_bit?", - ` url "https://github.com/AltimateAI/altimate-code/releases/download/v${Script.version}/altimate-code-linux-x64.tar.gz"`, + ` url "https://github.com/AltimateAI/altimate-code/releases/download/v${Script.version}/altimate-linux-x64.tar.gz"`, ` sha256 "${x64Sha}"`, " def install", ' bin.install "altimate"', @@ -309,7 +310,7 @@ if (!Script.preview) { " end", " end", " if Hardware::CPU.arm? and Hardware::CPU.is_64_bit?", - ` url "https://github.com/AltimateAI/altimate-code/releases/download/v${Script.version}/altimate-code-linux-arm64.tar.gz"`, + ` url "https://github.com/AltimateAI/altimate-code/releases/download/v${Script.version}/altimate-linux-arm64.tar.gz"`, ` sha256 "${arm64Sha}"`, " def install", ' bin.install "altimate"', diff --git a/packages/opencode/test/branding/branding.test.ts b/packages/opencode/test/branding/branding.test.ts index 69357c0ef0..95b8a7b698 100644 --- a/packages/opencode/test/branding/branding.test.ts +++ b/packages/opencode/test/branding/branding.test.ts @@ -91,16 +91,22 @@ describe("CLI Branding", () => { describe("Installation Script", () => { const installContent = readText(join(repoRoot, "install")) - test("APP variable is altimate-code", () => { - expect(installContent).toContain("APP=altimate-code") + test("APP variable is altimate", () => { + // APP is the standalone-archive prefix AND the installed binary name — + // matches the primary `altimate` npm bin, not the legacy `altimate-code` + // alias. The GitHub repo URL stays `AltimateAI/altimate-code` (covered by + // the next test) — only the binary name and archive prefix changed. + expect(installContent).toContain("APP=altimate") + expect(installContent).not.toContain("APP=altimate-code") }) test("GitHub release URL references AltimateAI/altimate-code", () => { expect(installContent).toContain("github.com/AltimateAI/altimate-code/releases") }) - test("install dir is .altimate-code", () => { - expect(installContent).toContain(".altimate-code/bin") + test("install dir is .altimate/bin", () => { + expect(installContent).toContain(".altimate/bin") + expect(installContent).not.toContain(".altimate-code/bin") }) test("no references to opencode.ai domain", () => { diff --git a/packages/opencode/test/install/brew-formula.test.ts b/packages/opencode/test/install/brew-formula.test.ts index e2d3c5b480..0108919755 100644 --- a/packages/opencode/test/install/brew-formula.test.ts +++ b/packages/opencode/test/install/brew-formula.test.ts @@ -37,7 +37,7 @@ function generateBrewFormula(opts: { "", " on_macos do", " if Hardware::CPU.intel?", - ` url "https://github.com/AltimateAI/altimate-code/releases/download/v${opts.version}/altimate-code-darwin-x64.zip"`, + ` url "https://github.com/AltimateAI/altimate-code/releases/download/v${opts.version}/altimate-darwin-x64.zip"`, ` sha256 "${opts.macX64Sha}"`, "", " def install", @@ -46,7 +46,7 @@ function generateBrewFormula(opts: { " end", " end", " if Hardware::CPU.arm?", - ` url "https://github.com/AltimateAI/altimate-code/releases/download/v${opts.version}/altimate-code-darwin-arm64.zip"`, + ` url "https://github.com/AltimateAI/altimate-code/releases/download/v${opts.version}/altimate-darwin-arm64.zip"`, ` sha256 "${opts.macArm64Sha}"`, "", " def install", @@ -58,7 +58,7 @@ function generateBrewFormula(opts: { "", " on_linux do", " if Hardware::CPU.intel? and Hardware::CPU.is_64_bit?", - ` url "https://github.com/AltimateAI/altimate-code/releases/download/v${opts.version}/altimate-code-linux-x64.tar.gz"`, + ` url "https://github.com/AltimateAI/altimate-code/releases/download/v${opts.version}/altimate-linux-x64.tar.gz"`, ` sha256 "${opts.x64Sha}"`, " def install", ' bin.install "altimate"', @@ -66,7 +66,7 @@ function generateBrewFormula(opts: { " end", " end", " if Hardware::CPU.arm? and Hardware::CPU.is_64_bit?", - ` url "https://github.com/AltimateAI/altimate-code/releases/download/v${opts.version}/altimate-code-linux-arm64.tar.gz"`, + ` url "https://github.com/AltimateAI/altimate-code/releases/download/v${opts.version}/altimate-linux-arm64.tar.gz"`, ` sha256 "${opts.arm64Sha}"`, " def install", ' bin.install "altimate"', @@ -104,10 +104,10 @@ describe("brew formula template", () => { }) test("all 4 platform variants are present", () => { - expect(formula).toContain("altimate-code-darwin-x64.zip") - expect(formula).toContain("altimate-code-darwin-arm64.zip") - expect(formula).toContain("altimate-code-linux-x64.tar.gz") - expect(formula).toContain("altimate-code-linux-arm64.tar.gz") + expect(formula).toContain("altimate-darwin-x64.zip") + expect(formula).toContain("altimate-darwin-arm64.zip") + expect(formula).toContain("altimate-linux-x64.tar.gz") + expect(formula).toContain("altimate-linux-arm64.tar.gz") }) test("macOS uses .zip format", () => { @@ -184,9 +184,9 @@ describe("publish.ts brew formula template matches test template", () => { test("publish.ts uses Script.version in URL construction", () => { // URLs must use v${Script.version} — not vv or raw tag - expect(PUBLISH_SCRIPT).toContain("v${Script.version}/altimate-code-darwin-x64.zip") - expect(PUBLISH_SCRIPT).toContain("v${Script.version}/altimate-code-darwin-arm64.zip") - expect(PUBLISH_SCRIPT).toContain("v${Script.version}/altimate-code-linux-x64.tar.gz") - expect(PUBLISH_SCRIPT).toContain("v${Script.version}/altimate-code-linux-arm64.tar.gz") + expect(PUBLISH_SCRIPT).toContain("v${Script.version}/altimate-darwin-x64.zip") + expect(PUBLISH_SCRIPT).toContain("v${Script.version}/altimate-darwin-arm64.zip") + expect(PUBLISH_SCRIPT).toContain("v${Script.version}/altimate-linux-x64.tar.gz") + expect(PUBLISH_SCRIPT).toContain("v${Script.version}/altimate-linux-arm64.tar.gz") }) }) diff --git a/packages/opencode/test/install/smoke-test-binary.test.ts b/packages/opencode/test/install/smoke-test-binary.test.ts index a86660fb6b..3390d3ab68 100644 --- a/packages/opencode/test/install/smoke-test-binary.test.ts +++ b/packages/opencode/test/install/smoke-test-binary.test.ts @@ -2,11 +2,14 @@ * Smoke tests for compiled binaries. * * These tests build a local binary (--single) and verify it actually starts - * with the required external NAPI modules resolvable via NODE_PATH. + * — both with NODE_PATH set (matches the npm bin wrapper environment) and + * with NODE_PATH cleared (matches the curl-install / Homebrew / AUR / GitHub + * release archive environment). * - * This is the test that would have caught the v0.5.10 regression where - * @altimateai/altimate-core was marked external but missing from standalone - * distributions, causing an immediate crash on startup. + * The "NODE_PATH cleared" test is the regression guard for the v0.7.x + * curl-install crash: the Bun-compiled binary now embeds altimate-core's + * NAPI .node into bunfs, so the standalone binary must start without any + * companion files. * * Run: bun test test/install/smoke-test-binary.test.ts * @@ -90,10 +93,13 @@ describe("compiled binary smoke test", () => { expect(result.stderr).not.toContain("Cannot find module") }) - runTest("binary fails gracefully without NODE_PATH (standalone mode)", () => { - // Simulate standalone distribution — no node_modules available. - // The binary should NOT crash with an unhandled error; it should - // either degrade gracefully or show a clear error message. + runTest("binary succeeds with NODE_PATH cleared (standalone mode)", () => { + // The Bun-compiled binary embeds @altimateai/altimate-core's NAPI .node + // directly into bunfs (see script/build.ts — staged shim + resolver + // plugin). It MUST start without any external NODE_PATH or companion + // node_modules. This is the regression guard for the v0.7.x curl-install + // crash where altimate-core was marked `external` and the standalone + // archive shipped without it. const result = spawnSync(binary!, ["--version"], { encoding: "utf-8", timeout: 15_000, @@ -101,21 +107,18 @@ describe("compiled binary smoke test", () => { PATH: process.env.PATH, HOME: process.env.HOME, OPENCODE_DISABLE_TELEMETRY: "1", - // Explicitly clear NODE_PATH to simulate standalone + // Explicitly clear NODE_PATH to simulate the curl-install layout NODE_PATH: "", }, }) - // Process must have exited (not been killed by timeout) - expect(result.status).not.toBeNull() - - // If it fails, the error should mention the missing module clearly if (result.status !== 0) { - const output = (result.stdout ?? "") + (result.stderr ?? "") - expect(output).toContain("altimate-core") + console.error("STDOUT:", result.stdout) + console.error("STDERR:", result.stderr) } - // Either way, it should not segfault (exit code > 128 means signal) - expect(result.status!).toBeLessThanOrEqual(128) + expect(result.status).toBe(0) + const output = (result.stdout ?? "") + (result.stderr ?? "") + expect(output).not.toContain("Cannot find module") }) runTest("binary responds to --help", () => { From 32a17c015bf71d1cb254eb0ae0de1e029b926ad4 Mon Sep 17 00:00:00 2001 From: Michiel De Smet Date: Mon, 18 May 2026 20:33:23 +0800 Subject: [PATCH 2/9] fix: archive step matched 'win32' but key uses 'windows' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `name` builder at build.ts:283 substitutes `win32 → windows` when constructing the per-target directory name, so the keys in the `binaries` map (e.g. `@altimateai/altimate-code-windows-x64`) never contain the literal substring `win32`. The archive step at line 461 was using `key.includes("win32")` to pick `altimate.exe` vs `altimate`, which evaluated false on every Windows target and caused the zip step to look for a non-existent `altimate` file on Windows. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/opencode/script/build.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 3de1554562..9f79990a6d 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -458,7 +458,10 @@ if (Script.release) { // binary named `altimate` — matching the primary npm bin entry. const archiveName = key.replace(/^@altimateai\/altimate-code-/, "altimate-") const archivePath = path.resolve("dist", archiveName) - const binaryName = key.includes("win32") ? "altimate.exe" : "altimate" + // Name construction at line 283 substitutes `win32 → windows`, so the key + // contains "windows", not "win32". Matching the wrong substring here would + // archive a non-existent `altimate` file on Windows targets. + const binaryName = key.includes("windows") ? "altimate.exe" : "altimate" if (key.includes("linux")) { await $`tar -czf ${archivePath}.tar.gz ${binaryName}`.cwd(`dist/${key}/bin`) } else { From 18dd4c6f1f22f8273b9c70f23aa48c8f1f18441a Mon Sep 17 00:00:00 2001 From: Michiel De Smet Date: Mon, 18 May 2026 21:33:15 +0800 Subject: [PATCH 3/9] fix: install script handles altimate.exe on windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C1 from PR #820 review. The install script hard-coded `mv "$tmp_dir/altimate"` and `chmod ... "$INSTALL_DIR/altimate"` for every platform, but the Windows release archive contains `altimate.exe`. Result on Windows: the script tries to move a file that doesn't exist (download path) or installs a file under the wrong name with no `.exe` extension (--binary path). Fix: - Set `binary_name="$APP"` (with `.exe` suffix on windows) right after computing the target triple, then route mv/chmod through `$binary_name` in `download_and_install`. - In `install_from_binary` use the caller's basename so the installed file matches what was supplied — also handles the `.exe` case naturally and doesn't break the existing non-windows callers. Local verification (darwin-arm64 host, simulating windows path): $ binary_name="$APP"; [ "$os" = "windows" ] && binary_name="$APP.exe" $ cp /tmp/c1-test/$binary_name "${INSTALL_DIR}/$binary_name" $ ls "${INSTALL_DIR}" altimate.exe # ← extension preserved end-to-end $ bash ./install --binary /tmp/c1-test/altimate.exe --no-modify-path $ ls $HOME/.altimate/bin altimate.exe # ← --binary path preserves the .exe basename $ bash ./install --binary /tmp/c1-test/altimate --no-modify-path $ ls $HOME/.altimate/bin altimate # ← non-.exe path unchanged Co-Authored-By: Claude Opus 4.7 (1M context) --- install | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/install b/install index 588cf01f83..e63089e72e 100755 --- a/install +++ b/install @@ -166,6 +166,11 @@ else fi filename="$APP-$target$archive_ext" + # Windows release archives ship altimate.exe, every other target ships altimate. + binary_name="$APP" + if [ "$os" = "windows" ]; then + binary_name="$APP.exe" + fi if [ "$os" = "linux" ]; then @@ -340,15 +345,19 @@ download_and_install() { unzip -q "$tmp_dir/$filename" -d "$tmp_dir" fi - mv "$tmp_dir/altimate" "$INSTALL_DIR" - chmod 755 "${INSTALL_DIR}/altimate" + mv "$tmp_dir/$binary_name" "${INSTALL_DIR}/$binary_name" + chmod 755 "${INSTALL_DIR}/$binary_name" rm -rf "$tmp_dir" } install_from_binary() { - print_message info "\n${MUTED}Installing ${NC}altimate ${MUTED}from: ${NC}$binary_path" - cp "$binary_path" "${INSTALL_DIR}/altimate" - chmod 755 "${INSTALL_DIR}/altimate" + # --binary may point to either altimate or altimate.exe; preserve the + # caller's basename so the installed file matches what was supplied. + local dest_name + dest_name=$(basename "$binary_path") + print_message info "\n${MUTED}Installing ${NC}$dest_name ${MUTED}from: ${NC}$binary_path" + cp "$binary_path" "${INSTALL_DIR}/$dest_name" + chmod 755 "${INSTALL_DIR}/$dest_name" } if [ -n "$binary_path" ]; then From 4fc156e349460284def7ae12cf9b499d8069b2cc Mon Sep 17 00:00:00 2001 From: Michiel De Smet Date: Mon, 18 May 2026 21:36:06 +0800 Subject: [PATCH 4/9] fix: hard-error on musl + win32-arm64 across all install paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C2 from PR #820 review. The previous PR dropped linux-*-musl and win32-arm64 from the build matrix (no @altimateai/altimate-core NAPI prebuild exists), but left three runtime paths that still tried to fetch / resolve archives for those platforms and emitted misleading errors: 1. The curl install script's musl detection sets is_musl=true and constructs `altimate-linux-x64-musl.tar.gz`. The release doesn't ship that archive, so the download 404s. curl without `--fail` writes the GitHub 404 HTML to disk; tar -xzf then dies with "not in gzip format". Fail fast with an actionable message (`apk add gcompat`, or use npm) before URL construction. 2. The npm wrapper at packages/opencode/bin/altimate has a names[] array that fans out to ${base}-musl / ${base}-baseline-musl as fallbacks. Those packages are no longer published. When findBinary() exhausts the list it prints "your package manager failed to install the right version of the altimate-code CLI" — wrong diagnosis. Hard-error on musl and win32-arm64 before findBinary() runs, with the same actionable message. 3. The npm postinstall script's findBinary() emits a generic "Could not find package @altimateai/altimate-code--" via the require catch path on these systems. Add the same explicit musl/win32-arm64 detection there so npm install fails with a clear message. Also add `curl --fail` to both download paths (the trace-enabled progress fetch and the simpler `-#` fallback). Without it, a future 404 on any unknown target archive silently writes the error page to disk and bombs later in tar/unzip. Verified locally (sed-patched is_musl=true to force the path on a darwin host): $ bash /tmp/install-musl-sim --version 0.1.0 Alpine Linux (musl) is not currently supported by the standalone install. altimate-core has no NAPI prebuild for musl yet. Workarounds: • apk add gcompat # run glibc binaries on Alpine • Use npm: npm install -g altimate-code $ echo $? 1 Co-Authored-By: Claude Opus 4.7 (1M context) --- install | 25 ++++++--- packages/opencode/bin/altimate | 67 ++++++++++++++---------- packages/opencode/script/postinstall.mjs | 34 ++++++++++++ 3 files changed, 91 insertions(+), 35 deletions(-) diff --git a/install b/install index e63089e72e..93c726a716 100755 --- a/install +++ b/install @@ -127,6 +127,18 @@ else fi fi + # @altimateai/altimate-core has no NAPI prebuild for musl. Without a + # musl-shaped archive, the install would 404 silently (without --fail + # curl writes the 404 HTML to disk, and tar then dies "not in gzip + # format"). Fail fast with an actionable message instead. + if [ "$is_musl" = "true" ]; then + echo -e "${RED}Alpine Linux (musl) is not currently supported by the standalone install.${NC}" + echo -e "${MUTED}altimate-core has no NAPI prebuild for musl yet. Workarounds:${NC}" + echo -e " • apk add gcompat ${MUTED}# run glibc binaries on Alpine${NC}" + echo -e " • Use npm: npm install -g altimate-code" + exit 1 + fi + needs_baseline=false if [ "$arch" = "x64" ]; then if [ "$os" = "linux" ]; then @@ -161,9 +173,7 @@ else if [ "$needs_baseline" = "true" ]; then target="$target-baseline" fi - if [ "$is_musl" = "true" ]; then - target="$target-musl" - fi + # is_musl=true would have exited above — no musl target suffix is constructed. filename="$APP-$target$archive_ext" # Windows release archives ship altimate.exe, every other target ships altimate. @@ -292,7 +302,9 @@ download_with_progress() { trap "trap - RETURN; rm -f \"$tracefile\"; printf '\033[?25h' >&4; exec 4>&-" RETURN ( - curl --trace-ascii "$tracefile" -s -L -o "$output" "$url" + # --fail so HTTP errors (e.g. 404 on an unknown target archive) become + # a non-zero exit instead of silently writing the error page to disk. + curl --fail --trace-ascii "$tracefile" -s -L -o "$output" "$url" ) & local curl_pid=$! @@ -335,8 +347,9 @@ download_and_install() { mkdir -p "$tmp_dir" if [[ "$os" == "windows" ]] || ! [ -t 2 ] || ! download_with_progress "$url" "$tmp_dir/$filename"; then - # Fallback to standard curl on Windows, non-TTY environments, or if custom progress fails - curl -# -L -o "$tmp_dir/$filename" "$url" + # Fallback to standard curl on Windows, non-TTY environments, or if custom progress fails. + # --fail so 404s exit non-zero instead of writing the error page to $tmp_dir/$filename. + curl --fail -# -L -o "$tmp_dir/$filename" "$url" fi if [ "$os" = "linux" ]; then diff --git a/packages/opencode/bin/altimate b/packages/opencode/bin/altimate index b441d008db..3ae3597799 100755 --- a/packages/opencode/bin/altimate +++ b/packages/opencode/bin/altimate @@ -153,42 +153,51 @@ function supportsAvx2() { return false } +function isMusl() { + if (platform !== "linux") return false + try { + if (fs.existsSync("/etc/alpine-release")) return true + } catch { + // ignore + } + try { + const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" }) + const text = ((result.stdout || "") + (result.stderr || "")).toLowerCase() + if (text.includes("musl")) return true + } catch { + // ignore + } + return false +} + +// @altimateai/altimate-core has no NAPI prebuild for musl or win32-arm64, +// and the altimate binary embeds altimate-core's .node file at build time. +// Hard-error early instead of letting findBinary() walk the whole tree and +// emit a misleading "package manager failed to install the right version" +// message — the right diagnosis is that these platforms aren't built. +if (isMusl()) { + console.error("altimate-code is not currently supported on Alpine Linux (musl).") + console.error("Workarounds:") + console.error(" • apk add gcompat # run glibc binaries on Alpine") + console.error(" • Use a glibc-based container (debian/ubuntu/alpine+gcompat)") + process.exit(1) +} +if (platform === "windows" && arch === "arm64") { + console.error("altimate-code is not currently built for Windows on ARM64.") + console.error("Run the x64 build under Windows ARM's x64 emulation, or use WSL.") + process.exit(1) +} + const names = (() => { const avx2 = supportsAvx2() const baseline = arch === "x64" && !avx2 if (platform === "linux") { - const musl = (() => { - try { - if (fs.existsSync("/etc/alpine-release")) return true - } catch { - // ignore - } - - try { - const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" }) - const text = ((result.stdout || "") + (result.stderr || "")).toLowerCase() - if (text.includes("musl")) return true - } catch { - // ignore - } - - return false - })() - - if (musl) { - if (arch === "x64") { - if (baseline) return [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base] - return [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`] - } - return [`${base}-musl`, base] - } - if (arch === "x64") { - if (baseline) return [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`] - return [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`] + if (baseline) return [`${base}-baseline`, base] + return [base, `${base}-baseline`] } - return [base, `${base}-musl`] + return [base] } if (arch === "x64") { diff --git a/packages/opencode/script/postinstall.mjs b/packages/opencode/script/postinstall.mjs index 98d13e60c5..487503fadd 100644 --- a/packages/opencode/script/postinstall.mjs +++ b/packages/opencode/script/postinstall.mjs @@ -47,8 +47,42 @@ function detectPlatformAndArch() { return { platform, arch } } +function isMuslPlatform() { + if (os.platform() !== "linux") return false + try { + if (fs.existsSync("/etc/alpine-release")) return true + } catch { + // ignore + } + try { + const out = require("child_process").execSync("ldd --version", { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }) + if (typeof out === "string" && out.toLowerCase().includes("musl")) return true + } catch { + // ignore — ldd may not exist or may exit non-zero + } + return false +} + function findBinary() { const { platform, arch } = detectPlatformAndArch() + + // @altimateai/altimate-core has no NAPI prebuild for musl or win32-arm64, + // and the altimate binary embeds altimate-core's .node at build time. Emit + // a clear, actionable error here rather than the generic + // "Could not find package" message that would otherwise fall out below. + if (isMuslPlatform()) { + throw new Error( + "altimate-code is not currently supported on Alpine Linux (musl). " + + "Run 'apk add gcompat' to execute glibc binaries on Alpine, or use a glibc-based base image.", + ) + } + if (platform === "windows" && arch === "arm64") { + throw new Error( + "altimate-code is not currently built for Windows on ARM64. " + + "Run the x64 build under Windows ARM's x64 emulation, or use WSL.", + ) + } + const packageName = `@altimateai/altimate-code-${platform}-${arch}` const binaryName = platform === "windows" ? "altimate-code.exe" : "altimate-code" From 154cb6c96522a437fd39d0c1d1ff22a078faa807 Mon Sep 17 00:00:00 2001 From: Michiel De Smet Date: Mon, 18 May 2026 21:39:02 +0800 Subject: [PATCH 5/9] fix: hermetic smoke test + content-level assertion on embedded .node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M1 from PR #820 review. The previous standalone-mode smoke test spawned the binary with NODE_PATH="" but inherited the worktree as cwd. Bun's compiled binary, when resolving an unbundled require, walks the filesystem from process.execPath upward — it doesn't actually need NODE_PATH if there's a node_modules anywhere in the parent chain. So if the staged-shim onResolve plugin ever silently fails to redirect @altimateai/altimate-core, the binary falls back to filesystem resolution and finds altimate-core in the worktree's node_modules. Test passes for the wrong reason. Two fixes: 1. Make the runtime test hermetic. Set cwd=os.tmpdir() in spawnSync so the binary cannot walk upward into the worktree node_modules tree. Mirror the change in release.yml smoke tests (build-time and pre-publish): resolve $BINARY to an absolute path first, then `cd "${RUNNER_TEMP:-/tmp}"` before invoking the binary. 2. Add an independent content-level assertion. Run `strings` against the binary and count distinct altimate-core..node references (after collapsing the bunfs hash suffix). If the staged shim ever silently fails and Bun pulls in the upstream multi- platform NAPI-RS loader, every platform's .node name leaks into bunfs and this test catches it independently of any runtime path. The bunfs hash-suffix regex needed a wider character class than the review's `[a-f0-9]+`: Bun uses an alphanumeric (not hex) suffix. With `[a-f0-9]+` the suffix `darwin-arm64-ptxrnv5e.node` doesn't strip (contains p/t/x/r/n/v) and the test sees 2 distinct names. Use `[a-z0-9]{6,}` instead — Bun's hash is 7+ chars; real platform last-segments (arm64/x64/gnu/msvc) are all ≤5 chars so the length bound is unambiguous. Local verification on darwin-arm64: $ bun test test/install/smoke-test-binary.test.ts 4 pass / 0 fail (was 3; added "binary embeds exactly one .node") $ strings .../bin/altimate | grep -oE 'altimate-core\\.(...).*\\.node' | sort -u altimate-core.darwin-arm64-ptxrnv5e.node # bunfs entry altimate-core.darwin-arm64.node # require() string # → collapse to 1 distinct after hash strip. Test passes. The Windows path in the content-level assertion no-ops because the `strings` binutil isn't on a stock Windows runner; Linux + macOS CI (where the build matrix actually runs) is fully covered. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release.yml | 24 +++++++---- .../test/install/smoke-test-binary.test.ts | 43 ++++++++++++++++++- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 51253af7f5..84594be2e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -104,19 +104,22 @@ jobs: - name: Smoke test binary if: matrix.name == 'linux-x64' run: | - BINARY=$(find packages/opencode/dist -name altimate -type f | head -1) + # Resolve to an absolute path before we cd away from the workspace. + BINARY=$(find "$(pwd)/packages/opencode/dist" -name altimate -type f | head -1) if [ -z "$BINARY" ]; then echo "::error::No binary found in dist/" exit 1 fi chmod +x "$BINARY" - # Run the binary with NO pre-set NODE_PATH. The Bun-compiled binary - # now embeds @altimateai/altimate-core's NAPI .node into bunfs, so - # it must start standalone. This is the regression gate for the - # v0.7.x curl-install crash (Cannot find module '@altimateai/altimate-core'). + # Run with NO pre-set NODE_PATH AND from a directory with no + # node_modules anywhere upward. Bun's compiled binary would + # otherwise walk the workspace tree for node_modules and resolve + # altimate-core that way — the test would pass for the wrong + # reason if the staged shim ever silently misses. + cd "${RUNNER_TEMP:-/tmp}" env -u NODE_PATH "$BINARY" --version - echo "Smoke test passed: standalone binary starts without external NODE_PATH" + echo "Smoke test passed: standalone binary starts hermetically" - name: Upload build artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 @@ -256,14 +259,17 @@ jobs: # compile-time checks miss (e.g. missing NAPI externals like v0.5.10). - name: Pre-publish smoke test run: | - BINARY=$(find packages/opencode/dist -path '*altimate-code-linux-x64/bin/altimate' -type f | head -1) + # Resolve to an absolute path before we cd away from the workspace. + BINARY=$(find "$(pwd)/packages/opencode/dist" -path '*altimate-code-linux-x64/bin/altimate' -type f | head -1) if [ -z "$BINARY" ]; then echo "::error::No linux-x64 binary found in artifacts — cannot verify release" exit 1 else chmod +x "$BINARY" - # No NODE_PATH — the binary embeds altimate-core itself. - # This is the gate that would have caught the v0.7.x missing-module bug. + # No NODE_PATH AND hermetic cwd: see the build-time smoke test + # comment for why this matters. The binary must start without + # walking the workspace for node_modules. + cd "${RUNNER_TEMP:-/tmp}" env -u NODE_PATH "$BINARY" --version echo "Pre-publish smoke test passed" fi diff --git a/packages/opencode/test/install/smoke-test-binary.test.ts b/packages/opencode/test/install/smoke-test-binary.test.ts index 3390d3ab68..edfa87bd6a 100644 --- a/packages/opencode/test/install/smoke-test-binary.test.ts +++ b/packages/opencode/test/install/smoke-test-binary.test.ts @@ -16,9 +16,10 @@ * NOTE: Requires a local build first: bun run build:local */ import { describe, test, expect } from "bun:test" -import { spawnSync } from "child_process" +import { spawnSync, execFileSync } from "child_process" import path from "path" import fs from "fs" +import os from "os" const PKG_DIR = path.resolve(import.meta.dir, "../..") const REPO_ROOT = path.resolve(PKG_DIR, "../..") @@ -100,7 +101,13 @@ describe("compiled binary smoke test", () => { // node_modules. This is the regression guard for the v0.7.x curl-install // crash where altimate-core was marked `external` and the standalone // archive shipped without it. + // + // Hermeticity: cwd is the OS tmpdir so the binary cannot walk upward and + // discover the worktree's node_modules. Without this, Bun's compiled + // binary falls back to filesystem resolution from process.execPath and + // the test passes even if the staged-shim onResolve silently misses. const result = spawnSync(binary!, ["--version"], { + cwd: os.tmpdir(), encoding: "utf-8", timeout: 15_000, env: { @@ -121,6 +128,40 @@ describe("compiled binary smoke test", () => { expect(output).not.toContain("Cannot find module") }) + // Content-level assertion: independent of any runtime resolution path, + // require that the compiled binary contains exactly one altimate-core .node + // reference. If the staged-shim onResolve ever silently fails to redirect + // and Bun pulls in the upstream multi-platform loader, every platform's + // .node name leaks into bunfs and this test fires. Pairs with the + // hermetic --version test above. + runTest("binary embeds exactly one altimate-core .node", () => { + if (process.platform === "win32") { + // `strings` isn't available on a stock Windows runner. The other tests + // already exercise the runtime path; this content-level check covers + // Linux + macOS CI which is where the build matrix actually runs. + return + } + const stringsOut = execFileSync("strings", [binary!], { + encoding: "utf-8", + maxBuffer: 256 * 1024 * 1024, + }) + // Strip the bunfs hash suffix Bun appends to embedded resources + // (e.g. "altimate-core.darwin-arm64-ptxrnv5e.node" → "altimate-core.darwin-arm64.node") + // so the require() string and the bunfs entry collapse to the same name. + // Bun uses an alphanumeric (not hex) hash of 7+ chars; real platform + // last-segments (arm64/x64/gnu/msvc) are all <=5 chars, so a length-bound + // of {6,} unambiguously matches the hash. + const refs = [...stringsOut.matchAll(/altimate-core\.(?:darwin|linux|win32)-[a-z0-9-]+\.node/g)] + .map((m) => m[0]) + .map((r) => r.replace(/-[a-z0-9]{6,}(?=\.node$)/, "")) + const distinct = new Set(refs) + if (distinct.size !== 1) { + console.error("altimate-core .node references found in binary:", [...distinct]) + } + expect(distinct.size).toBeGreaterThanOrEqual(1) + expect(distinct.size).toBe(1) + }) + runTest("binary responds to --help", () => { const result = spawnSync(binary!, ["--help"], { encoding: "utf-8", From 2564abfefc522e78d6dedc1e013d400b67df2240 Mon Sep 17 00:00:00 2001 From: Michiel De Smet Date: Mon, 18 May 2026 21:40:39 +0800 Subject: [PATCH 6/9] fix: restore _requiredExports validation in the staged-shim build path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M2 from PR #820 review. The single-line shim (`module.exports = require('./altimate-core..node')`) dropped the upstream NAPI-RS loader's `_requiredExports` correctness check. If the embedded .node ever ships missing an export (truncated artifact, mismatched napi version, etc.), the binary would crash later with a confusing "TypeError: X is not a function" instead of a clear startup error pointing at the broken binding. Extract the `_requiredExports` literal from the real loader at build time and inline it into the generated shim. If the upstream loader format ever changes and the regex stops matching, abort the build rather than ship a shim with no validation — replacing the review's suggested `?? "[]"` fallback with a hard `throw`. Verification: the embedded `strings` of the compiled binary now show the _requiredExports array (40 method names: analyzeMigration ... validate) and the throw branch. The binary still starts cleanly: $ env -u NODE_PATH .../bin/altimate --version 0.0.0-fix/curl-install-bundle-altimate-core-202605181339 $ strings .../bin/altimate | grep -c _requiredExports 6 # ← validation present (twice — main chunk + worker chunk) $ bun test test/install/smoke-test-binary.test.ts 4 pass / 0 fail Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/opencode/script/build.ts | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 9f79990a6d..02a1cad647 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -248,6 +248,22 @@ const altimateCoreLoaderPkgJson = fileURLToPath(import.meta.resolve("@altimateai const altimateCoreLoaderDir = fs.realpathSync(path.dirname(altimateCoreLoaderPkgJson)) const altimateCoreVersion = pkg.dependencies["@altimateai/altimate-core"] +// Extract the `_requiredExports` literal from the upstream NAPI-RS loader so +// the generated single-platform shim can keep the same correctness check +// (catches a stale or truncated .node file at startup with a clear error +// instead of a confusing "method is not a function" later). Pin the exact +// shape we expect — if the loader format changes, abort the build rather +// than silently shipping a shim with no validation. +const altimateCoreLoaderSource = fs.readFileSync(path.join(altimateCoreLoaderDir, "index.js"), "utf8") +const requiredExportsMatch = altimateCoreLoaderSource.match(/const _requiredExports = (\[[\s\S]*?\])/) +if (!requiredExportsMatch) { + throw new Error( + "build.ts: could not extract _requiredExports from @altimateai/altimate-core/index.js. " + + "The upstream NAPI-RS loader format changed — update the regex (see script/build.ts).", + ) +} +const altimateCoreRequiredExportsLiteral = requiredExportsMatch[1] + // Locate the on-disk dir for an @altimateai/altimate-core- NAPI // prebuild. bun installs each optional platform package in its own `.bun/` // store entry, not next to the loader, so import.meta.resolve doesn't find it @@ -331,12 +347,22 @@ for (const item of targets) { if (fs.existsSync(path.join(altimateCoreLoaderDir, "index.d.ts"))) { fs.copyFileSync(path.join(altimateCoreLoaderDir, "index.d.ts"), path.join(stagedAltimateCoreDir, "index.d.ts")) } - // The shim — single static require() of the target's .node file. + // The shim — single static require() of the target's .node file plus the + // same _requiredExports correctness check the upstream NAPI-RS loader does. fs.writeFileSync( path.join(stagedAltimateCoreDir, "index.js"), `// Generated by packages/opencode/script/build.ts for ${name}.\n` + `// Replaces the multi-platform NAPI-RS loader so Bun embeds exactly one .node.\n` + - `module.exports = require('./${platform.nodeFile}')\n`, + `const nativeBinding = require('./${platform.nodeFile}')\n` + + `const _requiredExports = ${altimateCoreRequiredExportsLiteral}\n` + + `const _missing = _requiredExports.filter((n) => typeof nativeBinding[n] !== 'function')\n` + + `if (_missing.length > 0) {\n` + + ` throw new Error(\n` + + ` '@altimateai/altimate-core: embedded NAPI binary missing ' + _missing.length + ' export(s): ' +\n` + + ` _missing.slice(0, 5).join(', ') + (_missing.length > 5 ? '...' : '')\n` + + ` )\n` + + `}\n` + + `module.exports = nativeBinding\n`, ) // The actual native binding, co-located so the shim's relative require() resolves. fs.copyFileSync(platformNodeSrc, path.join(stagedAltimateCoreDir, platform.nodeFile)) From 85dd3516485bb7677c5d8d7f9b75ff155532c5a5 Mon Sep 17 00:00:00 2001 From: Michiel De Smet Date: Mon, 18 May 2026 21:42:20 +0800 Subject: [PATCH 7/9] fix: resolve altimate-core platform packages via createRequire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M3 from PR #820 review. The old locatePlatformPackageDir walked the filesystem hand-checking three different prefix shapes for bun's flat `.bun/+@/node_modules//` layout and interpolated `altimateCoreVersion` (read from packages/opencode/package.json dependencies) into the path literally. Both fragile: - pkg.dependencies values can carry semver prefixes (`^0.3.1`, `~0.3.1`, `>=0.3.1`) — the literal interpolation would miss the on-disk dir, which is keyed by the exact resolved version. - bun could change its flat-layout dir name format at any time; we'd silently start failing to find the prebuild. Replace with `createRequire(path.join(loaderDir, "index.js"))` then `req.resolve(`${pkgName}/package.json`)`. Node's resolver walks parent node_modules from the require base, which (in this project's bun-hoisted layout) finds the prebuild via the loader's `.bun/@altimateai+altimate-core@.../node_modules/@altimateai/altimate-core-` sibling entries without us having to know what the surrounding store looks like. Verified all 5 platform packages resolve to the same `.bun` paths the old function returned: darwin-arm64 -> .../node_modules/.bun/@altimateai+altimate-core-darwin-arm64@0.3.1/node_modules/@altimateai/altimate-core-darwin-arm64 darwin-x64 -> .../node_modules/.bun/@altimateai+altimate-core-darwin-x64@0.3.1/node_modules/@altimateai/altimate-core-darwin-x64 linux-arm64 -> .../node_modules/.bun/@altimateai+altimate-core-linux-arm64-gnu@0.3.1/node_modules/@altimateai/altimate-core-linux-arm64-gnu linux-x64 -> .../node_modules/.bun/@altimateai+altimate-core-linux-x64-gnu@0.3.1/node_modules/@altimateai/altimate-core-linux-x64-gnu win32-x64 -> .../node_modules/.bun/@altimateai+altimate-core-win32-x64-msvc@0.3.1/node_modules/@altimateai/altimate-core-win32-x64-msvc Build still succeeds, binary still starts hermetically: $ rm -rf packages/opencode/dist && \ bun run packages/opencode/script/build.ts --single --skip-install building @altimateai/altimate-code-darwin-arm64 $ cd /tmp && env -u NODE_PATH .../bin/altimate --version 0.0.0-fix/curl-install-bundle-altimate-core-202605181341 $ bun test test/install/ test/branding/ 401 pass / 0 fail altimateCoreVersion is no longer used and is dropped. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/opencode/script/build.ts | 40 +++++++++++-------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 02a1cad647..a088975859 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -4,6 +4,7 @@ import { $ } from "bun" import fs from "fs" import path from "path" import { fileURLToPath } from "url" +import { createRequire } from "node:module" import solidPlugin from "@opentui/solid/bun-plugin" const __filename = fileURLToPath(import.meta.url) @@ -246,7 +247,13 @@ function altimateCorePlatformFor(item: { os: string; arch: "arm64" | "x64"; abi? // node_modules/.bun) — we copy from this for each per-target staging dir. const altimateCoreLoaderPkgJson = fileURLToPath(import.meta.resolve("@altimateai/altimate-core/package.json")) const altimateCoreLoaderDir = fs.realpathSync(path.dirname(altimateCoreLoaderPkgJson)) -const altimateCoreVersion = pkg.dependencies["@altimateai/altimate-core"] + +// A `require` rooted at the loader's index.js so we can resolve sibling +// `@altimateai/altimate-core-` packages without hand-walking bun's +// `.bun/` flat layout. Node's resolution walks parent node_modules from the +// require base, which (in bun's hoisted layout used by this project) reaches +// the top-level `node_modules/@altimateai/altimate-core-` symlinks. +const altimateCoreLoaderRequire = createRequire(path.join(altimateCoreLoaderDir, "index.js")) // Extract the `_requiredExports` literal from the upstream NAPI-RS loader so // the generated single-platform shim can keep the same correctness check @@ -265,32 +272,13 @@ if (!requiredExportsMatch) { const altimateCoreRequiredExportsLiteral = requiredExportsMatch[1] // Locate the on-disk dir for an @altimateai/altimate-core- NAPI -// prebuild. bun installs each optional platform package in its own `.bun/` -// store entry, not next to the loader, so import.meta.resolve doesn't find it -// from the workspace. Walk up from the loader dir looking for the surrounding -// `.bun` store, then look up the platform package by its conventional name. +// prebuild. Use createRequire rooted at the loader's index.js — Node's +// require.resolve walks parent node_modules from the require base, which +// reaches both bun's hoisted top-level @altimateai/altimate-core- +// symlinks and any nested layout. function locatePlatformPackageDir(pkgName: string): string { - // bun's flat layout: `@scope+name@version` — only the `/` becomes `+`, the - // leading `@` of the scope stays. - const flatName = pkgName.replace("/", "+") - let current = altimateCoreLoaderDir - for (;;) { - // Match every layout we walk through: - // • current === `.bun` → check current - // • current === node_modules → check current/.bun - // • current === workspace root above node_modules → check current/node_modules/.bun - for (const prefix of [current, path.join(current, ".bun"), path.join(current, "node_modules", ".bun")]) { - const candidate = path.join(prefix, `${flatName}@${altimateCoreVersion}`, "node_modules", pkgName) - if (fs.existsSync(candidate)) return fs.realpathSync(candidate) - } - const parent = path.dirname(current) - if (parent === current) break - current = parent - } - throw new Error( - `Could not locate ${pkgName}@${altimateCoreVersion} on disk under any .bun store; ` + - `ensure 'bun install --os=* --cpu=*' has fetched it.`, - ) + const pkgJsonPath = altimateCoreLoaderRequire.resolve(`${pkgName}/package.json`) + return fs.realpathSync(path.dirname(pkgJsonPath)) } for (const item of targets) { const name = [ From bfe430f772ff8c9ea1543d1833350ec55467d61e Mon Sep 17 00:00:00 2001 From: Michiel De Smet Date: Mon, 18 May 2026 22:12:20 +0800 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20postinstall=20musl=20detection=20?= =?UTF-8?q?=E2=80=94=20execSync=20silently=20misses=20non-Alpine=20musl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit execSync throws on non-zero exit AND only returns stdout. ldd --version on musl exits 1 and prints to stderr — so isMuslPlatform() would silently return false on every non-Alpine musl distro (Void musl, custom builds), falling through to the generic "Could not find package" error instead of the actionable apk-add-gcompat message. packages/opencode/bin/altimate already used spawnSync correctly; this brings postinstall.mjs in line with it. Flagged by cubic-dev-ai (P2) and coderabbitai (Major) in PR #820 review. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/opencode/script/postinstall.mjs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/opencode/script/postinstall.mjs b/packages/opencode/script/postinstall.mjs index 487503fadd..ee22894c42 100644 --- a/packages/opencode/script/postinstall.mjs +++ b/packages/opencode/script/postinstall.mjs @@ -55,10 +55,17 @@ function isMuslPlatform() { // ignore } try { - const out = require("child_process").execSync("ldd --version", { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }) - if (typeof out === "string" && out.toLowerCase().includes("musl")) return true + // Mirror the detection in packages/opencode/bin/altimate: on musl + // systems `ldd --version` exits non-zero and prints to stderr. + // execSync would throw AND only return stdout — silently missing every + // non-Alpine musl distro. spawnSync gives both streams regardless of + // exit code. + const { spawnSync } = require("child_process") + const result = spawnSync("ldd", ["--version"], { encoding: "utf8" }) + const text = ((result.stdout || "") + (result.stderr || "")).toLowerCase() + if (text.includes("musl")) return true } catch { - // ignore — ldd may not exist or may exit non-zero + // ignore — ldd may not exist at all } return false } From 2898d9f0161a31a268ea0b6e952b718169d3df8e Mon Sep 17 00:00:00 2001 From: Michiel De Smet Date: Tue, 19 May 2026 22:08:04 +0800 Subject: [PATCH 9/9] fix: pipefail-safe musl detection, build-time guards, hermetic test cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three issues surfaced by coderabbitai's outside-diff review. 1. install: capture ldd output before grep `set -o pipefail` (line 2) made `ldd --version 2>&1 | grep -qi musl` evaluate false on every non-Alpine musl distro — ldd exits 1 by design, pipefail propagates that, the if-block never fires, and is_musl stays false. The C2 fail-fast logic at line 134-139 then doesn't trigger, and Void/Adelie/custom musl users hit the cryptic 404→tar failure C2 was supposed to fix. Capture ldd output into a variable with `|| true`, grep on the captured string. Sanity-tested with `false` simulating musl ldd — new form returns is_musl=true, old form returns false. 2. build.ts: refuse to silently produce no artifact + reject musl host for --single `--target-index=N` for an index that no longer exists after the musl/ win32-arm64 cull yielded `[allTargets[N]].filter(Boolean) === []`, which ran the build loop zero times and exited 0 — looked like success in CI. `--single` on Alpine matches process.platform=linux/x64, builds the glibc target, and produces a binary the host can't load. Add a targets.length === 0 guard with a reason-specific error, and a musl host check for --single mode. 3. smoke-test-binary.test.ts: use the repo's tmpdir() fixture `cwd: os.tmpdir()` works but doesn't auto-clean. Repo coding guidelines require `await using tmp = await tmpdir(...)` for tests. Functional behavior unchanged; cleanup is now deterministic when the scope closes. Tests: 397 pass / 0 fail / 5 skip (the local-binary smoke tests skip when no build is present). typecheck clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- install | 8 +++- packages/opencode/script/build.ts | 42 +++++++++++++++++++ .../test/install/smoke-test-binary.test.ts | 17 ++++---- 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/install b/install index 93c726a716..e748eb1be5 100755 --- a/install +++ b/install @@ -121,7 +121,13 @@ else fi if command -v ldd >/dev/null 2>&1; then - if ldd --version 2>&1 | grep -qi musl; then + # ldd --version exits non-zero on musl by design. With `set -o pipefail` + # (line 2), a pipeline `ldd --version 2>&1 | grep -qi musl` would inherit + # ldd's failure and the if-block would never fire — defeating musl + # detection on every non-Alpine musl distro (Void, Adelie, custom builds). + # Capture output first, then grep. + ldd_out="$(ldd --version 2>&1 || true)" + if printf '%s' "$ldd_out" | grep -qi musl; then is_musl=true fi fi diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index a088975859..8dad623d7d 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -183,6 +183,48 @@ const targets = targetIndexFlag !== undefined ? allTargets.filter(t => targetsFlag.includes(t.os)) : allTargets +// Defense in depth: refuse to produce no artifacts at all, and refuse to build +// the glibc target on a musl host where the binary would crash at startup. +// +// Why it matters: +// - `--target-index=N` for an index that no longer exists (after the +// musl/win32-arm64 cull) silently yields an empty `targets` array. Without +// this guard the build "succeeds" with zero output and CI proceeds. +// - `--single` only filters on os/arch, not libc. On Alpine that matches +// `linux-x64` (glibc), produces a glibc binary that the musl host can't +// load, and dies later with a cryptic linker error. +if (targets.length === 0) { + const reason = targetIndexFlag !== undefined + ? `--target-index=${targetIndexFlag} is out of range (allTargets has ${allTargets.length} entries — musl/win32-arm64 were removed).` + : singleFlag + ? `--single found no entry in allTargets matching ${process.platform}/${process.arch} (host may be excluded — see allTargets at the top of build.ts).` + : targetsFlag + ? `--targets=${targetsFlag.join(",")} matched nothing in allTargets.` + : "allTargets is empty." + console.error(`error: no build targets selected. ${reason}`) + process.exit(1) +} + +if (singleFlag && process.platform === "linux") { + const isMuslHost = (() => { + try { + if (fs.existsSync("/etc/alpine-release")) return true + } catch {} + try { + const { spawnSync } = require("node:child_process") as typeof import("node:child_process") + const r = spawnSync("ldd", ["--version"], { encoding: "utf8" }) + const text = ((r.stdout ?? "") + (r.stderr ?? "")).toLowerCase() + if (text.includes("musl")) return true + } catch {} + return false + })() + if (isMuslHost) { + console.error("error: --single on a musl-linux host would build the glibc target and produce a binary the host cannot run.") + console.error(" altimate-core has no NAPI prebuild for musl yet. Build on a glibc host, or install via `apk add gcompat` + the npm wrapper.") + process.exit(1) + } +} + await $`rm -rf dist` // Packages excluded from the compiled binary — must be resolvable from diff --git a/packages/opencode/test/install/smoke-test-binary.test.ts b/packages/opencode/test/install/smoke-test-binary.test.ts index edfa87bd6a..73ecdf3da2 100644 --- a/packages/opencode/test/install/smoke-test-binary.test.ts +++ b/packages/opencode/test/install/smoke-test-binary.test.ts @@ -19,7 +19,7 @@ import { describe, test, expect } from "bun:test" import { spawnSync, execFileSync } from "child_process" import path from "path" import fs from "fs" -import os from "os" +import { tmpdir } from "../fixture/fixture" const PKG_DIR = path.resolve(import.meta.dir, "../..") const REPO_ROOT = path.resolve(PKG_DIR, "../..") @@ -94,7 +94,7 @@ describe("compiled binary smoke test", () => { expect(result.stderr).not.toContain("Cannot find module") }) - runTest("binary succeeds with NODE_PATH cleared (standalone mode)", () => { + runTest("binary succeeds with NODE_PATH cleared (standalone mode)", async () => { // The Bun-compiled binary embeds @altimateai/altimate-core's NAPI .node // directly into bunfs (see script/build.ts — staged shim + resolver // plugin). It MUST start without any external NODE_PATH or companion @@ -102,12 +102,15 @@ describe("compiled binary smoke test", () => { // crash where altimate-core was marked `external` and the standalone // archive shipped without it. // - // Hermeticity: cwd is the OS tmpdir so the binary cannot walk upward and - // discover the worktree's node_modules. Without this, Bun's compiled - // binary falls back to filesystem resolution from process.execPath and - // the test passes even if the staged-shim onResolve silently misses. + // Hermeticity: cwd is a freshly-created tmp dir so the binary cannot walk + // upward and discover the worktree's node_modules. Without this, Bun's + // compiled binary falls back to filesystem resolution from process.execPath + // and the test passes even if the staged-shim onResolve silently misses. + // + // Uses the repo's tmpdir() fixture for auto-cleanup via `await using`. + await using tmp = await tmpdir() const result = spawnSync(binary!, ["--version"], { - cwd: os.tmpdir(), + cwd: tmp.path, encoding: "utf-8", timeout: 15_000, env: {