diff --git a/.github/workflows/publish-platform.yml b/.github/workflows/publish-platform.yml index 173c11795..74089679f 100644 --- a/.github/workflows/publish-platform.yml +++ b/.github/workflows/publish-platform.yml @@ -35,15 +35,15 @@ jobs: # - Uploads compressed artifacts for the publish job # ============================================================================= build: - runs-on: ${{ matrix.platform == 'windows-x64' && 'windows-latest' || 'ubuntu-latest' }} + runs-on: ${{ startsWith(matrix.platform, 'windows-') && 'windows-latest' || 'ubuntu-latest' }} defaults: run: shell: bash strategy: fail-fast: false - max-parallel: 7 + max-parallel: 11 matrix: - platform: [darwin-arm64, darwin-x64, linux-x64, linux-arm64, linux-x64-musl, linux-arm64-musl, windows-x64] + platform: [darwin-arm64, darwin-x64, darwin-x64-baseline, linux-x64, linux-x64-baseline, linux-arm64, linux-x64-musl, linux-x64-musl-baseline, linux-arm64-musl, windows-x64, windows-x64-baseline] steps: - uses: actions/checkout@v4 @@ -95,14 +95,18 @@ jobs: case "$PLATFORM" in darwin-arm64) TARGET="bun-darwin-arm64" ;; darwin-x64) TARGET="bun-darwin-x64" ;; + darwin-x64-baseline) TARGET="bun-darwin-x64-baseline" ;; linux-x64) TARGET="bun-linux-x64" ;; + linux-x64-baseline) TARGET="bun-linux-x64-baseline" ;; linux-arm64) TARGET="bun-linux-arm64" ;; linux-x64-musl) TARGET="bun-linux-x64-musl" ;; + linux-x64-musl-baseline) TARGET="bun-linux-x64-musl-baseline" ;; linux-arm64-musl) TARGET="bun-linux-arm64-musl" ;; windows-x64) TARGET="bun-windows-x64" ;; + windows-x64-baseline) TARGET="bun-windows-x64-baseline" ;; esac - if [ "$PLATFORM" = "windows-x64" ]; then + if [[ "$PLATFORM" == windows-* ]]; then OUTPUT="packages/${PLATFORM}/bin/oh-my-opencode.exe" else OUTPUT="packages/${PLATFORM}/bin/oh-my-opencode" @@ -119,7 +123,7 @@ jobs: PLATFORM="${{ matrix.platform }}" cd packages/${PLATFORM} - if [ "$PLATFORM" = "windows-x64" ]; then + if [[ "$PLATFORM" == windows-* ]]; then # Windows: use 7z (pre-installed on windows-latest) 7z a -tzip ../../binary-${PLATFORM}.zip bin/ package.json else @@ -155,7 +159,7 @@ jobs: fail-fast: false max-parallel: 2 matrix: - platform: [darwin-arm64, darwin-x64, linux-x64, linux-arm64, linux-x64-musl, linux-arm64-musl, windows-x64] + platform: [darwin-arm64, darwin-x64, darwin-x64-baseline, linux-x64, linux-x64-baseline, linux-arm64, linux-x64-musl, linux-x64-musl-baseline, linux-arm64-musl, windows-x64, windows-x64-baseline] steps: - name: Check if already published id: check @@ -184,7 +188,7 @@ jobs: PLATFORM="${{ matrix.platform }}" mkdir -p packages/${PLATFORM} - if [ "$PLATFORM" = "windows-x64" ]; then + if [[ "$PLATFORM" == windows-* ]]; then unzip binary-${PLATFORM}.zip -d packages/${PLATFORM}/ else tar -xzvf binary-${PLATFORM}.tar.gz -C packages/${PLATFORM}/ diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d430e7caf..a64ddd55f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -189,7 +189,7 @@ jobs: VERSION="${{ steps.version.outputs.version }}" jq --arg v "$VERSION" '.version = $v' package.json > tmp.json && mv tmp.json package.json - for platform in darwin-arm64 darwin-x64 linux-x64 linux-arm64 linux-x64-musl linux-arm64-musl windows-x64; do + for platform in darwin-arm64 darwin-x64 darwin-x64-baseline linux-x64 linux-x64-baseline linux-arm64 linux-x64-musl linux-x64-musl-baseline linux-arm64-musl windows-x64 windows-x64-baseline; do jq --arg v "$VERSION" '.version = $v' "packages/${platform}/package.json" > tmp.json mv tmp.json "packages/${platform}/package.json" done diff --git a/bin/oh-my-opencode.js b/bin/oh-my-opencode.js index 4ad39550b..0d66e55eb 100755 --- a/bin/oh-my-opencode.js +++ b/bin/oh-my-opencode.js @@ -3,8 +3,9 @@ // Wrapper script that detects platform and spawns the correct binary import { spawnSync } from "node:child_process"; +import { readFileSync } from "node:fs"; import { createRequire } from "node:module"; -import { getPlatformPackage, getBinaryPath } from "./platform.js"; +import { getPlatformPackageCandidates, getBinaryPath } from "./platform.js"; const require = createRequire(import.meta.url); @@ -26,55 +27,116 @@ function getLibcFamily() { } } +function supportsAvx2() { + if (process.arch !== "x64") { + return null; + } + + if (process.env.OH_MY_OPENCODE_FORCE_BASELINE === "1") { + return false; + } + + if (process.platform === "linux") { + try { + const cpuInfo = readFileSync("/proc/cpuinfo", "utf8").toLowerCase(); + return cpuInfo.includes("avx2"); + } catch { + return null; + } + } + + if (process.platform === "darwin") { + const probe = spawnSync("sysctl", ["-n", "machdep.cpu.leaf7_features"], { + encoding: "utf8", + }); + + if (probe.error || probe.status !== 0) { + return null; + } + + return probe.stdout.toUpperCase().includes("AVX2"); + } + + return null; +} + +function getSignalExitCode(signal) { + const signalCodeByName = { + SIGINT: 2, + SIGILL: 4, + SIGKILL: 9, + SIGTERM: 15, + }; + + return 128 + (signalCodeByName[signal] ?? 1); +} + function main() { const { platform, arch } = process; const libcFamily = getLibcFamily(); + const avx2Supported = supportsAvx2(); - // Get platform package name - let pkg; + let packageCandidates; try { - pkg = getPlatformPackage({ platform, arch, libcFamily }); + packageCandidates = getPlatformPackageCandidates({ + platform, + arch, + libcFamily, + preferBaseline: avx2Supported === false, + }); } catch (error) { console.error(`\noh-my-opencode: ${error.message}\n`); process.exit(1); } - - // Resolve binary path - const binRelPath = getBinaryPath(pkg, platform); - - let binPath; - try { - binPath = require.resolve(binRelPath); - } catch { + + const resolvedBinaries = packageCandidates + .map((pkg) => { + try { + return { pkg, binPath: require.resolve(getBinaryPath(pkg, platform)) }; + } catch { + return null; + } + }) + .filter((entry) => entry !== null); + + if (resolvedBinaries.length === 0) { console.error(`\noh-my-opencode: Platform binary not installed.`); console.error(`\nYour platform: ${platform}-${arch}${libcFamily === "musl" ? "-musl" : ""}`); - console.error(`Expected package: ${pkg}`); + console.error(`Expected packages (in order): ${packageCandidates.join(", ")}`); console.error(`\nTo fix, run:`); - console.error(` npm install ${pkg}\n`); + console.error(` npm install ${packageCandidates[0]}\n`); process.exit(1); } - - // Spawn the binary - const result = spawnSync(binPath, process.argv.slice(2), { - stdio: "inherit", - }); - - // Handle spawn errors - if (result.error) { - console.error(`\noh-my-opencode: Failed to execute binary.`); - console.error(`Error: ${result.error.message}\n`); - process.exit(2); - } - - // Handle signals - if (result.signal) { - const signalNum = result.signal === "SIGTERM" ? 15 : - result.signal === "SIGKILL" ? 9 : - result.signal === "SIGINT" ? 2 : 1; - process.exit(128 + signalNum); + + for (let index = 0; index < resolvedBinaries.length; index += 1) { + const currentBinary = resolvedBinaries[index]; + const hasFallback = index < resolvedBinaries.length - 1; + const result = spawnSync(currentBinary.binPath, process.argv.slice(2), { + stdio: "inherit", + }); + + if (result.error) { + if (hasFallback) { + continue; + } + + console.error(`\noh-my-opencode: Failed to execute binary.`); + console.error(`Error: ${result.error.message}\n`); + process.exit(2); + } + + if (result.signal === "SIGILL" && hasFallback) { + continue; + } + + if (result.signal) { + process.exit(getSignalExitCode(result.signal)); + } + + process.exit(result.status ?? 1); } - process.exit(result.status ?? 1); + process.exit(1); } main(); diff --git a/bin/platform.d.ts b/bin/platform.d.ts new file mode 100644 index 000000000..ed3987957 --- /dev/null +++ b/bin/platform.d.ts @@ -0,0 +1,14 @@ +export declare function getPlatformPackage(options: { + platform: string; + arch: string; + libcFamily?: string | null; +}): string; + +export declare function getPlatformPackageCandidates(options: { + platform: string; + arch: string; + libcFamily?: string | null; + preferBaseline?: boolean; +}): string[]; + +export declare function getBinaryPath(pkg: string, platform: string): string; diff --git a/bin/platform.js b/bin/platform.js index ac728d3c8..a2a6c3c32 100644 --- a/bin/platform.js +++ b/bin/platform.js @@ -26,6 +26,50 @@ export function getPlatformPackage({ platform, arch, libcFamily }) { return `oh-my-opencode-${os}-${arch}${suffix}`; } +/** @param {{ platform: string, arch: string, libcFamily?: string | null, preferBaseline?: boolean }} options */ +export function getPlatformPackageCandidates({ platform, arch, libcFamily, preferBaseline = false }) { + const primaryPackage = getPlatformPackage({ platform, arch, libcFamily }); + const baselinePackage = getBaselinePlatformPackage({ platform, arch, libcFamily }); + + if (!baselinePackage) { + return [primaryPackage]; + } + + return preferBaseline ? [baselinePackage, primaryPackage] : [primaryPackage, baselinePackage]; +} + +/** @param {{ platform: string, arch: string, libcFamily?: string | null }} options */ +function getBaselinePlatformPackage({ platform, arch, libcFamily }) { + if (arch !== "x64") { + return null; + } + + if (platform === "darwin") { + return "oh-my-opencode-darwin-x64-baseline"; + } + + if (platform === "win32") { + return "oh-my-opencode-windows-x64-baseline"; + } + + if (platform === "linux") { + if (libcFamily === null || libcFamily === undefined) { + throw new Error( + "Could not detect libc on Linux. " + + "Please ensure detect-libc is installed or report this issue." + ); + } + + if (libcFamily === "musl") { + return "oh-my-opencode-linux-x64-musl-baseline"; + } + + return "oh-my-opencode-linux-x64-baseline"; + } + + return null; +} + /** * Get the path to the binary within a platform package * @param {string} pkg Package name diff --git a/bin/platform.test.ts b/bin/platform.test.ts index 775509929..88b8b877b 100644 --- a/bin/platform.test.ts +++ b/bin/platform.test.ts @@ -1,6 +1,6 @@ // bin/platform.test.ts import { describe, expect, test } from "bun:test"; -import { getPlatformPackage, getBinaryPath } from "./platform.js"; +import { getBinaryPath, getPlatformPackage, getPlatformPackageCandidates } from "./platform.js"; describe("getPlatformPackage", () => { // #region Darwin platforms @@ -146,3 +146,58 @@ describe("getBinaryPath", () => { expect(result).toBe("oh-my-opencode-linux-x64/bin/oh-my-opencode"); }); }); + +describe("getPlatformPackageCandidates", () => { + test("returns x64 and baseline candidates for Linux glibc", () => { + // #given Linux x64 with glibc + const input = { platform: "linux", arch: "x64", libcFamily: "glibc" }; + + // #when getting package candidates + const result = getPlatformPackageCandidates(input); + + // #then returns modern first then baseline fallback + expect(result).toEqual([ + "oh-my-opencode-linux-x64", + "oh-my-opencode-linux-x64-baseline", + ]); + }); + + test("returns x64 musl and baseline candidates for Linux musl", () => { + // #given Linux x64 with musl + const input = { platform: "linux", arch: "x64", libcFamily: "musl" }; + + // #when getting package candidates + const result = getPlatformPackageCandidates(input); + + // #then returns musl modern first then musl baseline fallback + expect(result).toEqual([ + "oh-my-opencode-linux-x64-musl", + "oh-my-opencode-linux-x64-musl-baseline", + ]); + }); + + test("returns baseline first when preferBaseline is true", () => { + // #given Windows x64 and baseline preference + const input = { platform: "win32", arch: "x64", preferBaseline: true }; + + // #when getting package candidates + const result = getPlatformPackageCandidates(input); + + // #then baseline package is preferred first + expect(result).toEqual([ + "oh-my-opencode-windows-x64-baseline", + "oh-my-opencode-windows-x64", + ]); + }); + + test("returns only one candidate for ARM64", () => { + // #given non-x64 platform + const input = { platform: "linux", arch: "arm64", libcFamily: "glibc" }; + + // #when getting package candidates + const result = getPlatformPackageCandidates(input); + + // #then baseline fallback is not included + expect(result).toEqual(["oh-my-opencode-linux-arm64"]); + }); +}); diff --git a/package.json b/package.json index 0559493b7..f5138b11c 100644 --- a/package.json +++ b/package.json @@ -77,11 +77,15 @@ "optionalDependencies": { "oh-my-opencode-darwin-arm64": "3.8.5", "oh-my-opencode-darwin-x64": "3.8.5", + "oh-my-opencode-darwin-x64-baseline": "3.8.5", "oh-my-opencode-linux-arm64": "3.8.5", "oh-my-opencode-linux-arm64-musl": "3.8.5", "oh-my-opencode-linux-x64": "3.8.5", + "oh-my-opencode-linux-x64-baseline": "3.8.5", "oh-my-opencode-linux-x64-musl": "3.8.5", - "oh-my-opencode-windows-x64": "3.8.5" + "oh-my-opencode-linux-x64-musl-baseline": "3.8.5", + "oh-my-opencode-windows-x64": "3.8.5", + "oh-my-opencode-windows-x64-baseline": "3.8.5" }, "trustedDependencies": [ "@ast-grep/cli", diff --git a/postinstall.mjs b/postinstall.mjs index 8243a562f..35f77a6d4 100644 --- a/postinstall.mjs +++ b/postinstall.mjs @@ -2,7 +2,7 @@ // Runs after npm install to verify platform binary is available import { createRequire } from "node:module"; -import { getPlatformPackage, getBinaryPath } from "./bin/platform.js"; +import { getPlatformPackageCandidates, getBinaryPath } from "./bin/platform.js"; const require = createRequire(import.meta.url); @@ -27,12 +27,28 @@ function main() { const libcFamily = getLibcFamily(); try { - const pkg = getPlatformPackage({ platform, arch, libcFamily }); - const binPath = getBinaryPath(pkg, platform); - - // Try to resolve the binary - require.resolve(binPath); - console.log(`✓ oh-my-opencode binary installed for ${platform}-${arch}`); + const packageCandidates = getPlatformPackageCandidates({ + platform, + arch, + libcFamily, + }); + + const resolvedPackage = packageCandidates.find((pkg) => { + try { + require.resolve(getBinaryPath(pkg, platform)); + return true; + } catch { + return false; + } + }); + + if (!resolvedPackage) { + throw new Error( + `No platform binary package installed. Tried: ${packageCandidates.join(", ")}` + ); + } + + console.log(`✓ oh-my-opencode binary installed for ${platform}-${arch} (${resolvedPackage})`); } catch (error) { console.warn(`⚠ oh-my-opencode: ${error.message}`); console.warn(` The CLI may not work on this platform.`);