Files
oh-my-pi/.github/actions/build-native/action.yml
T
can1357 5f988a8270 ci: configured native artifact caching and parallel execution in ci workflows
- Enhanced CI workflows and GitHub actions to support native artifact caching and parallel builds.
- Added composite actions and scripts for computing sources, finding artifacts, and managing caches.
- Updated infrastructure documentation and runner deployment scripts with revised resource limits.
2026-07-27 07:53:28 +02:00

428 lines
20 KiB
YAML

name: Build native addon
description: >
Build the pi_natives cdylib for one platform/arch/variant and upload it as a
hash-tagged artifact. Self-detects the runner via $SCCACHE_BUCKET (injected
only on the self-hosted omp-kata pods): on-infra it uses the image's baked
toolchains + the RustFS-backed sccache; on GitHub-hosted runners it installs
the toolchains and uses Swatinem target/ cache + the GitHub Actions sccache
backend. PRs run on GitHub-hosted runners, so the on-infra path only ever
serves trusted push/main + release builds.
inputs:
hash:
description: Rust source hash used in the artifact name
required: true
platform:
description: Target platform (linux, darwin, win32)
required: true
arch:
description: Target arch (x64, arm64)
required: true
variant:
description: Optional build variant (baseline, modern); required for native x64 builds.
required: false
default: ""
target:
description: Optional rustc target triple for cross-compilation
required: false
default: ""
glibc:
description: >
Optional glibc floor override for linux-gnu builds (defaults to "2.17").
Routes the build through cargo-zigbuild against that floor without
affecting the rustup target or host-arch native test steps.
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
default: "false"
skip_validation:
description: Skip clippy/rustfmt and the Rust test suite for build-only matrix entries.
required: false
default: "false"
skip_build:
description: Run validation and cache population without building or uploading a native addon.
required: false
default: "false"
cache_scope:
description: Separates target snapshots whose Cargo work differs (for example, build and validation).
required: false
default: "build"
save_cache:
description: Whether to persist the GitHub-hosted or RustFS target snapshot.
required: false
default: "false"
runs:
using: composite
steps:
- name: Detect runner environment
id: detect
shell: bash
run: |
# $SCCACHE_BUCKET is injected only on the self-hosted omp-kata runner
# pods (envFrom sccache-s3); its presence is the repo's single
# "on can.internal infra?" signal. On-infra: baked toolchains, RustFS
# sccache, no GitHub target/ cache. Off-infra (GitHub-hosted): install
# toolchains, Swatinem target/ cache, GitHub Actions sccache backend.
if [ -n "${SCCACHE_BUCKET:-}" ]; then
echo "on_infra=true" >> "$GITHUB_OUTPUT"
echo "runner: self-hosted omp-kata (baked tools + RustFS sccache)"
else
echo "on_infra=false" >> "$GITHUB_OUTPUT"
echo "runner: GitHub-hosted (install tools + Swatinem cache + GHA sccache)"
fi
- name: Resolve build targets
id: resolve
shell: bash
env:
TARGET: ${{ inputs.target }}
GLIBC: ${{ inputs.glibc }}
PLATFORM: ${{ inputs.platform }}
LIBC: ${{ inputs.libc }}
ARCH: ${{ inputs.arch }}
run: |
set -euo pipefail
# Keep the portability floor here: this action is included in the
# native source hash, and every workflow then consumes one value.
if [ "$PLATFORM" = linux ] && [ "$LIBC" != musl ]; then
GLIBC="${GLIBC:-2.17}"
elif [ -n "$GLIBC" ]; then
echo "::error::glibc floor '$GLIBC' is only valid for linux-gnu builds"
exit 1
fi
# `cross_target` is what the napi build feeds cargo: cargo-zigbuild reads
# the glibc floor as a `.<major>.<minor>` triple suffix. `bare_target` is
# what rustup needs — never glibc-suffixed (rustup rejects the suffix)
# and empty for host-arch builds whose target is already installed.
base="$TARGET"
if [ -z "$base" ]; then
case "$ARCH" in
x64) base="x86_64-unknown-linux-gnu" ;;
arm64) base="aarch64-unknown-linux-gnu" ;;
esac
fi
cross_target=""
if [ -n "$GLIBC" ]; then
case "$base" in
*-linux-gnu) cross_target="${base}.${GLIBC}" ;;
*)
echo "::error::glibc floor '$GLIBC' requires a linux-gnu target, got '$base'"
exit 1
;;
esac
elif [ -n "$TARGET" ]; then
cross_target="$TARGET"
fi
bare_target="${TARGET%%.*}"
{
echo "cross_target=$cross_target"
echo "bare_target=$bare_target"
} >> "$GITHUB_OUTPUT"
echo "Resolved cross_target='$cross_target' bare_target='$bare_target'"
# --- Rust toolchain -----------------------------------------------------
- name: Ensure baked Rust toolchain (omp-kata)
if: steps.detect.outputs.on_infra == 'true'
uses: ./.github/actions/ensure-rust-toolchain
with:
toolchain: nightly-2026-04-29
components: ${{ inputs.rust_checks == 'true' && 'clippy,rustfmt' || '' }}
target: ${{ steps.resolve.outputs.bare_target }}
- name: Install Rust toolchain (GitHub-hosted)
if: steps.detect.outputs.on_infra == 'false'
uses: dtolnay/rust-toolchain@nightly
with:
toolchain: nightly-2026-04-29
components: ${{ inputs.rust_checks == 'true' && 'clippy, rustfmt' || '' }}
targets: ${{ steps.resolve.outputs.bare_target }}
- name: Install Linux build prerequisites (GitHub-hosted)
if: steps.detect.outputs.on_infra == 'false' && runner.os == 'Linux'
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y build-essential
# audiopus_sys builds bundled libopus via CMake (Ninja generator for MSVC
# cross); baked into the omp-kata image and GitHub-hosted images, so this
# only self-heals runners that predate the bake.
- uses: ./.github/actions/ensure-cmake
- name: Prepend rustup toolchain bin to PATH (GitHub-hosted)
if: steps.detect.outputs.on_infra == 'false'
shell: bash
run: |
# Homebrew on macOS runners ships rustup-init with shadow proxies for
# `cargo`/`rustc`/etc. that error out as the installer ("unexpected
# argument 'metadata' found"). Force the real toolchain binaries to win
# on PATH. (ensure-rust-toolchain already does this on omp-kata.)
toolchain_bin="$(dirname "$(rustup which cargo)")"
echo "$toolchain_bin" >> "$GITHUB_PATH"
echo "Prepended $toolchain_bin to PATH"
# --- Rust flags (shared) ------------------------------------------------
- name: Configure native Rust flags
if: inputs.target == '' || inputs.arch == 'x64'
shell: bash
env:
TARGET_ARCH: ${{ inputs.arch }}
TARGET_VARIANT: ${{ inputs.variant }}
run: |
case "$TARGET_ARCH:$TARGET_VARIANT" in
x64:modern)
rustflags="-C target-cpu=x86-64-v3"
;;
x64:baseline)
rustflags="-C target-cpu=x86-64-v2"
;;
x64:*)
echo "::error::x64 native builds require variant=modern or variant=baseline"
exit 1
;;
*)
if [ -n "${RUSTFLAGS:-}" ]; then
echo "Using caller-provided RUSTFLAGS=$RUSTFLAGS"
exit 0
fi
# Non-x64 native builds (darwin arm64) keep the target's default
# CPU features. `-C target-cpu=native` both baked the CI host's
# CPU features into shipped artifacts and trips ring 0.17's
# aarch64-apple const assertion (CAPS_STATIC == MIN_STATIC_FEATURES)
# once extra static features are enabled.
echo "Using default target CPU features (no RUSTFLAGS)"
exit 0
;;
esac
echo "RUSTFLAGS=$rustflags" >> "$GITHUB_ENV"
echo "Configured RUSTFLAGS=$rustflags"
# --- Cargo + rolling target cache (GitHub-hosted only) ------------------
# Swatinem keeps the registry/tool cache. target/ uses an explicit rolling
# key: a new source hash restores the latest compatible snapshot through
# restore-keys, then saves the rebuilt state under a fresh immutable key.
# This is especially important for the three-core Intel macOS runner.
- name: Cache Cargo dependencies (GitHub-hosted)
if: steps.detect.outputs.on_infra == 'false'
uses: Swatinem/rust-cache@v2
with:
shared-key: native-deps-${{ inputs.platform }}-${{ inputs.libc || 'default' }}-${{ inputs.arch }}
cache-on-failure: true
save-if: ${{ inputs.save_cache == 'true' }}
cache-targets: false
- name: Restore rolling Rust target/ (GitHub-hosted)
id: gha-target
if: steps.detect.outputs.on_infra == 'false'
uses: actions/cache/restore@v4
with:
path: target
key: native-target-v1-${{ runner.os }}-${{ runner.arch }}-${{ inputs.platform }}-${{ inputs.libc || 'default' }}-${{ inputs.arch }}-${{ inputs.variant || 'default' }}-${{ inputs.cache_scope }}-${{ hashFiles('rust-toolchain.toml') }}-h${{ inputs.hash }}
restore-keys: |
native-target-v1-${{ runner.os }}-${{ runner.arch }}-${{ inputs.platform }}-${{ inputs.libc || 'default' }}-${{ inputs.arch }}-${{ inputs.variant || 'default' }}-${{ inputs.cache_scope }}-${{ hashFiles('rust-toolchain.toml') }}-
# --- sccache ------------------------------------------------------------
- name: Ensure baked sccache (omp-kata)
if: steps.detect.outputs.on_infra == 'true'
uses: ./.github/actions/ensure-sccache
with:
version: "0.15.0"
- name: Setup sccache (GitHub-hosted)
if: steps.detect.outputs.on_infra == 'false'
uses: mozilla-actions/sccache-action@v0.0.10
- name: Enable sccache for cargo
# CARGO_INCREMENTAL=0 is required: sccache silently skips caching when
# incremental is enabled, turning the wrapper into a no-op. The backend is
# conditional: omp-kata reads the shared S3 (RustFS) config from the
# inherited pod env; GitHub-hosted runners use the GHA cache backend.
shell: bash
env:
CROSS_TARGET: ${{ steps.resolve.outputs.cross_target }}
run: |
jobs="${OMP_CI_CPU_COUNT:-$(getconf _NPROCESSORS_ONLN)}"
if [ -z "${OMP_CI_CPU_COUNT:-}" ] && [ -r /sys/fs/cgroup/cpu.max ]; then
read -r quota period < /sys/fs/cgroup/cpu.max
if [ "$quota" != "max" ]; then
quota_jobs=$((quota / period))
[ "$quota_jobs" -ge 1 ] || quota_jobs=1
[ "$quota_jobs" -ge "$jobs" ] || jobs="$quota_jobs"
fi
fi
{
echo "OMP_CI_CPU_COUNT=$jobs"
echo "RUSTC_WRAPPER=sccache"
echo "CARGO_INCREMENTAL=0"
echo "CARGO_BUILD_JOBS=$jobs"
echo "CMAKE_BUILD_PARALLEL_LEVEL=$jobs"
echo "NEXTEST_TEST_THREADS=$jobs"
} >> "$GITHUB_ENV"
echo "Native build parallelism: $jobs"
# Route CMake-built C (audiopus_sys' bundled opus) through sccache
# too — build scripts bypass RUSTC_WRAPPER. Non-cross builds only:
# cross builds compile C with zig cc / clang-cl wrapper scripts that
# sccache may fail to classify, which would hard-fail the compile.
if [ -z "$CROSS_TARGET" ]; then
{
echo "CMAKE_C_COMPILER_LAUNCHER=sccache"
echo "CMAKE_CXX_COMPILER_LAUNCHER=sccache"
} >> "$GITHUB_ENV"
fi
if [ -n "${SCCACHE_BUCKET:-}" ]; then
echo "sccache backend: shared S3 ($SCCACHE_BUCKET @ $SCCACHE_ENDPOINT)"
else
echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
echo "sccache backend: GitHub Actions cache"
fi
# --- cargo-nextest (native test runner; non-cross builds only) ----------
- name: Ensure baked cargo-nextest (omp-kata)
if: steps.detect.outputs.on_infra == 'true' && inputs.target == '' && inputs.skip_validation != 'true'
uses: ./.github/actions/ensure-cargo-tool
with:
binary: cargo-nextest
crate: cargo-nextest
- name: Install cargo-nextest (GitHub-hosted)
if: steps.detect.outputs.on_infra == 'false' && inputs.target == '' && inputs.skip_validation != 'true'
uses: taiki-e/install-action@v2
with:
tool: nextest
- uses: ./.github/actions/bun-install
# --- Cross-compile toolchains -------------------------------------------
# Non-MSVC targets (e.g. aarch64-unknown-linux-gnu) build with
# cargo-zigbuild (needs zig); MSVC targets (e.g. x86_64-pc-windows-msvc)
# build with cargo-xwin (needs clang/lld/llvm). The napi CLI's
# --cross-compile flag picks the backend; we just install what it needs.
# Cross builds only run on push/main + release (omp-kata), so the
# GitHub-hosted cross branches exist for portability and never fire here.
- name: Ensure baked zig (omp-kata, non-MSVC cross)
if: steps.detect.outputs.on_infra == 'true' && steps.resolve.outputs.cross_target != '' && !endsWith(steps.resolve.outputs.cross_target, '-msvc')
uses: ./.github/actions/ensure-zig
with:
version: "0.16.0"
- name: Setup zig (GitHub-hosted, non-MSVC cross)
if: steps.detect.outputs.on_infra == 'false' && steps.resolve.outputs.cross_target != '' && !endsWith(steps.resolve.outputs.cross_target, '-msvc')
uses: mlugg/setup-zig@v2
with:
version: 0.16.0
- name: Ensure baked cargo-zigbuild (omp-kata, non-MSVC cross)
if: steps.detect.outputs.on_infra == 'true' && steps.resolve.outputs.cross_target != '' && !endsWith(steps.resolve.outputs.cross_target, '-msvc')
uses: ./.github/actions/ensure-cargo-tool
with:
binary: cargo-zigbuild
crate: cargo-zigbuild
- name: Install cargo-zigbuild (GitHub-hosted, non-MSVC cross)
if: steps.detect.outputs.on_infra == 'false' && steps.resolve.outputs.cross_target != '' && !endsWith(steps.resolve.outputs.cross_target, '-msvc')
uses: taiki-e/install-action@v2
with:
tool: cargo-zigbuild
- name: Install LLVM tooling (GitHub-hosted, MSVC cross)
if: steps.detect.outputs.on_infra == 'false' && endsWith(steps.resolve.outputs.cross_target, '-msvc')
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y clang lld llvm
- name: Ensure baked cargo-xwin (omp-kata, MSVC cross)
if: steps.detect.outputs.on_infra == 'true' && endsWith(steps.resolve.outputs.cross_target, '-msvc')
uses: ./.github/actions/ensure-cargo-tool
with:
binary: cargo-xwin
crate: cargo-xwin
- name: Install cargo-xwin (GitHub-hosted, MSVC cross)
if: steps.detect.outputs.on_infra == 'false' && endsWith(steps.resolve.outputs.cross_target, '-msvc')
uses: taiki-e/install-action@v2
with:
tool: cargo-xwin
- name: Cache cargo-xwin Windows SDK
if: endsWith(steps.resolve.outputs.cross_target, '-msvc')
uses: actions/cache@v4
with:
path: ~/.cache/cargo-xwin
key: cargo-xwin-${{ runner.os }}-v1
- name: Accept xwin license
if: endsWith(steps.resolve.outputs.cross_target, '-msvc')
shell: bash
run: echo "XWIN_ACCEPT_LICENSE=1" >> "$GITHUB_ENV"
# --- target/ cache (omp-kata) --------------------------------------------
# sccache only covers rustc invocations; build-script outputs (57
# tree-sitter grammar C compiles, bundled opus via CMake, ring asm) and
# cargo's fingerprint/link work bypass it. Snapshot target/ to the same
# RustFS S3 bucket, keyed per platform/libc/arch/variant + toolchain and
# overwritten on each save so storage stays bounded at one snapshot per
# key. GitHub-hosted runners get the same effect from the rolling cache above.
- name: Verify target cache compressor (omp-kata)
if: steps.detect.outputs.on_infra == 'true'
shell: bash
run: zstd --version
- name: Restore target/ cache (omp-kata)
if: steps.detect.outputs.on_infra == 'true'
shell: bash
env:
TARGET_CACHE_KEY: native-${{ inputs.platform }}-${{ inputs.libc || 'default' }}-${{ inputs.arch }}-${{ inputs.variant || 'default' }}-${{ inputs.cache_scope }}
run: bun scripts/ci-target-cache.ts restore "$TARGET_CACHE_KEY"
# --- Checks, build, upload (shared) -------------------------------------
- name: Rust checks
if: inputs.rust_checks == 'true' && inputs.skip_validation != 'true'
shell: bash
run: bun run check:rs
- name: Test workspace (Rust)
# macOS has no `#[cfg(target_os = "macos")]` tests in the workspace, and
# Windows-only tests are no longer exercised in CI (win32-x64 cross-builds
# on Linux). Skipping the duplicate Linux runs on macOS saves ~10 min of
# parallel runner time.
if: inputs.target == '' && inputs.platform != 'darwin' && inputs.skip_validation != 'true'
shell: bash
run: bun run test:rs
- name: Build native addon(s)
if: inputs.skip_build != 'true'
shell: bash
env:
CROSS_TARGET: ${{ steps.resolve.outputs.cross_target }}
TARGET_PLATFORM: ${{ inputs.platform }}
TARGET_ARCH: ${{ inputs.arch }}
TARGET_VARIANTS: ${{ inputs.variant }}
run: |
if [ "$CROSS_TARGET" = x86_64-apple-darwin ]; then
# Do not accept Homebrew's arm64 libopus through pkg-config;
# build audiopus_sys's bundled x64 archive.
export OPUS_NO_PKG_CONFIG=1
fi
bun run ci:build:native
- name: sccache stats
shell: bash
run: sccache --show-stats || true
- name: Save native addon(s) to in-cluster cache
if: inputs.skip_build != 'true' && steps.detect.outputs.on_infra == 'true'
shell: bash
env:
ARTIFACT_NAME: pi-natives-${{ inputs.platform }}-${{ inputs.libc && format('{0}-', inputs.libc) || '' }}${{ inputs.arch }}${{ inputs.variant && format('-{0}', inputs.variant) || '' }}-h${{ inputs.hash }}
run: bun scripts/ci-native-artifact-cache.ts save "${{ inputs.hash }}" "$ARTIFACT_NAME"
- name: Upload native addon(s)
if: inputs.skip_build != 'true'
uses: actions/upload-artifact@v4
with:
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
retention-days: 90
- name: Save target/ cache (omp-kata)
if: steps.detect.outputs.on_infra == 'true' && inputs.save_cache == 'true'
shell: bash
env:
TARGET_CACHE_KEY: native-${{ inputs.platform }}-${{ inputs.libc || 'default' }}-${{ inputs.arch }}-${{ inputs.variant || 'default' }}-${{ inputs.cache_scope }}
run: bun scripts/ci-target-cache.ts save "$TARGET_CACHE_KEY"
- name: Save rolling Rust target/ (GitHub-hosted)
if: steps.detect.outputs.on_infra == 'false' && inputs.save_cache == 'true' && steps.gha-target.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: target
key: native-target-v1-${{ runner.os }}-${{ runner.arch }}-${{ inputs.platform }}-${{ inputs.libc || 'default' }}-${{ inputs.arch }}-${{ inputs.variant || 'default' }}-${{ inputs.cache_scope }}-${{ hashFiles('rust-toolchain.toml') }}-h${{ inputs.hash }}