Build Platform Binaries #5
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: Build Platform Binaries | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| upload-artifacts: | |
| description: 'Create tarballs and upload as artifacts' | |
| required: false | |
| default: false | |
| type: boolean | |
| workflow_call: | |
| inputs: | |
| upload-artifacts: | |
| description: 'Create tarballs and upload as artifacts for release' | |
| required: false | |
| default: false | |
| type: boolean | |
| jobs: | |
| # =================================================================== | |
| # glibc builds (macOS + Linux) | |
| # =================================================================== | |
| build: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: macos-15 | |
| arch: arm64 | |
| platform: darwin | |
| - os: macos-15-intel | |
| arch: x64 | |
| platform: darwin | |
| - os: ubuntu-24.04 | |
| arch: x64 | |
| platform: linux | |
| - os: ubuntu-24.04-arm | |
| arch: arm64 | |
| platform: linux | |
| runs-on: ${{ matrix.os }} | |
| name: Build ${{ matrix.platform }}-${{ matrix.arch }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # ---- Prebuild: native .napi.node binary ---- | |
| - name: Cache prebuild | |
| id: prebuild-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: packages/libgpod-node/prebuilds/ | |
| key: prebuild-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('packages/libgpod-node/native/**', 'packages/libgpod-node/binding.gyp', 'tools/prebuild/**') }} | |
| - name: Set environment | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' | |
| run: | | |
| echo "STATIC_DEPS_DIR=${GITHUB_WORKSPACE}/static-deps" >> "$GITHUB_ENV" | |
| echo "WORK_DIR=${RUNNER_TEMP}/prebuild-work" >> "$GITHUB_ENV" | |
| - name: Cache static dependencies | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' | |
| id: static-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ${{ env.STATIC_DEPS_DIR }} | |
| key: static-deps-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('tools/prebuild/build-static-deps.sh') }} | |
| # Install build toolchain (only when building prebuilds from scratch) | |
| - name: Install macOS build deps | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' && matrix.platform == 'darwin' && steps.static-cache.outputs.cache-hit != 'true' | |
| run: | | |
| brew install --quiet libplist gdk-pixbuf intltool autoconf automake libtool \ | |
| gtk-doc pkg-config gettext libpng jpeg-turbo libtiff meson ninja 2>/dev/null | |
| sudo cpan -T XML::Parser | |
| - name: Install Linux build deps | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' && matrix.platform == 'linux' && steps.static-cache.outputs.cache-hit != 'true' | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y -qq \ | |
| build-essential pkg-config python3-pip \ | |
| libglib2.0-dev libgdk-pixbuf-2.0-dev \ | |
| libplist-dev libffi-dev libsqlite3-dev \ | |
| libpng-dev libjpeg-dev libtiff-dev libxml2-dev \ | |
| intltool autoconf automake libtool gtk-doc-tools \ | |
| meson ninja-build curl | |
| - name: Build static dependencies | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' && steps.static-cache.outputs.cache-hit != 'true' | |
| run: bash tools/prebuild/build-static-deps.sh | |
| # Install only headers when static deps are cached but prebuild needs building | |
| - name: Install macOS prebuild deps | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' && matrix.platform == 'darwin' && steps.static-cache.outputs.cache-hit == 'true' | |
| run: brew install --quiet libplist gdk-pixbuf pkg-config gettext libpng jpeg-turbo libtiff 2>/dev/null | |
| - name: Install Linux prebuild deps | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' && matrix.platform == 'linux' && steps.static-cache.outputs.cache-hit == 'true' | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y -qq \ | |
| build-essential pkg-config \ | |
| libglib2.0-dev libgdk-pixbuf-2.0-dev \ | |
| libplist-dev libffi-dev \ | |
| libpng-dev libjpeg-dev libtiff-dev | |
| - uses: actions/setup-node@v4 | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' | |
| with: | |
| node-version: 20 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies (with scripts for prebuild) | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' | |
| run: bun install | |
| - name: Create prebuild | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' | |
| working-directory: packages/libgpod-node | |
| run: npx prebuildify --napi --strip | |
| - name: Verify static linking (macOS) | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' && matrix.platform == 'darwin' | |
| working-directory: packages/libgpod-node | |
| run: | | |
| echo "Dynamic dependencies:" | |
| PREBUILD=$(find prebuilds -name "*.node" | head -1) | |
| otool -L "$PREBUILD" | |
| if otool -L "$PREBUILD" | tail -n +2 | grep -E 'libgpod|libglib|libgobject|libgdk_pixbuf'; then | |
| echo "ERROR: Found unexpected dynamic dependencies" | |
| exit 1 | |
| fi | |
| - name: Verify static linking (Linux) | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' && matrix.platform == 'linux' | |
| working-directory: packages/libgpod-node | |
| run: | | |
| echo "Dynamic dependencies:" | |
| PREBUILD=$(find prebuilds -name "*.node" | head -1) | |
| ldd "$PREBUILD" | |
| if ldd "$PREBUILD" | grep -E 'libgpod|libgdk_pixbuf'; then | |
| echo "ERROR: Found unexpected dynamic dependencies" | |
| exit 1 | |
| fi | |
| - name: List prebuilds | |
| run: find packages/libgpod-node/prebuilds -type f -name "*.node" | sort | |
| # ---- Compile: TypeScript build + standalone CLI binary ---- | |
| - name: Install dependencies | |
| if: steps.prebuild-cache.outputs.cache-hit == 'true' | |
| run: bun install --frozen-lockfile --ignore-scripts | |
| - name: Build packages | |
| run: bunx turbo run build --filter=!@podkit/docs-site | |
| - name: Compile CLI binary | |
| run: bun run compile | |
| # ---- Smoke tests ---- | |
| - name: Smoke test - version | |
| run: | | |
| VERSION=$(packages/podkit-cli/bin/podkit --version) | |
| echo "Version: $VERSION" | |
| if [ -z "$VERSION" ]; then | |
| echo "ERROR: --version produced no output" | |
| exit 1 | |
| fi | |
| - name: Smoke test - help | |
| run: packages/podkit-cli/bin/podkit --help | |
| - name: Smoke test - dynamic deps (macOS) | |
| if: matrix.platform == 'darwin' | |
| run: | | |
| echo "Dynamic dependencies:" | |
| otool -L packages/podkit-cli/bin/podkit | |
| if otool -L packages/podkit-cli/bin/podkit | tail -n +2 | grep -E 'libgpod|libglib|libgobject|libgdk_pixbuf'; then | |
| echo "ERROR: Found unexpected dynamic dependencies" | |
| exit 1 | |
| fi | |
| - name: Smoke test - dynamic deps (Linux) | |
| if: matrix.platform == 'linux' | |
| run: | | |
| echo "Dynamic dependencies:" | |
| ldd packages/podkit-cli/bin/podkit || true | |
| if ldd packages/podkit-cli/bin/podkit 2>/dev/null | grep -E 'libgpod|libgdk_pixbuf'; then | |
| echo "ERROR: Found unexpected dynamic dependencies" | |
| exit 1 | |
| fi | |
| - name: Smoke test - native binding loads (isolated) | |
| run: | | |
| # Copy binary to an isolated temp dir to verify the native .node addon | |
| # is truly embedded and doesn't depend on repo-local files. | |
| ISOLATED_DIR=$(mktemp -d) | |
| cp packages/podkit-cli/bin/podkit "$ISOLATED_DIR/" | |
| mkdir -p /tmp/test-ipod/iPod_Control/iTunes | |
| OUTPUT=$("$ISOLATED_DIR/podkit" device info --device /tmp/test-ipod 2>&1) || true | |
| echo "$OUTPUT" | |
| rm -rf "$ISOLATED_DIR" | |
| # The empty iPod has no iTunesDB, so libgpod returns "Couldn't find an iPod database". | |
| # This proves the native binding loaded and executed C++ code. | |
| # If the binding failed, we'd see "Failed to load native binding" instead. | |
| if echo "$OUTPUT" | grep -qi "Failed to load native\|Native binding not found\|binding not found"; then | |
| echo "ERROR: Native addon failed to load — .node file is not embedded in the binary." | |
| exit 1 | |
| fi | |
| if ! echo "$OUTPUT" | grep -q "Could not read"; then | |
| echo "ERROR: Expected database error from libgpod (proves native binding loaded)." | |
| exit 1 | |
| fi | |
| # ---- Upload (release only) ---- | |
| - name: Create tarball | |
| if: inputs.upload-artifacts | |
| run: | | |
| cd packages/podkit-cli/bin | |
| tar czf "${GITHUB_WORKSPACE}/podkit-${{ matrix.platform }}-${{ matrix.arch }}.tar.gz" podkit | |
| - name: Upload tarball | |
| if: inputs.upload-artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: podkit-${{ matrix.platform }}-${{ matrix.arch }} | |
| path: podkit-${{ matrix.platform }}-${{ matrix.arch }}.tar.gz | |
| retention-days: 30 | |
| # =================================================================== | |
| # musl builds (Alpine Linux — for Docker and musl-based distros) | |
| # | |
| # x64: uses container: alpine (JS actions work on x64) | |
| # arm64: uses docker run (GH Actions doesn't support JS actions | |
| # in Alpine containers on ARM64 runners) | |
| # =================================================================== | |
| build-musl-x64: | |
| runs-on: ubuntu-24.04 | |
| container: alpine:3.21 | |
| name: Build linux-x64-musl | |
| steps: | |
| - name: Install git | |
| run: apk add --no-cache git | |
| - uses: actions/checkout@v4 | |
| - name: Cache prebuild | |
| id: prebuild-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: packages/libgpod-node/prebuilds/ | |
| key: prebuild-linux-x64-musl-${{ hashFiles('packages/libgpod-node/native/**', 'packages/libgpod-node/binding.gyp', 'tools/prebuild/**') }} | |
| - name: Set environment | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' | |
| run: | | |
| echo "STATIC_DEPS_DIR=${GITHUB_WORKSPACE}/static-deps" >> "$GITHUB_ENV" | |
| echo "WORK_DIR=/tmp/prebuild-work" >> "$GITHUB_ENV" | |
| - name: Cache static dependencies | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' | |
| id: static-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ${{ env.STATIC_DEPS_DIR }} | |
| key: static-deps-linux-x64-musl-${{ hashFiles('tools/prebuild/build-static-deps.sh') }} | |
| - name: Install Alpine build deps (full) | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' && steps.static-cache.outputs.cache-hit != 'true' | |
| run: | | |
| apk add --no-cache \ | |
| bash build-base pkgconf python3 curl \ | |
| glib-dev gdk-pixbuf-dev \ | |
| libplist-dev libffi-dev sqlite-dev \ | |
| libpng-dev libjpeg-turbo-dev tiff-dev libxml2-dev \ | |
| intltool autoconf automake libtool gtk-doc \ | |
| meson ninja perl-xml-parser linux-headers | |
| - name: Build static dependencies | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' && steps.static-cache.outputs.cache-hit != 'true' | |
| run: bash tools/prebuild/build-static-deps.sh | |
| - name: Install Alpine prebuild deps (headers only) | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' && steps.static-cache.outputs.cache-hit == 'true' | |
| run: | | |
| apk add --no-cache \ | |
| bash build-base pkgconf python3 \ | |
| glib-dev gdk-pixbuf-dev \ | |
| libplist-dev libffi-dev \ | |
| libpng-dev libjpeg-turbo-dev tiff-dev | |
| - name: Install Node.js | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' | |
| run: apk add --no-cache nodejs npm | |
| - name: Install Bun | |
| run: | | |
| apk add --no-cache bash curl unzip jq | |
| export BUN_INSTALL=/usr/local | |
| curl -fsSL https://bun.sh/install | bash | |
| - name: Install dependencies (with scripts for prebuild) | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' | |
| run: bun install | |
| - name: Create prebuild | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' | |
| working-directory: packages/libgpod-node | |
| run: | | |
| npx prebuildify --napi --strip | |
| mv prebuilds/linux-x64 prebuilds/linux-x64-musl | |
| - name: Verify static linking | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' | |
| working-directory: packages/libgpod-node | |
| run: | | |
| echo "Dynamic dependencies:" | |
| PREBUILD=$(find prebuilds -name "*.node" | head -1) | |
| ldd "$PREBUILD" || true | |
| if ldd "$PREBUILD" 2>/dev/null | grep -E 'libgpod|libgdk_pixbuf'; then | |
| echo "ERROR: Found unexpected dynamic dependencies" | |
| exit 1 | |
| fi | |
| - name: List prebuilds | |
| run: find packages/libgpod-node/prebuilds -type f -name "*.node" | sort | |
| - name: Install dependencies (cached prebuild) | |
| if: steps.prebuild-cache.outputs.cache-hit == 'true' | |
| run: bun install --frozen-lockfile --ignore-scripts | |
| - name: Build packages | |
| run: bunx turbo run build --filter=!@podkit/docs-site | |
| - name: Compile CLI binary | |
| run: bun run compile | |
| - name: Smoke test - version | |
| run: | | |
| VERSION=$(packages/podkit-cli/bin/podkit --version) | |
| echo "Version: $VERSION" | |
| [ -n "$VERSION" ] || { echo "ERROR: --version produced no output"; exit 1; } | |
| - name: Smoke test - help | |
| run: packages/podkit-cli/bin/podkit --help | |
| - name: Smoke test - dynamic deps | |
| run: | | |
| ldd packages/podkit-cli/bin/podkit || true | |
| if ldd packages/podkit-cli/bin/podkit 2>/dev/null | grep -E 'libgpod|libgdk_pixbuf'; then | |
| echo "ERROR: Found unexpected dynamic dependencies"; exit 1 | |
| fi | |
| - name: Smoke test - native binding loads (isolated) | |
| run: | | |
| ISOLATED_DIR=$(mktemp -d) | |
| cp packages/podkit-cli/bin/podkit "$ISOLATED_DIR/" | |
| mkdir -p /tmp/test-ipod/iPod_Control/iTunes | |
| OUTPUT=$("$ISOLATED_DIR/podkit" device info --device /tmp/test-ipod 2>&1) || true | |
| echo "$OUTPUT" | |
| rm -rf "$ISOLATED_DIR" | |
| if echo "$OUTPUT" | grep -qi "Failed to load native\|Native binding not found\|binding not found"; then | |
| echo "ERROR: Native addon failed to load — .node file is not embedded in the binary." | |
| exit 1 | |
| fi | |
| if ! echo "$OUTPUT" | grep -q "Could not read"; then | |
| echo "ERROR: Expected database error from libgpod (proves native binding loaded)." | |
| exit 1 | |
| fi | |
| - name: Create tarball | |
| if: inputs.upload-artifacts | |
| run: | | |
| cd packages/podkit-cli/bin | |
| tar czf "${GITHUB_WORKSPACE}/podkit-linux-x64-musl.tar.gz" podkit | |
| - name: Upload tarball | |
| if: inputs.upload-artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: podkit-linux-x64-musl | |
| path: podkit-linux-x64-musl.tar.gz | |
| retention-days: 30 | |
| # arm64 musl: can't use container: alpine on ARM runners (GH Actions limitation) | |
| # Instead, run checkout/cache on host, build steps via docker run. | |
| build-musl-arm64: | |
| runs-on: ubuntu-24.04-arm | |
| name: Build linux-arm64-musl | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Cache prebuild | |
| id: prebuild-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: packages/libgpod-node/prebuilds/ | |
| key: prebuild-linux-arm64-musl-${{ hashFiles('packages/libgpod-node/native/**', 'packages/libgpod-node/binding.gyp', 'tools/prebuild/**') }} | |
| - name: Cache static dependencies | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' | |
| id: static-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: static-deps | |
| key: static-deps-linux-arm64-musl-${{ hashFiles('tools/prebuild/build-static-deps.sh') }} | |
| - name: Build prebuild in Alpine | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' | |
| run: | | |
| docker run --rm \ | |
| -v "$GITHUB_WORKSPACE:/workspace" \ | |
| -w /workspace \ | |
| -e STATIC_DEPS_DIR=/workspace/static-deps \ | |
| -e WORK_DIR=/tmp/prebuild-work \ | |
| alpine:3.21 sh -c ' | |
| set -e | |
| echo "==> Installing build dependencies..." | |
| apk add --no-cache \ | |
| bash build-base pkgconf python3 curl git \ | |
| glib-dev gdk-pixbuf-dev \ | |
| libplist-dev libffi-dev sqlite-dev \ | |
| libpng-dev libjpeg-turbo-dev tiff-dev libxml2-dev \ | |
| intltool autoconf automake libtool gtk-doc \ | |
| meson ninja perl-xml-parser linux-headers \ | |
| nodejs npm | |
| if [ ! -f /workspace/static-deps/lib/libgpod.a ] || [ ! -f /workspace/static-deps/lib/libgdk_pixbuf-2.0.a ]; then | |
| echo "==> Building static dependencies..." | |
| bash tools/prebuild/build-static-deps.sh | |
| else | |
| echo "==> Static dependencies cached, skipping build" | |
| fi | |
| echo "==> Installing Bun..." | |
| export BUN_INSTALL=/usr/local | |
| curl -fsSL https://bun.sh/install | bash | |
| echo "==> Installing Node.js dependencies..." | |
| bun install | |
| echo "==> Creating prebuild..." | |
| cd packages/libgpod-node | |
| npx prebuildify --napi --strip | |
| mv prebuilds/linux-arm64 prebuilds/linux-arm64-musl | |
| echo "==> Verifying static linking..." | |
| PREBUILD=$(find prebuilds -name "*.node" | head -1) | |
| ldd "$PREBUILD" || true | |
| if ldd "$PREBUILD" 2>/dev/null | grep -E "libgpod|libgdk_pixbuf"; then | |
| echo "ERROR: Found unexpected dynamic dependencies" | |
| exit 1 | |
| fi | |
| ' | |
| - name: List prebuilds | |
| run: find packages/libgpod-node/prebuilds -type f -name "*.node" | sort | |
| - name: Build and compile in Alpine | |
| run: | | |
| docker run --rm \ | |
| -v "$GITHUB_WORKSPACE:/workspace" \ | |
| -w /workspace \ | |
| alpine:3.21 sh -c ' | |
| set -e | |
| apk add --no-cache bash curl unzip jq glib gdk-pixbuf libplist libpng libjpeg-turbo tiff | |
| echo "==> Installing Bun..." | |
| export BUN_INSTALL=/usr/local | |
| curl -fsSL https://bun.sh/install | bash | |
| echo "==> Installing dependencies..." | |
| bun install --frozen-lockfile --ignore-scripts | |
| echo "==> Building packages..." | |
| bunx turbo run build --filter=!@podkit/docs-site | |
| echo "==> Compiling CLI binary..." | |
| bun run compile | |
| echo "==> Smoke tests..." | |
| VERSION=$(packages/podkit-cli/bin/podkit --version) | |
| echo "Version: $VERSION" | |
| [ -n "$VERSION" ] || { echo "ERROR: --version produced no output"; exit 1; } | |
| packages/podkit-cli/bin/podkit --help | |
| ldd packages/podkit-cli/bin/podkit || true | |
| if ldd packages/podkit-cli/bin/podkit 2>/dev/null | grep -E "libgpod|libgdk_pixbuf"; then | |
| echo "ERROR: Found unexpected dynamic dependencies"; exit 1 | |
| fi | |
| ISOLATED_DIR=$(mktemp -d) | |
| cp packages/podkit-cli/bin/podkit "$ISOLATED_DIR/" | |
| mkdir -p /tmp/test-ipod/iPod_Control/iTunes | |
| OUTPUT=$("$ISOLATED_DIR/podkit" device info --device /tmp/test-ipod 2>&1) || true | |
| echo "$OUTPUT" | |
| rm -rf "$ISOLATED_DIR" | |
| if echo "$OUTPUT" | grep -qi "Failed to load native\|Native binding not found\|binding not found"; then | |
| echo "ERROR: Native addon failed to load — .node file is not embedded in the binary." | |
| exit 1 | |
| fi | |
| if ! echo "$OUTPUT" | grep -q "Could not read"; then | |
| echo "ERROR: Expected database error from libgpod (proves native binding loaded)." | |
| exit 1 | |
| fi | |
| ' | |
| - name: Create tarball | |
| if: inputs.upload-artifacts | |
| run: | | |
| cd packages/podkit-cli/bin | |
| tar czf "${GITHUB_WORKSPACE}/podkit-linux-arm64-musl.tar.gz" podkit | |
| - name: Upload tarball | |
| if: inputs.upload-artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: podkit-linux-arm64-musl | |
| path: podkit-linux-arm64-musl.tar.gz | |
| retention-days: 30 |