Skip to content

Build Platform Binaries #5

Build Platform Binaries

Build Platform Binaries #5

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