Skip to content

Build Runtime

Build Runtime #202

Workflow file for this run

# Build BoxLite core runtime, upload artifacts, and publish to crates.io.
#
# This workflow builds the Rust runtime and populates the shared cache.
# On push to main: chains after Warm Caches workflow for sccache hits.
# On release: builds, uploads runtime tarballs to GitHub Release, and publishes
# Rust crates to crates.io (requires CARGO_REGISTRY_TOKEN secret).
#
# Platform-specific considerations:
# - Linux: Uses manylinux_2_28 container for glibc compatibility
# - Guest binary built on host first (Ubuntu has musl-tools, manylinux doesn't)
# - Shim/libkrun built in container (needs glibc 2.28)
# - macOS: Builds directly on runner (ARM64 only)
#
# Note: SDK workflows (build-wheels, build-node) are self-contained
# and trigger independently on release.
name: Build Runtime
on:
workflow_run:
workflows: ["Warm Caches"]
types: [completed]
branches: [main]
release:
types: [published]
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: '0'
SKIP_INSTALL_NODEJS: '1'
jobs:
config:
uses: ./.github/workflows/config.yml
build:
name: Build Runtime (${{ matrix.target }})
needs: config
if: >-
github.event_name != 'workflow_run' ||
github.event.workflow_run.conclusion == 'success'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.config.outputs.platforms) }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ needs.config.outputs.rust-toolchain }}
# sccache caches individual compilation units via GHA cache API.
# Works on host and inside Docker containers.
# Cache is pre-warmed by the Warm Caches workflow on push to main.
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
continue-on-error: true
- name: Export GHA cache env vars
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
core.exportVariable('ACTIONS_CACHE_SERVICE_V2', process.env.ACTIONS_CACHE_SERVICE_V2 || '');
# Build guest on host BEFORE manylinux container because:
# - manylinux (AlmaLinux/RHEL) doesn't have musl packages in repos
# - Building musl-cross-make from source takes ~15 minutes
# - Ubuntu host has musl-tools via apt (~30 seconds)
# - Guest is static musl binary, doesn't need manylinux glibc compatibility
# - Shim/libkrun must be built IN container (needs glibc 2.28 for manylinux)
- name: Build guest binary (Linux only)
if: runner.os == 'Linux'
run: |
make setup:build guest
# Cache only the final binary (11MB), not entire target dir (2.3GB)
# Guest is not rebuilt in container (SKIP_GUEST_BUILD=1), so no incremental compilation needed
GUEST_TARGET=$(scripts/util.sh --target)
mkdir -p ".cache/$GUEST_TARGET/release"
cp "target/$GUEST_TARGET/release/boxlite-guest" ".cache/$GUEST_TARGET/release/"
# Clean up to save disk space (sccache stores compilation cache separately)
rm -rf target ~/.rustup ~/.cargo
mkdir -p target
- name: Build runtime (Linux)
if: runner.os == 'Linux'
run: |
cat > "$RUNNER_TEMP/build.sh" << 'CONTAINER_SCRIPT'
set -ex
git config --global --add safe.directory /work
# sccache fallback: if not available, disable wrapper for normal compilation
if [ -n "${RUSTC_WRAPPER:-}" ] && ! command -v "$RUSTC_WRAPPER" &>/dev/null; then
echo "::warning::sccache not available in container, falling back to normal compilation"
unset RUSTC_WRAPPER
fi
GUEST_TARGET=$(scripts/util.sh --target)
if [ -d ".cache/$GUEST_TARGET" ]; then
echo "Restoring guest from .cache/$GUEST_TARGET"
mkdir -p target
cp -a ".cache/$GUEST_TARGET" "target/$GUEST_TARGET"
fi
export SKIP_GUEST_BUILD=1
export PATH="/usr/local/go/bin:$CARGO_HOME/bin:$PATH"
make setup:build runtime
# Build CLI binary (reuses compiled boxlite crate artifacts)
cargo build -p boxlite-cli --release
command -v sccache &>/dev/null && sccache --show-stats || true
CONTAINER_SCRIPT
# Conditionally mount sccache binary and pass env vars into Docker.
# If sccache-action failed or binary is missing, build proceeds without caching.
SCCACHE_DOCKER_ARGS=""
if command -v sccache &>/dev/null; then
SCCACHE_DOCKER_ARGS="-v $(which sccache):/usr/local/bin/sccache:ro -e SCCACHE_GHA_ENABLED=true -e RUSTC_WRAPPER=sccache -e ACTIONS_CACHE_SERVICE_V2 -e ACTIONS_RESULTS_URL -e ACTIONS_RUNTIME_TOKEN"
fi
docker run --rm \
-v ${{ github.workspace }}:/work \
-v "$RUNNER_TEMP/build.sh:/tmp/build.sh:ro" \
$SCCACHE_DOCKER_ARGS \
-w /work \
-e CARGO_HOME=/work/.cargo-manylinux \
quay.io/pypa/manylinux_2_28_${{ contains(matrix.target, 'arm64') && 'aarch64' || 'x86_64' }} \
bash /tmp/build.sh
- name: Build runtime (macOS)
if: runner.os == 'macOS'
run: |
make setup:build runtime
cargo build -p boxlite-cli --release
command -v sccache &>/dev/null && sccache --show-stats || true
- name: Package runtime
run: |
VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
RUNTIME_DIR=$(ls -dt target/release/build/boxlite-*/out/runtime 2>/dev/null | head -1)
[ -n "$RUNTIME_DIR" ] || { echo "ERROR: Runtime directory not found under target/"; exit 1; }
cp -a "$RUNTIME_DIR" boxlite-runtime
tar czf "boxlite-runtime-v${VERSION}-${{ matrix.target }}.tar.gz" boxlite-runtime/
rm -rf boxlite-runtime
- name: Upload runtime artifact
uses: actions/upload-artifact@v4
with:
name: runtime-${{ matrix.target }}
path: boxlite-runtime-v*-${{ matrix.target }}.tar.gz
retention-days: 7
- name: Package CLI binary
run: |
VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
CLI_BIN="target/release/boxlite"
[ -f "$CLI_BIN" ] || { echo "ERROR: CLI binary not found at $CLI_BIN"; exit 1; }
tar czf "boxlite-cli-v${VERSION}-${{ matrix.rust_target }}.tar.gz" -C target/release boxlite
- name: Upload CLI artifact
uses: actions/upload-artifact@v4
with:
name: cli-${{ matrix.target }}
path: boxlite-cli-v*-${{ matrix.rust_target }}.tar.gz
retention-days: 7
upload_to_release:
name: Upload to GitHub Release
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'release'
permissions:
contents: write
steps:
- name: Download all build artifacts
uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Upload to release
uses: softprops/action-gh-release@v2
with:
files: |
dist/boxlite-runtime-*.tar.gz
dist/boxlite-cli-*.tar.gz
fail_on_unmatched_files: true
# Publish Rust crates to crates.io after runtime tarballs are on GitHub Release.
# Runtime tarballs must exist first because crates.io consumers download them
# via boxlite/build.rs (auto_detect_registry -> download_prebuilt_runtime).
#
# Publish order matters: leaf crates (Tier 1) before boxlite (Tier 2).
# BOXLITE_DEPS_STUB=1 skips native builds during cargo publish verification.
publish_crates:
name: Publish to crates.io
needs: upload_to_release
runs-on: ubuntu-latest
if: github.event_name == 'release'
env:
BOXLITE_DEPS_STUB: "1"
steps:
- uses: actions/checkout@v4
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
- name: Install protoc
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
- name: Publish leaf crates (Tier 1)
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
set -euo pipefail
publish_crate() {
local crate="$1"
shift
local extra_args="$*"
echo "::group::Publishing $crate"
if cargo publish -p "$crate" $extra_args 2>&1 | tee /tmp/publish-"$crate".log; then
echo "$crate published successfully"
else
if grep -q "already uploaded" /tmp/publish-"$crate".log; then
echo "$crate already on crates.io, skipping"
else
echo "::error::Failed to publish $crate"
exit 1
fi
fi
echo "::endgroup::"
}
# Pure-Rust crate verifies normally
publish_crate boxlite-shared
# -sys crates skip verification: vendor sources excluded from package,
# real build already succeeded in the build job above
publish_crate libkrun-sys --no-verify
publish_crate e2fsprogs-sys --no-verify
publish_crate libgvproxy-sys --no-verify
publish_crate bubblewrap-sys --no-verify
- name: Wait for crates.io indexing
run: sleep 60
# boxlite uses --no-verify because the boxlite-shim binary links libkrun,
# which isn't available in stub mode. The real build already succeeded in the
# build job, so skipping verification is safe.
- name: Publish boxlite (Tier 2)
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
set -euo pipefail
echo "::group::Publishing boxlite"
if cargo publish -p boxlite --no-verify 2>&1 | tee /tmp/publish-boxlite.log; then
echo "boxlite published successfully"
else
if grep -q "already uploaded" /tmp/publish-boxlite.log; then
echo "boxlite already on crates.io, skipping"
else
echo "::error::Failed to publish boxlite"
exit 1
fi
fi
echo "::endgroup::"
- name: Wait for boxlite indexing
run: sleep 60
# boxlite-cli uses --no-verify because verification would download the
# prebuilt runtime from GitHub Releases (slow and unnecessary since the
# real build already succeeded in the build job above).
- name: Publish boxlite-cli (Tier 3)
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
set -euo pipefail
echo "::group::Publishing boxlite-cli"
if cargo publish -p boxlite-cli --no-verify 2>&1 | tee /tmp/publish-boxlite-cli.log; then
echo "boxlite-cli published successfully"
else
if grep -q "already uploaded" /tmp/publish-boxlite-cli.log; then
echo "boxlite-cli already on crates.io, skipping"
else
echo "::error::Failed to publish boxlite-cli"
exit 1
fi
fi
echo "::endgroup::"