diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 107071869..19fde8873 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -173,9 +173,81 @@ jobs: - name: Install method smoke tests run: bun run ci:test:install-methods - release: + release_binary: if: startsWith(github.ref, 'refs/tags/v') needs: [check, native, test, install_methods] + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-22.04 + target_id: linux-x64 + native_artifact: pi-natives-linux-x64 + binary_path: packages/coding-agent/binaries/omp-linux-x64 + - os: ubuntu-24.04-arm + target_id: linux-arm64 + native_artifact: pi-natives-linux-arm64 + binary_path: packages/coding-agent/binaries/omp-linux-arm64 + - os: macos-15-intel + target_id: darwin-x64 + native_artifact: pi-natives-darwin-x64 + binary_path: packages/coding-agent/binaries/omp-darwin-x64 + - os: macos-14 + target_id: darwin-arm64 + native_artifact: pi-natives-darwin-arm64 + binary_path: packages/coding-agent/binaries/omp-darwin-arm64 + - os: windows-latest + target_id: win32-x64 + native_artifact: pi-natives-win32-x64 + binary_path: packages/coding-agent/binaries/omp-windows-x64.exe + runs-on: ${{ matrix.os }} + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + lfs: true + - uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3" + - name: Cache bun dependencies + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} + - run: bun install --frozen-lockfile + - name: Download native addon(s) + uses: actions/download-artifact@v4 + with: + name: ${{ matrix.native_artifact }} + path: packages/natives/native + - name: Build release binary + env: + RELEASE_TARGETS: ${{ matrix.target_id }} + run: bun run ci:release:build-binaries + - name: Smoke release binary + if: runner.os != 'Windows' + run: | + runtime_dir="$(mktemp -d)" + HOME="$runtime_dir/home" XDG_DATA_HOME="$runtime_dir/xdg" "${{ matrix.binary_path }}" --version + - name: Smoke release binary (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $runtimeDir = Join-Path $env:TEMP ("omp-runtime-" + [System.Guid]::NewGuid().ToString("N")) + New-Item -ItemType Directory -Force -Path $runtimeDir | Out-Null + $env:HOME = Join-Path $runtimeDir "home" + $env:XDG_DATA_HOME = Join-Path $runtimeDir "xdg" + & "${{ matrix.binary_path }}" --version + - name: Upload release binary artifact + uses: actions/upload-artifact@v4 + with: + name: omp-binary-${{ matrix.target_id }} + path: ${{ matrix.binary_path }} + + release: + if: startsWith(github.ref, 'refs/tags/v') + needs: [release_binary] runs-on: ubuntu-22.04 permissions: contents: write @@ -197,6 +269,12 @@ jobs: path: ~/.bun/install/cache key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} - run: bun install --frozen-lockfile + - name: Download release binaries + uses: actions/download-artifact@v4 + with: + pattern: omp-binary-* + path: packages/coding-agent/binaries + merge-multiple: true - name: Download native addons uses: actions/download-artifact@v4 with: @@ -205,9 +283,6 @@ jobs: merge-multiple: true - name: Verify native addons run: bun run ci:release:verify-natives - - - name: Build binaries - run: bun run ci:release:build-binaries - name: Stage native addons for release run: cp packages/natives/native/*.node packages/coding-agent/binaries/ - name: Create GitHub Release diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 89ed66b81..5bab1713d 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -10,6 +10,7 @@ - Fixed edit tool diff and replace operations to report missing-file failures as `File not found: ` errors instead of raw filesystem ENOENT errors - Fixed `local://` URL path leak on Linux where `//` collapsing to `/` produced `local:/path` forms that bypassed the internal protocol handler and leaked as filesystem paths, breaking plan mode file resolution +- Fixed Darwin compiled binaries failing to start under Bun 1.3.12 by ad-hoc signing local and release binary builds after applying Bun's no-codesign workaround ([#754](https://github.com/can1357/oh-my-pi/issues/754)) - Fixed Tavily web search silently returning off-topic news articles when `--recency` was set. The provider was unconditionally coupling `topic: "news"` to recency, which scoped Tavily's index to news publications and excluded documentation, release notes, GitHub, and all non-news technical content. Technical queries with `--recency` now return the correct corpus. - Fixed status-line sanitization to strip OSC, DCS, PM, APC, and 8-bit CSI escape sequences instead of leaving payload fragments in the UI - Fixed inline read tool previews to avoid rendering duplicate summary rows above the same code cell @@ -7115,4 +7116,4 @@ Initial public release. - Git branch display in footer - Message queueing during streaming responses - OAuth integration for Gmail and Google Calendar access -- HTML export with syntax highlighting and collapsible sections \ No newline at end of file +- HTML export with syntax highlighting and collapsible sections diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index bdb3eba05..1e3a249bf 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -31,7 +31,7 @@ "omp": "src/cli.ts" }, "scripts": { - "build": "bun --cwd=../stats scripts/generate-client-bundle.ts --generate && bun --cwd=../natives run embed:native && bun build --compile --define PI_COMPILED=true --external mupdf --root ../.. ./src/cli.ts --outfile dist/omp && bun --cwd=../natives run embed:native --reset && bun --cwd=../stats scripts/generate-client-bundle.ts --reset", + "build": "bun scripts/build-binary.ts", "check": "biome check . && bun run check:types", "check:types": "tsgo -p tsconfig.json --noEmit", "lint": "biome lint .", diff --git a/packages/coding-agent/scripts/build-binary.ts b/packages/coding-agent/scripts/build-binary.ts new file mode 100644 index 000000000..9e6f1af28 --- /dev/null +++ b/packages/coding-agent/scripts/build-binary.ts @@ -0,0 +1,61 @@ +#!/usr/bin/env bun + +import * as path from "node:path"; + +const packageDir = path.join(import.meta.dir, ".."); +const outputPath = path.join(packageDir, "dist", "omp"); + +function shouldAdhocSignDarwinBinary(): boolean { + return process.platform === "darwin"; +} + +async function runCommand(command: string[], env: NodeJS.ProcessEnv = Bun.env): Promise { + const proc = Bun.spawn(command, { + cwd: packageDir, + env, + stdout: "inherit", + stderr: "inherit", + }); + const exitCode = await proc.exited; + if (exitCode !== 0) { + throw new Error(`Command failed with exit code ${exitCode}: ${command.join(" ")}`); + } +} + +async function main(): Promise { + await runCommand(["bun", "--cwd=../stats", "scripts/generate-client-bundle.ts", "--generate"]); + try { + await runCommand(["bun", "--cwd=../natives", "run", "embed:native"]); + try { + const buildEnv = shouldAdhocSignDarwinBinary() ? { ...Bun.env, BUN_NO_CODESIGN_MACHO_BINARY: "1" } : Bun.env; + await runCommand( + [ + "bun", + "build", + "--compile", + "--define", + "PI_COMPILED=true", + "--external", + "mupdf", + "--root", + "../..", + "./src/cli.ts", + "--outfile", + "dist/omp", + ], + buildEnv, + ); + + // Bun 1.3.12 emits a truncated Mach-O signature on darwin builds. + if (shouldAdhocSignDarwinBinary()) { + await runCommand(["codesign", "--force", "--sign", "-", outputPath]); + } + } finally { + await runCommand(["bun", "--cwd=../natives", "run", "embed:native", "--reset"]); + } + } finally { + await runCommand(["bun", "--cwd=../stats", "scripts/generate-client-bundle.ts", "--reset"]); + } +} + +await main(); diff --git a/scripts/ci-release-build-binaries.ts b/scripts/ci-release-build-binaries.ts index 8eb2d961b..6e83bbbd6 100644 --- a/scripts/ci-release-build-binaries.ts +++ b/scripts/ci-release-build-binaries.ts @@ -2,9 +2,9 @@ import * as fs from "node:fs/promises"; import * as path from "node:path"; -import { $ } from "bun"; interface BinaryTarget { + id: string; platform: string; arch: string; target: string; @@ -17,30 +17,35 @@ const entrypoint = "./packages/coding-agent/src/cli.ts"; const isDryRun = process.argv.includes("--dry-run"); const targets: BinaryTarget[] = [ { + id: "darwin-arm64", platform: "darwin", arch: "arm64", target: "bun-darwin-arm64", outfile: "packages/coding-agent/binaries/omp-darwin-arm64", }, { + id: "darwin-x64", platform: "darwin", arch: "x64", target: "bun-darwin-x64", outfile: "packages/coding-agent/binaries/omp-darwin-x64", }, { + id: "linux-x64", platform: "linux", arch: "x64", target: "bun-linux-x64-modern", outfile: "packages/coding-agent/binaries/omp-linux-x64", }, { + id: "linux-arm64", platform: "linux", arch: "arm64", target: "bun-linux-arm64", outfile: "packages/coding-agent/binaries/omp-linux-arm64", }, { + id: "win32-x64", platform: "win32", arch: "x64", target: "bun-windows-x64-modern", @@ -48,19 +53,53 @@ const targets: BinaryTarget[] = [ }, ]; +function parseRequestedTargets(): Set | null { + const flagIndex = process.argv.findIndex(arg => arg === "--targets"); + const flagValue = + flagIndex >= 0 + ? process.argv[flagIndex + 1] + : process.argv.find(arg => arg.startsWith("--targets="))?.split("=", 2)[1] ?? Bun.env.RELEASE_TARGETS; + + if (!flagValue) { + return null; + } + + return new Set( + flagValue + .split(",") + .map(value => value.trim()) + .filter(Boolean), + ); +} + +function shouldAdhocSignDarwinBinary(target: BinaryTarget): boolean { + return target.platform === "darwin" && process.platform === "darwin"; +} + +async function runCommand(command: string[], cwd: string, env: NodeJS.ProcessEnv = Bun.env): Promise { + const proc = Bun.spawn(command, { + cwd, + env, + stdout: "inherit", + stderr: "inherit", + }); + const exitCode = await proc.exited; + if (exitCode !== 0) { + throw new Error(`Command failed with exit code ${exitCode}: ${command.join(" ")}`); + } +} + async function embedNative(target: BinaryTarget): Promise { if (isDryRun) { console.log(`DRY RUN bun --cwd=packages/natives run embed:native [${target.platform}/${target.arch}]`); return; } - await $`bun --cwd=packages/natives run embed:native` - .cwd(repoRoot) - .env({ - ...Bun.env, - TARGET_PLATFORM: target.platform, - TARGET_ARCH: target.arch, - }); + await runCommand(["bun", "--cwd=packages/natives", "run", "embed:native"], repoRoot, { + ...Bun.env, + TARGET_PLATFORM: target.platform, + TARGET_ARCH: target.arch, + }); } async function buildBinary(target: BinaryTarget): Promise { @@ -71,9 +110,19 @@ async function buildBinary(target: BinaryTarget): Promise { return; } - await $`bun build --compile --define PI_COMPILED=true --root . --external mupdf --target=${target.target} ${entrypoint} --outfile ${target.outfile}`.cwd( + const buildEnv = shouldAdhocSignDarwinBinary(target) + ? { ...Bun.env, BUN_NO_CODESIGN_MACHO_BINARY: "1" } + : Bun.env; + await runCommand( + ["bun", "build", "--compile", "--define", "PI_COMPILED=true", "--root", ".", "--external", "mupdf", "--target", target.target, entrypoint, "--outfile", target.outfile], repoRoot, + buildEnv, ); + + // Bun 1.3.12 emits a truncated Mach-O signature on darwin builds. + if (shouldAdhocSignDarwinBinary(target)) { + await runCommand(["codesign", "--force", "--sign", "-", path.join(repoRoot, target.outfile)], repoRoot); + } } async function generateBundle(): Promise { @@ -81,7 +130,7 @@ async function generateBundle(): Promise { console.log("DRY RUN bun --cwd=packages/stats scripts/generate-client-bundle.ts --generate"); return; } - await $`bun --cwd=packages/stats scripts/generate-client-bundle.ts --generate`.cwd(repoRoot); + await runCommand(["bun", "--cwd=packages/stats", "scripts/generate-client-bundle.ts", "--generate"], repoRoot); } async function resetArtifacts(): Promise { @@ -90,15 +139,33 @@ async function resetArtifacts(): Promise { console.log("DRY RUN bun --cwd=packages/stats scripts/generate-client-bundle.ts --reset"); return; } - await $`bun --cwd=packages/natives run embed:native --reset`.cwd(repoRoot); - await $`bun --cwd=packages/stats scripts/generate-client-bundle.ts --reset`.cwd(repoRoot); + await runCommand(["bun", "--cwd=packages/natives", "run", "embed:native", "--reset"], repoRoot); + await runCommand(["bun", "--cwd=packages/stats", "scripts/generate-client-bundle.ts", "--reset"], repoRoot); } async function main(): Promise { + const requestedTargets = parseRequestedTargets(); + const selectedTargets = requestedTargets + ? targets.filter(target => requestedTargets.has(target.id)) + : targets; + + if (requestedTargets) { + const unknownTargets = [...requestedTargets].filter( + requestedTarget => !targets.some(target => target.id === requestedTarget), + ); + if (unknownTargets.length > 0) { + throw new Error(`Unknown release target(s): ${unknownTargets.join(", ")}`); + } + } + + if (selectedTargets.length === 0) { + throw new Error("No release targets selected."); + } + await fs.mkdir(binariesDir, { recursive: true }); await generateBundle(); try { - for (const target of targets) { + for (const target of selectedTargets) { await buildBinary(target); } } finally { diff --git a/scripts/install-tests/binary.dockerfile b/scripts/install-tests/binary.dockerfile index 27965a53e..da804617a 100644 --- a/scripts/install-tests/binary.dockerfile +++ b/scripts/install-tests/binary.dockerfile @@ -20,12 +20,10 @@ RUN bun install --frozen-lockfile RUN bun --cwd=packages/natives run build RUN cd packages/coding-agent && bun run build -# Install binary and native addon to PATH +# Install binary to PATH RUN mkdir -p /root/.local/bin && \ - cp packages/coding-agent/dist/omp /root/.local/bin/ && \ - cp packages/natives/native/pi_natives.linux-x64-modern.node /root/.local/bin/ && \ - cp packages/natives/native/pi_natives.linux-x64-baseline.node /root/.local/bin/ + cp packages/coding-agent/dist/omp /root/.local/bin/ ENV PATH="/root/.local/bin:$PATH" # Verify -RUN omp --version +RUN HOME=/tmp/omp-home XDG_DATA_HOME=/tmp/omp-xdg omp --version diff --git a/scripts/install-tests/run-ci.sh b/scripts/install-tests/run-ci.sh index 86c0bdfdb..6f96bc1e4 100755 --- a/scripts/install-tests/run-ci.sh +++ b/scripts/install-tests/run-ci.sh @@ -4,6 +4,9 @@ set -euo pipefail cd "$(dirname "$0")/../.." ROOT_DIR="$(pwd)" WORK_DIR="$(mktemp -d)" +TMP_WORK_DIR="$WORK_DIR/tmp" +mkdir -p "$TMP_WORK_DIR" +export TMPDIR="$TMP_WORK_DIR" trap 'rm -rf "$WORK_DIR"' EXIT section() { @@ -13,9 +16,11 @@ section() { smoke_cli() { local omp_bin="$1" - "$omp_bin" --version - "$omp_bin" --help >/dev/null - "$omp_bin" stats --summary >/dev/null + local runtime_dir + runtime_dir="$(mktemp -d "$WORK_DIR/compiled-runtime.XXXXXX")" + XDG_DATA_HOME="$runtime_dir/xdg" HOME="$runtime_dir/home" "$omp_bin" --version + XDG_DATA_HOME="$runtime_dir/xdg" HOME="$runtime_dir/home" "$omp_bin" --help >/dev/null + XDG_DATA_HOME="$runtime_dir/xdg" HOME="$runtime_dir/home" "$omp_bin" stats --summary >/dev/null } find_tarball() { @@ -40,15 +45,6 @@ bun --cwd=packages/coding-agent run build BINARY_DIR="$WORK_DIR/binary-bin" mkdir -p "$BINARY_DIR" cp packages/coding-agent/dist/omp "$BINARY_DIR/omp" -shopt -s nullglob -native_addons=(packages/natives/native/pi_natives.*.node) -shopt -u nullglob -if [ "${#native_addons[@]}" -eq 0 ]; then - echo "No native addon files found in packages/natives/native" - exit 1 -fi -cp "${native_addons[@]}" "$BINARY_DIR/" - smoke_cli "$BINARY_DIR/omp" section "Source install smoke"