diff --git a/.github/actions/build-native/action.yml b/.github/actions/build-native/action.yml index 3d778a106..7a0da342a 100644 --- a/.github/actions/build-native/action.yml +++ b/.github/actions/build-native/action.yml @@ -34,6 +34,10 @@ inputs: cross-arch linux build, or set alone for a host-arch (x64) linux build. required: false default: "" + libc: + description: Optional Linux libc artifact qualifier (for example, musl) + required: false + default: "" rust_checks: description: Run clippy/rustfmt checks (only one matrix entry should set this) required: false @@ -135,7 +139,7 @@ runs: # --- Rust flags (shared) ------------------------------------------------ - name: Configure native Rust flags - if: inputs.target == '' + if: inputs.target == '' || inputs.arch == 'x64' shell: bash env: TARGET_ARCH: ${{ inputs.arch }} @@ -178,7 +182,7 @@ runs: if: steps.detect.outputs.on_infra == 'false' uses: Swatinem/rust-cache@v2 with: - shared-key: native-${{ inputs.platform }}-${{ inputs.arch }}-${{ inputs.variant || 'default' }}-h${{ inputs.hash }} + shared-key: native-${{ inputs.platform }}-${{ inputs.libc || 'default' }}-${{ inputs.arch }}-${{ inputs.variant || 'default' }}-h${{ inputs.hash }} cache-on-failure: true save-if: ${{ inputs.save_cache == 'true' }} cache-workspace-crates: true @@ -305,7 +309,7 @@ runs: - name: Upload native addon(s) uses: actions/upload-artifact@v4 with: - name: pi-natives-${{ inputs.platform }}-${{ inputs.arch }}${{ inputs.variant && format('-{0}', inputs.variant) || '' }}-h${{ inputs.hash }} + name: pi-natives-${{ inputs.platform }}-${{ inputs.libc && format('{0}-', inputs.libc) || '' }}${{ inputs.arch }}${{ inputs.variant && format('-{0}', inputs.variant) || '' }}-h${{ inputs.hash }} path: packages/natives/native/pi_natives.${{ inputs.platform }}-${{ inputs.arch }}*.node if-no-files-found: error # Explicit so the native_artifact_lookup canary keeps working even if diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00df0a797..3e0955943 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,6 +144,8 @@ jobs: # `actions/upload-artifact` `name:` template in build-native action. cross_platform_required=( "pi-natives-linux-arm64-h${hash}" + "pi-natives-linux-musl-x64-baseline-h${hash}" + "pi-natives-linux-musl-arm64-h${hash}" "pi-natives-darwin-x64-baseline-h${hash}" "pi-natives-darwin-arm64-h${hash}" "pi-natives-win32-x64-baseline-h${hash}" @@ -238,7 +240,7 @@ jobs: # building the artifacts that ship in releases. Skipped on main when # native_artifact_lookup already found a recent run with all artifacts intact. native_cross_platform_kata: - name: "Native: ${{ matrix.platform }} ${{ matrix.arch }}" + name: "Native: ${{ matrix.platform }} ${{ matrix.libc || '' }} ${{ matrix.arch }}" needs: [release_metadata, native_artifact_lookup] if: ${{ needs.release_metadata.outputs.is-release == 'true' || (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.native_artifact_lookup.outputs.cross-platform-run-id == '') }} strategy: @@ -246,6 +248,8 @@ jobs: matrix: include: - { os: omp-kata, platform: linux, arch: arm64, target: aarch64-unknown-linux-gnu } + - { os: omp-kata, platform: linux, libc: musl, arch: x64, target: x86_64-unknown-linux-musl, variant: baseline } + - { os: omp-kata, platform: linux, libc: musl, arch: arm64, target: aarch64-unknown-linux-musl } - { os: omp-kata, platform: win32, arch: x64, target: x86_64-pc-windows-msvc, variant: baseline } runs-on: ${{ matrix.os }} steps: @@ -255,9 +259,10 @@ jobs: hash: ${{ needs.native_artifact_lookup.outputs.source-hash }} platform: ${{ matrix.platform }} arch: ${{ matrix.arch }} + libc: ${{ matrix.libc }} variant: ${{ matrix.variant }} target: ${{ matrix.target }} - glibc: ${{ matrix.platform == 'linux' && env.GLIBC_FLOOR || '' }} + glibc: ${{ matrix.platform == 'linux' && matrix.libc != 'musl' && env.GLIBC_FLOOR || '' }} save_cache: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} native_cross_platform_macos: @@ -575,6 +580,16 @@ jobs: arch: x64, target_id: linux-x64, binary_path: packages/coding-agent/binaries/omp-linux-x64, + native_artifact_pattern: pi-natives-linux-x64-*, + } + - { + os: ubuntu-22.04, + platform: linux, + libc: musl, + arch: x64, + target_id: linux-musl-x64, + binary_path: packages/coding-agent/binaries/omp-linux-musl-x64, + native_artifact_pattern: pi-natives-linux-musl-x64-*, } - { os: ubuntu-24.04-arm, @@ -582,6 +597,16 @@ jobs: arch: arm64, target_id: linux-arm64, binary_path: packages/coding-agent/binaries/omp-linux-arm64, + native_artifact_pattern: pi-natives-linux-arm64*, + } + - { + os: ubuntu-24.04-arm, + platform: linux, + libc: musl, + arch: arm64, + target_id: linux-musl-arm64, + binary_path: packages/coding-agent/binaries/omp-linux-musl-arm64, + native_artifact_pattern: pi-natives-linux-musl-arm64*, } - { os: macos-15-intel, @@ -589,6 +614,7 @@ jobs: arch: x64, target_id: darwin-x64, binary_path: packages/coding-agent/binaries/omp-darwin-x64, + native_artifact_pattern: pi-natives-darwin-x64*, } - { os: macos-14, @@ -596,6 +622,7 @@ jobs: arch: arm64, target_id: darwin-arm64, binary_path: packages/coding-agent/binaries/omp-darwin-arm64, + native_artifact_pattern: pi-natives-darwin-arm64*, } - { os: ubuntu-22.04, @@ -603,6 +630,7 @@ jobs: arch: x64, target_id: win32-x64, binary_path: packages/coding-agent/binaries/omp-windows-x64.exe, + native_artifact_pattern: pi-natives-win32-x64*, } runs-on: ${{ matrix.os }} permissions: @@ -636,7 +664,7 @@ jobs: - name: Download native addon(s) uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - pattern: pi-natives-${{ matrix.platform }}-${{ matrix.arch }}*-h${{ needs.native_artifact_lookup.outputs.source-hash }} + pattern: ${{ matrix.native_artifact_pattern }}-h${{ needs.native_artifact_lookup.outputs.source-hash }} path: packages/natives/native merge-multiple: true - name: Build release binary @@ -666,8 +694,17 @@ jobs: runtime_dir="$(mktemp -d)" HOME="$runtime_dir/home" XDG_DATA_HOME="$runtime_dir/xdg" "${{ matrix.binary_path }}" --version HOME="$runtime_dir/home" XDG_DATA_HOME="$runtime_dir/xdg" "${{ matrix.binary_path }}" --smoke-test + - name: Smoke musl release binary on Alpine + if: matrix.libc == 'musl' + run: | + binary="$(realpath "${{ matrix.binary_path }}")" + docker run --rm -v "$binary:/usr/local/bin/omp:ro" alpine:3.22 sh -c ' + runtime_dir="$(mktemp -d)" + HOME="$runtime_dir/home" XDG_DATA_HOME="$runtime_dir/xdg" omp --version + HOME="$runtime_dir/home" XDG_DATA_HOME="$runtime_dir/xdg" omp --smoke-test + ' - name: Publish native addon package - if: ${{ !inputs.skip_npm }} + if: ${{ !inputs.skip_npm && matrix.libc != 'musl' }} env: # Fallback auth: setup-node wrote an .npmrc referencing # NODE_AUTH_TOKEN; npm uses it only when OIDC has no trusted diff --git a/package.json b/package.json index 7372fc6e7..d649ec4bd 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "build:native": "bun --cwd=packages/natives run build", "test": "bun scripts/ci-test-ts.ts local", "test:ts": "bun scripts/ci-test-ts.ts local-ts", - "test:scripts": "bun test scripts/ci-build-native.test.ts scripts/ci-concurrency.test.ts scripts/ci-release-build-binaries.test.ts scripts/ci-release-notes.test.ts scripts/ci-release-publish.test.ts scripts/fix-dts-extensions.test.ts scripts/link-omp.test.ts", + "test:scripts": "bun test scripts/ci-build-native.test.ts scripts/ci-concurrency.test.ts scripts/ci-release-build-binaries.test.ts scripts/ci-release-notes.test.ts scripts/ci-release-publish.test.ts scripts/fix-dts-extensions.test.ts scripts/link-omp.test.ts scripts/musl-release.test.ts", "test:rs": "bun scripts/run-rs-task.ts test:rs", "check": "bun run --parallel check:ts check:rs", "check:ts": "bun run check:tools && bun run --workspaces --if-present check", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index b82ded227..cf15fa112 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -747,6 +747,7 @@ - Redesigned OAuth logins to run inside a cancellable dialog (aborted via Esc) rather than an inescapable pairing prompt. - Reworked the subagent soft request budget to gracefully steer child agents to wrap up and yield partial findings instead of silently terminating them, and raised the default budget to 200 requests. - Updated task rendering to retain the agent type badge on live progress and finished result rows. +- Added `omp-linux-musl-x64` and `omp-linux-musl-arm64` release binaries for Alpine and other musl-based Linux distributions, with automatic musl selection in the binary installer. ([#3367](https://github.com/can1357/oh-my-pi/issues/3367)) ### Fixed diff --git a/scripts/ci-release-build-binaries.ts b/scripts/ci-release-build-binaries.ts index e2dc67071..004af66be 100644 --- a/scripts/ci-release-build-binaries.ts +++ b/scripts/ci-release-build-binaries.ts @@ -59,6 +59,20 @@ const targets: BinaryTarget[] = [ target: "bun-linux-arm64", outfile: "packages/coding-agent/binaries/omp-linux-arm64", }, + { + id: "linux-musl-x64", + platform: "linux", + arch: "x64", + target: "bun-linux-x64-musl-baseline", + outfile: "packages/coding-agent/binaries/omp-linux-musl-x64", + }, + { + id: "linux-musl-arm64", + platform: "linux", + arch: "arm64", + target: "bun-linux-arm64-musl", + outfile: "packages/coding-agent/binaries/omp-linux-musl-arm64", + }, { id: "win32-x64", platform: "win32", diff --git a/scripts/install.sh b/scripts/install.sh index c3c2ac70e..ae40e0161 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -232,6 +232,12 @@ install_binary() { *) echo "Unsupported architecture: $ARCH"; exit 1 ;; esac + if [ "$PLATFORM" = "linux" ]; then + if [ -f /etc/alpine-release ] || { command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; }; then + PLATFORM="linux-musl" + fi + fi + BINARY="omp-${PLATFORM}-${ARCH}" # Get release tag if [ -n "$REF" ]; then diff --git a/scripts/musl-release.test.ts b/scripts/musl-release.test.ts new file mode 100644 index 000000000..5c04194e2 --- /dev/null +++ b/scripts/musl-release.test.ts @@ -0,0 +1,86 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import * as fs from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; + +const repoRoot = path.join(import.meta.dir, ".."); +const tempDirs: string[] = []; + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map(dir => fs.rm(dir, { recursive: true, force: true }))); +}); + +async function run(command: string[], env: NodeJS.ProcessEnv = process.env): Promise<{ exitCode: number; stdout: string; stderr: string }> { + const proc = Bun.spawn(command, { + cwd: repoRoot, + env, + stdout: "pipe", + stderr: "pipe", + }); + const [exitCode, stdout, stderr] = await Promise.all([ + proc.exited, + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]); + return { exitCode, stdout, stderr }; +} + +async function writeExecutable(directory: string, name: string, content: string): Promise { + const file = path.join(directory, name); + await Bun.write(file, content); + await fs.chmod(file, 0o755); +} + +describe("musl release artifacts", () => { + test("builds the requested x64 and arm64 musl asset names with Bun's musl targets", async () => { + const result = await run([ + "bun", + "scripts/ci-release-build-binaries.ts", + "--dry-run", + "--targets", + "linux-musl-x64,linux-musl-arm64", + ]); + + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout).toContain( + "--target=bun-linux-x64-musl-baseline ./packages/coding-agent/src/cli.ts --outfile packages/coding-agent/binaries/omp-linux-musl-x64", + ); + expect(result.stdout).toContain( + "--target=bun-linux-arm64-musl ./packages/coding-agent/src/cli.ts --outfile packages/coding-agent/binaries/omp-linux-musl-arm64", + ); + }); + + test("selects the musl asset when the Linux host reports musl", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-musl-install-")); + tempDirs.push(dir); + const binDir = path.join(dir, "bin"); + const installDir = path.join(dir, "install"); + await fs.mkdir(binDir); + await writeExecutable(binDir, "uname", '#!/bin/sh\n[ "$1" = "-s" ] && echo Linux || echo x86_64\n'); + await writeExecutable(binDir, "ldd", "#!/bin/sh\necho 'musl libc (x86_64)'\n"); + await writeExecutable( + binDir, + "curl", + `#!/bin/sh +case "$*" in + *api.github.com*) echo '{"tag_name":"v1.0.0"}' ;; + *) while [ "$#" -gt 0 ]; do + [ "$1" = "-o" ] && { printf binary > "$2"; exit 0; } + shift + done ;; +esac +`, + ); + + const result = await run(["sh", "scripts/install.sh", "--binary"], { + ...process.env, + PATH: `${binDir}:${process.env.PATH ?? ""}`, + HOME: dir, + PI_INSTALL_DIR: installDir, + }); + + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout).toContain("Downloading omp-linux-musl-x64..."); + expect(await Bun.file(path.join(installDir, "omp")).text()).toBe("binary"); + }); +});