Build Runtime #202
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
| # 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::" |