name: Release on: push: tags: ['v*'] env: CARGO_TERM_COLOR: always jobs: # ========================================================================== # Run CI checks first # ========================================================================== ci: uses: ./.github/workflows/ci.yml # ========================================================================== # Build agent binaries (both architectures) # ========================================================================== build-agent: name: Build agent (${{ matrix.arch }}) needs: ci runs-on: ubuntu-latest strategy: matrix: include: - arch: x86_64 target: x86_64-unknown-linux-musl linker_pkg: "" linker_env: "" - arch: aarch64 target: aarch64-unknown-linux-musl linker_pkg: gcc-aarch64-linux-gnu linker_env: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-gnu-gcc steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - name: Cache cargo uses: actions/cache@v5 with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ key: agent-${{ matrix.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }} restore-keys: agent-${{ matrix.arch }}-cargo- - name: Install cross-compilation tools if: matrix.linker_pkg != '' run: | sudo apt-get update sudo apt-get install -y musl-tools ${{ matrix.linker_pkg }} - name: Install musl-tools if: matrix.linker_pkg == '' run: | sudo apt-get update sudo apt-get install -y musl-tools - name: Build agent run: | ${{ matrix.linker_env }} \ cargo build --profile release-small -p smolvm-agent --target ${{ matrix.target }} - name: Verify static binary run: file target/${{ matrix.target }}/release-small/smolvm-agent - uses: actions/upload-artifact@v7 with: name: agent-${{ matrix.arch }} path: target/${{ matrix.target }}/release-small/smolvm-agent # ========================================================================== # Build agent rootfs (both architectures) # ========================================================================== build-rootfs: name: Build rootfs (${{ matrix.arch }}) needs: build-agent runs-on: ubuntu-latest strategy: matrix: include: - arch: aarch64 - arch: x86_64 steps: - uses: actions/checkout@v6 - name: Download agent binary uses: actions/download-artifact@v8 with: name: agent-${{ matrix.arch }} path: /tmp/agent/ - name: Build rootfs run: | chmod +x /tmp/agent/smolvm-agent AGENT_BINARY=/tmp/agent/smolvm-agent \ ./scripts/build-agent-rootfs.sh --arch ${{ matrix.arch }} --no-build-agent - name: Package rootfs as tarball run: | # Tar preserves ownership/permissions without needing read access # to restricted dirs like /var/run/chrony (owned by chrony:chrony 700) sudo tar -cf target/agent-rootfs.tar -C target/agent-rootfs . - uses: actions/upload-artifact@v7 with: name: rootfs-${{ matrix.arch }} path: target/agent-rootfs.tar # ========================================================================== # Build distribution tarballs (per platform) # ========================================================================== build-dist: name: Build dist (${{ matrix.platform }}) needs: [build-agent, build-rootfs] runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: - platform: darwin-arm64 runner: macos-14 rootfs_arch: aarch64 agent_arch: aarch64 lib_dir: lib # Linux dist binaries build on 22.04 (glibc 2.35), NOT ubuntu-latest # (24.04 = glibc 2.39). Symbol versioning is backward-compatible only, # so the build host's glibc is the MINIMUM a user needs at runtime — # a 2.39-built binary dies on 22.04 with "GLIBC_2.39 not found". 2.35 # covers Ubuntu 22.04+ / Debian 12+ / RHEL 9+. - platform: linux-x86_64 runner: ubuntu-22.04 rootfs_arch: x86_64 agent_arch: x86_64 lib_dir: lib/linux-x86_64 - platform: linux-arm64 runner: ubuntu-22.04-arm rootfs_arch: aarch64 agent_arch: aarch64 lib_dir: lib/linux-aarch64 steps: - uses: actions/checkout@v6 with: lfs: true submodules: ${{ startsWith(matrix.platform, 'linux-') && 'recursive' || 'false' }} - name: Pull LFS files run: git lfs pull - uses: dtolnay/rust-toolchain@stable - name: Cache cargo uses: actions/cache@v5 with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ # Include lib dir hash so libkrun updates bust the cache key: dist-${{ matrix.platform }}-cargo-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles(format('{0}/*', matrix.lib_dir)) }} restore-keys: dist-${{ matrix.platform }}-cargo- # Force relink if cached binary was linked against a different libkrun - name: Clean stale build artifacts run: rm -f target/release/smolvm target/release/deps/smolvm-* - name: Install build dependencies (Linux) if: runner.os == 'Linux' run: | sudo apt-get update # patchelf: build-dist.sh strips libkrun's hard libvirglrenderer NEEDED # so non-GPU Linux hosts can dlopen it (GPU stays optional at runtime). # Without patchelf the strip is silently skipped and `smolvm run` fails # with "libvirglrenderer.so.1: cannot open shared object file". sudo apt-get install -y build-essential libssl-dev pkg-config e2fsprogs patchelf - name: Install build dependencies (macOS) if: runner.os == 'macOS' run: brew install e2fsprogs - name: Verify LFS libraries run: | echo "Library directory: ${{ matrix.lib_dir }}" ls -la ${{ matrix.lib_dir }}/ file ${{ matrix.lib_dir }}/* - name: Download rootfs uses: actions/download-artifact@v8 with: name: rootfs-${{ matrix.rootfs_arch }} path: /tmp/rootfs-dl/ - name: Extract rootfs tarball run: | mkdir -p target/agent-rootfs sudo tar -xf /tmp/rootfs-dl/agent-rootfs.tar -C target/agent-rootfs - name: Download agent binary uses: actions/download-artifact@v8 with: name: agent-${{ matrix.agent_arch }} path: /tmp/agent/ - name: Place agent binary for build-dist.sh run: | mkdir -p target/release-small cp /tmp/agent/smolvm-agent target/release-small/smolvm-agent chmod +x target/release-small/smolvm-agent # Import Developer ID certificate if available (macOS only). # Set secrets APPLE_CERTIFICATE_P12 (base64-encoded .p12) and # APPLE_CERTIFICATE_PASSWORD in your GitHub repo settings. - name: Import signing certificate (macOS) if: runner.os == 'macOS' && env.APPLE_CERTIFICATE_P12 != '' env: APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} run: | echo "$APPLE_CERTIFICATE_P12" | base64 --decode > /tmp/certificate.p12 KEYCHAIN_PASSWORD="$(openssl rand -hex 16)" security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security default-keychain -s build.keychain security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security import /tmp/certificate.p12 -k build.keychain \ -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple: \ -s -k "$KEYCHAIN_PASSWORD" build.keychain rm /tmp/certificate.p12 # Check out the unified `smol` CLI (a separate repo) into ./smol so # build-dist.sh builds and bundles it alongside the engine — one tarball # serves both `smol` and `smolvm`. Requires repo secret SMOL_REPO_TOKEN # with read access to smol-machines/smol. If unset, the step is skipped and # an engine-only tarball is produced. Ref is left at the smol default # branch (its versioning is independent of the engine tag); pin a ref here # if you want reproducible smol-CLI releases. - name: Check out smol CLI source if: env.SMOL_REPO_TOKEN != '' uses: actions/checkout@v6 with: repository: smol-machines/smol path: smol token: ${{ secrets.SMOL_REPO_TOKEN }} env: SMOL_REPO_TOKEN: ${{ secrets.SMOL_REPO_TOKEN }} # No separate guest-init build: libkrun 2.0.0 embeds the guest init # (krun-init) directly in libkrun.so (INIT_BLOB is on by default), so the # old per-arch `cc init/init.c` step is gone — init.c no longer exists in # the 2.0.0 submodule, and a VM boots from the embedded init with no # external init.krun (verified). build-dist.sh no longer bundles init.krun. - name: Build distribution tarball run: ./scripts/build-dist.sh --skip-agent-build env: LIB_DIR: ${{ matrix.lib_dir }} CODESIGN_IDENTITY: ${{ secrets.APPLE_CERTIFICATE_P12 != '' && secrets.CODESIGN_IDENTITY || '-' }} # Notarize the macOS binary if Developer ID signing was used. # Requires secrets: APPLE_ID, APPLE_TEAM_ID, APPLE_APP_PASSWORD. - name: Notarize (macOS) if: runner.os == 'macOS' && env.APPLE_CERTIFICATE_P12 != '' env: APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} run: | TARBALL=$(ls dist/smolvm-*.tar.gz) xcrun notarytool submit "$TARBALL" \ --apple-id "$APPLE_ID" \ --team-id "$APPLE_TEAM_ID" \ --password "$APPLE_APP_PASSWORD" \ --wait - name: Verify bundled libraries match source run: | VERSION="$(grep '^version' Cargo.toml | head -1 | cut -d'"' -f2)" DIST_LIB="dist/smolvm-${VERSION}-${{ matrix.platform }}/lib" echo "Source libraries:" ls -la ${{ matrix.lib_dir }}/ echo "Bundled libraries:" ls -la "$DIST_LIB"/ # Verify sizes match for all bundled libraries (catches stale LFS or copy issues). # Covers both required libs (libkrun, libkrunfw) and optional GPU libs. for lib in "$DIST_LIB"/lib*; do [[ -L "$lib" ]] && continue # skip symlinks name=$(basename "$lib") src="${{ matrix.lib_dir }}/$name" if [[ -f "$src" ]]; then src_size=$(wc -c < "$src") dst_size=$(wc -c < "$lib") if [[ "$src_size" != "$dst_size" ]]; then echo "ERROR: $name size mismatch: source=$src_size bundled=$dst_size" exit 1 fi echo "OK: $name ($dst_size bytes)" fi done - name: List distribution contents run: | VERSION="$(grep '^version' Cargo.toml | head -1 | cut -d'"' -f2)" ls -la "dist/smolvm-${VERSION}-"*/ - uses: actions/upload-artifact@v7 with: name: dist-${{ matrix.platform }} path: dist/smolvm-*.tar.gz # ========================================================================== # Build Windows distribution (cross-compiled; zipped) # ========================================================================== build-dist-windows: name: Build dist (windows-x86_64) needs: [build-agent, build-rootfs] runs-on: ubuntu-latest env: CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc steps: - uses: actions/checkout@v6 with: lfs: true - name: Pull LFS files run: git lfs pull - uses: dtolnay/rust-toolchain@stable with: targets: x86_64-pc-windows-gnu - name: Install build dependencies run: | sudo apt-get update # mingw: cross-link smolvm.exe; clang/libclang: bindgen in build deps # (matches the CI windows job); e2fsprogs: pre-format disk templates # (Windows has no host mkfs.ext4); zip: package the dist. sudo apt-get install -y gcc-mingw-w64-x86-64 clang libclang-dev e2fsprogs zip - name: Verify Windows libraries run: | ls -la lib/windows-x86_64/ file lib/windows-x86_64/*.dll - name: Download rootfs (x86_64) uses: actions/download-artifact@v8 with: name: rootfs-x86_64 path: /tmp/rootfs-dl/ - name: Extract rootfs run: | mkdir -p target/agent-rootfs sudo tar -xf /tmp/rootfs-dl/agent-rootfs.tar -C target/agent-rootfs - name: Download agent binary (x86_64) uses: actions/download-artifact@v8 with: name: agent-x86_64 path: /tmp/agent/ # The rootfs artifact ships agent-less; inject the freshly built agent and # repoint /sbin/init at it (same as the Unix build-dist.sh path). - name: Inject agent into rootfs run: | sudo cp /tmp/agent/smolvm-agent target/agent-rootfs/usr/local/bin/smolvm-agent sudo chmod +x target/agent-rootfs/usr/local/bin/smolvm-agent sudo ln -sf /usr/local/bin/smolvm-agent target/agent-rootfs/sbin/init - name: Build Windows distribution run: ./scripts/build-dist-windows.sh env: LIB_DIR: lib/windows-x86_64 - uses: actions/upload-artifact@v7 with: name: dist-windows-x86_64 path: dist/smolvm-*-windows-x86_64.zip # ========================================================================== # Create GitHub Release # ========================================================================== release: name: Create Release needs: [build-dist, build-dist-windows] runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v6 - name: Download all dist artifacts uses: actions/download-artifact@v8 with: pattern: dist-* path: dist/ merge-multiple: true - name: List release artifacts run: ls -la dist/ - name: Generate checksums run: | cd dist sha256sum *.tar.gz *.zip > checksums.sha256 cat checksums.sha256 - name: Create GitHub Release env: GH_TOKEN: ${{ github.token }} run: | gh release create "${{ github.ref_name }}" \ --title "smolvm ${{ github.ref_name }}" \ --generate-notes \ dist/*.tar.gz \ dist/*.zip \ dist/checksums.sha256 # Ping the Homebrew tap to bump its formula to this release immediately, # instead of waiting for its scheduled poll. Best-effort: if the token # secret isn't configured, this no-ops (the tap's cron picks the release up # within a few hours anyway) and never fails the release. The token is a # fine-grained PAT with Contents:write on smol-machines/homebrew-tap. - name: Notify Homebrew tap of the new release env: GH_TOKEN: ${{ secrets.HOMEBREW_TAP_DISPATCH_TOKEN }} TAG: ${{ github.ref_name }} run: | if [ -z "${GH_TOKEN:-}" ]; then echo "HOMEBREW_TAP_DISPATCH_TOKEN not set — skipping instant bump (the tap polls on a schedule)." exit 0 fi version="${TAG#v}" gh api --method POST repos/smol-machines/homebrew-tap/dispatches \ -f event_type=smolvm-release \ -f "client_payload[version]=$version" echo "Dispatched smolvm-release ($version) to the Homebrew tap." # Build + publish the Arch/pacman packages for this release. Invoked directly # (not via the `release: published` event) because that event never fires for a # release created with the default GITHUB_TOKEN — the same GitHub restriction # the Homebrew step above works around. Runs after the release exists so the # PKGBUILD can download its release binaries. publish-pacman: name: Publish pacman repo needs: release permissions: contents: write uses: ./.github/workflows/pacman-repo.yml with: tag: ${{ github.ref_name }}