feat(npm): add Linux musl and ARM64 N-API binaries (#233) #85
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publish npm package | |
| on: | |
| push: | |
| branches: [main] | |
| paths: ['napi/package.json'] | |
| # Manual fallback: retry a publish that failed partway (per-package | |
| # already-published checks make re-runs idempotent). | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| id-token: write | |
| jobs: | |
| check-version: | |
| name: Check version change | |
| # Guard manual dispatches: npm trusted publishing matches | |
| # repo+workflow+environment but NOT branch, so without this a | |
| # workflow_dispatch from any branch could publish unmerged code. | |
| if: github.ref == 'refs/heads/main' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| changed: ${{ steps.check.outputs.changed }} | |
| version: ${{ steps.check.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 2 | |
| - name: Check if version changed | |
| id: check | |
| run: | | |
| NEW_VERSION=$(node -p "require('./napi/package.json').version") | |
| echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT" | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| # Manual dispatch rebuilds and publishes the current version; the | |
| # per-package already-published checks in the publish job skip | |
| # anything that made it out in a previous partial run. | |
| echo "manual dispatch: publishing v$NEW_VERSION" | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| OLD_VERSION=$(git show HEAD~1:napi/package.json | node -p "JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).version") | |
| echo "old=$OLD_VERSION new=$NEW_VERSION" | |
| if [ "$NEW_VERSION" != "$OLD_VERSION" ]; then | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "changed=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| build: | |
| needs: check-version | |
| if: needs.check-version.outputs.changed == 'true' | |
| name: Build ${{ matrix.target }} | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-gnu | |
| # napi-cross builds gnu targets against an old glibc sysroot for | |
| # broad distro compatibility; musl targets cross-compile with | |
| # zig via cargo-zigbuild (napi's -x flag). | |
| - os: ubuntu-latest | |
| target: aarch64-unknown-linux-gnu | |
| build-flags: --use-napi-cross | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-musl | |
| build-flags: -x | |
| - os: ubuntu-latest | |
| target: aarch64-unknown-linux-musl | |
| build-flags: -x | |
| - os: macos-14 | |
| target: aarch64-apple-darwin | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Install zig | |
| if: contains(matrix.target, 'musl') | |
| uses: mlugg/setup-zig@v2 | |
| with: | |
| version: 0.14.1 | |
| - name: Install cargo-zigbuild | |
| if: contains(matrix.target, 'musl') | |
| uses: taiki-e/install-action@v2 | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| with: | |
| tool: cargo-zigbuild | |
| - name: Cache cargo | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry/index/ | |
| ~/.cargo/registry/cache/ | |
| ~/.cargo/git/db/ | |
| ~/.napi-rs | |
| napi/target/ | |
| key: ${{ matrix.target }}-cargo-napi-${{ hashFiles('**/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ matrix.target }}-cargo-napi- | |
| - name: Install dependencies | |
| working-directory: napi | |
| run: bun install | |
| - name: Build native addon | |
| working-directory: napi | |
| run: bunx napi build --platform --release --target ${{ matrix.target }} ${{ matrix.build-flags }} | |
| - name: Upload native binary | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: bindings-${{ matrix.target }} | |
| path: napi/*.node | |
| if-no-files-found: error | |
| - name: Upload generated JS bindings | |
| if: matrix.target == 'x86_64-unknown-linux-gnu' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: js-bindings | |
| path: | | |
| napi/index.js | |
| napi/index.d.ts | |
| if-no-files-found: error | |
| smoke-test: | |
| name: Smoke test ${{ matrix.target }} | |
| needs: [check-version, build] | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-gnu | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-musl | |
| - os: ubuntu-24.04-arm | |
| target: aarch64-unknown-linux-gnu | |
| - os: ubuntu-24.04-arm | |
| target: aarch64-unknown-linux-musl | |
| - os: macos-14 | |
| target: aarch64-apple-darwin | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download native binary | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: bindings-${{ matrix.target }} | |
| path: napi | |
| - name: Download generated JS bindings | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: js-bindings | |
| path: napi | |
| # musl binaries must load under a real musl libc, so run inside Alpine. | |
| - name: Run smoke test (Alpine) | |
| if: contains(matrix.target, 'musl') | |
| run: docker run --rm -v "$PWD:/repo" -w /repo/napi node:24-alpine node test.mjs | |
| - name: Run smoke test | |
| if: ${{ !contains(matrix.target, 'musl') }} | |
| working-directory: napi | |
| run: node test.mjs | |
| publish: | |
| name: Publish to npm | |
| needs: [check-version, build, smoke-test] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: '24' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: napi/artifacts | |
| - name: Publish platform packages | |
| working-directory: napi | |
| run: | | |
| VERSION="${{ needs.check-version.outputs.version }}" | |
| for node_file in artifacts/bindings-*/pdf-inspector.*.node; do | |
| base=$(basename "$node_file") | |
| suffix=${base#pdf-inspector.} | |
| suffix=${suffix%.node} | |
| pkg="@firecrawl/pdf-inspector-$suffix" | |
| if npm view "$pkg@$VERSION" version >/dev/null 2>&1; then | |
| echo "$pkg@$VERSION already published — skipping" | |
| continue | |
| fi | |
| dir="npm-dist/$suffix" | |
| mkdir -p "$dir" | |
| cp "$node_file" "$dir/" | |
| node -e ' | |
| const [suffix, version] = process.argv.slice(1) | |
| const meta = { | |
| "linux-x64-gnu": { os: ["linux"], cpu: ["x64"], libc: ["glibc"] }, | |
| "linux-x64-musl": { os: ["linux"], cpu: ["x64"], libc: ["musl"] }, | |
| "linux-arm64-gnu": { os: ["linux"], cpu: ["arm64"], libc: ["glibc"] }, | |
| "linux-arm64-musl": { os: ["linux"], cpu: ["arm64"], libc: ["musl"] }, | |
| "darwin-arm64": { os: ["darwin"], cpu: ["arm64"] }, | |
| "win32-x64-msvc": { os: ["win32"], cpu: ["x64"] }, | |
| }[suffix] | |
| if (!meta) { | |
| console.error(`unknown platform suffix: ${suffix} — add it to the meta map`) | |
| process.exit(1) | |
| } | |
| const pkg = { | |
| name: `@firecrawl/pdf-inspector-${suffix}`, | |
| version, | |
| description: `Prebuilt ${suffix} binary for @firecrawl/pdf-inspector`, | |
| main: `pdf-inspector.${suffix}.node`, | |
| files: [`pdf-inspector.${suffix}.node`], | |
| license: "MIT", | |
| engines: { node: ">= 10" }, | |
| repository: { type: "git", url: "https://github.com/firecrawl/pdf-inspector" }, | |
| publishConfig: { access: "public" }, | |
| ...meta, | |
| } | |
| require("fs").writeFileSync(`npm-dist/${suffix}/package.json`, JSON.stringify(pkg, null, 2) + "\n") | |
| ' "$suffix" "$VERSION" | |
| echo "=== $pkg@$VERSION ===" | |
| ls -la "$dir" | |
| (cd "$dir" && npm publish --provenance --access public) | |
| done | |
| - name: Publish main package | |
| working-directory: napi | |
| run: | | |
| VERSION="${{ needs.check-version.outputs.version }}" | |
| if npm view "@firecrawl/pdf-inspector@$VERSION" version >/dev/null 2>&1; then | |
| echo "@firecrawl/pdf-inspector@$VERSION already published — skipping" | |
| exit 0 | |
| fi | |
| cp artifacts/js-bindings/index.js . | |
| cp artifacts/js-bindings/index.d.ts . | |
| # Stamp optionalDependencies to this exact version so the platform | |
| # pins can never drift from the main package version. | |
| node -e ' | |
| const fs = require("fs") | |
| const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")) | |
| for (const dep of Object.keys(pkg.optionalDependencies ?? {})) { | |
| pkg.optionalDependencies[dep] = pkg.version | |
| } | |
| fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n") | |
| ' | |
| echo "=== Main package contents ===" | |
| npm pack --dry-run | |
| npm publish --provenance --access public |