From 6efcdfb66e0c19fb38c94d0c305b6955e6009173 Mon Sep 17 00:00:00 2001 From: roboomp Date: Wed, 22 Jul 2026 19:06:20 +0000 Subject: [PATCH 1/2] feat(release): added musl-linked linux builds Built x64 and arm64 musl release artifacts with matching native addons, Alpine smoke coverage, and installer detection. Fixes #3367 --- .github/actions/build-native/action.yml | 10 ++- .github/workflows/ci.yml | 43 ++++++++++++- package.json | 2 +- packages/coding-agent/CHANGELOG.md | 4 ++ scripts/ci-release-build-binaries.ts | 14 ++++ scripts/install.sh | 6 ++ scripts/musl-release.test.ts | 86 +++++++++++++++++++++++++ 7 files changed, 158 insertions(+), 7 deletions(-) create mode 100644 scripts/musl-release.test.ts 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 7dbe06010..1f948de55 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,6 +259,7 @@ 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 || '' }} @@ -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: @@ -635,7 +663,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 @@ -665,8 +693,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 772ce431e..95e558ff0 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "build:native": "bun --cwd=packages/natives run build", "test": "bun run --parallel test:ts test:rs", "test:ts": "GITHUB_ACTIONS= bun run --workspaces --if-present test -- --only-failures && bun run test:scripts", - "test:scripts": "bun test scripts/ci-concurrency.test.ts scripts/ci-release-notes.test.ts", + "test:scripts": "bun test scripts/ci-concurrency.test.ts scripts/ci-release-notes.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 03ec72a40..79c4fdc37 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- 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 - Fixed all extension loading silently failing on the cross-compiled `omp-darwin-arm64` release binary (downloaded directly or via a Homebrew tap wrapper) because `__computeBunfsPackageRoot` mis-handled `import.meta.dir = "//root/omp-darwin-arm64"`. Bun 1.3.14 reports `/` for the compiled entry's `import.meta.dir`, but the pre-fix function joined `metaDir + "packages"` and produced `/root/omp-darwin-arm64/packages` — the binary basename was baked into every bunfs path, so the TypeBox/legacy-pi shims and every `@oh-my-pi/pi-*` package-root override failed `existsSync` validation and `resolveCanonicalPiSpecifier` fell through to a bunfs `Bun.resolveSync` that also could not find the module. The function now detects the bunfs-root + binary-basename shape (`path.basename(path.dirname(metaDir)) === "root"`) and strips the trailing binary segment by slicing the original `metaDir`; the production bunfs shim join path also preserves Bun's bunfs-native `//root` / `B:\~BUN\root` prefix that `path.join` would otherwise collapse. ([#3329](https://github.com/can1357/oh-my-pi/issues/3329)) diff --git a/scripts/ci-release-build-binaries.ts b/scripts/ci-release-build-binaries.ts index 8a6f65e2a..d5b4db97b 100644 --- a/scripts/ci-release-build-binaries.ts +++ b/scripts/ci-release-build-binaries.ts @@ -48,6 +48,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 d65859483..272834a92 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -201,6 +201,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"); + }); +}); From 83cb70eafcb9164d6a10d5cda03cd17076b3f269 Mon Sep 17 00:00:00 2001 From: roboomp Date: Wed, 22 Jul 2026 19:11:57 +0000 Subject: [PATCH 2/2] fix(release): skip glibc floor for musl native builds The glibc floor input was applied to every linux row, suffixing musl cross-targets to invalid *-unknown-linux-musl.2.17 triples. Gate the floor on non-musl libc. Fixes #3367 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f948de55..ee5ac7ea1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -262,7 +262,7 @@ jobs: 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: