os: add experimental reproducible Debian/mkosi backend #24
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
| # SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network> | |
| # | |
| # SPDX-License-Identifier: Apache-2.0 | |
| name: Verify mkosi reproducibility | |
| on: | |
| workflow_dispatch: | |
| # Bootstrap this new workflow once before it exists on the default branch. | |
| # Remove this trigger after the hosted-runner baseline check completes. | |
| pull_request: | |
| paths: | |
| - '.github/workflows/mkosi-build.yml' | |
| concurrency: | |
| group: mkosi-build-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| # These values were produced by two cache-disabled local builds. The job | |
| # checks out this exact source revision so the embedded Git revision is also | |
| # identical. Refresh all four values together when updating the baseline. | |
| BASELINE_REVISION: a7e7059dce4f710af3d2cae66ff05aa0435772e8 | |
| SOURCE_DATE_EPOCH: '1784835822' | |
| BARE_SHA256: a020d9b42deeab621af5122d298a2338b15784278d7d93ad95937cd6d0c974bc | |
| UKI_SHA256: bc46dd619c706fc99e033cfabf885be99ae94c8f8da451ae278dbbd57576276f | |
| jobs: | |
| cross-host: | |
| name: Compare with local mkosi baseline | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 360 | |
| permissions: | |
| contents: read | |
| steps: | |
| # GitHub-hosted runners have enough CPU and memory for this build, but | |
| # their preinstalled SDKs consume most of the available disk. Remove only | |
| # those unused SDKs before checkout; the OS build itself remains hermetic. | |
| - name: Reclaim runner disk space | |
| run: | | |
| sudo rm -rf \ | |
| /opt/ghc \ | |
| /opt/hostedtoolcache/CodeQL \ | |
| /usr/local/.ghcup \ | |
| /usr/local/lib/android \ | |
| /usr/local/share/boost \ | |
| /usr/share/dotnet | |
| df -h / | |
| - name: Checkout locally verified revision | |
| uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ env.BASELINE_REVISION }} | |
| fetch-depth: 0 | |
| - name: Set up Python for mkosi | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.13' | |
| - name: Install host image assembly tools | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install --yes --no-install-recommends \ | |
| build-essential ca-certificates cmake cpio cryptsetup-bin curl \ | |
| debian-archive-keyring dosfstools gdisk git jq libssl-dev mtools \ | |
| pax-utils pkg-config python3-venv squashfs-tools systemd-ukify \ | |
| xz-utils zstd | |
| - name: Install pinned mkosi and Rust/Go tools | |
| run: | | |
| python3 -m venv "$RUNNER_TEMP/mkosi-venv" | |
| "$RUNNER_TEMP/mkosi-venv/bin/pip" install --disable-pip-version-check \ | |
| 'mkosi @ git+https://github.com/systemd/mkosi.git@84af20892b61c8e177e391f997ded8b4cb5514f2' | |
| echo "$RUNNER_TEMP/mkosi-venv/bin" >> "$GITHUB_PATH" | |
| sudo ./os/mkosi/scripts/install-toolchains.sh \ | |
| "$RUNNER_TEMP/toolchain-downloads" | |
| echo '/opt/dstack-toolchains/rust/bin' >> "$GITHUB_PATH" | |
| echo '/opt/dstack-toolchains/go/bin' >> "$GITHUB_PATH" | |
| - name: Build pinned host measurement tools | |
| env: | |
| CARGO_HOME: ${{ runner.temp }}/cargo-home | |
| CARGO_TARGET_DIR: ${{ runner.temp }}/cargo-target | |
| run: | | |
| cargo install --locked \ | |
| --git https://github.com/aws/NitroTPM-Tools \ | |
| --rev d76d6eeebd4169b00a3c3af9858852d48f40e748 \ | |
| --root "$RUNNER_TEMP/host-tools" nitro-tpm-pcr-compute | |
| cargo build --locked --release --manifest-path dstack/Cargo.toml \ | |
| -p dstack-mr | |
| echo "$RUNNER_TEMP/host-tools/bin" >> "$GITHUB_PATH" | |
| echo "DSTACK_MR_BIN=$RUNNER_TEMP/cargo-target/release/dstack-mr" \ | |
| >> "$GITHUB_ENV" | |
| - name: Build without component cache | |
| env: | |
| # ubuntu-latest currently provides four vCPUs and 16 GiB of RAM. | |
| # Do not oversubscribe either the CPU or compiler memory. | |
| JOBS: '4' | |
| run: | | |
| test -z "${DSTACK_DEV_CACHE_DIR:-}" | |
| # GitHub-hosted Ubuntu runners restrict unprivileged user namespaces. | |
| # Run mkosi as root instead of weakening the runner's AppArmor policy. | |
| # Keep setup-python out of the build PATH because Ubuntu's lddtree | |
| # expects the distro Python and its python3-pyelftools module. | |
| ci_bin="$RUNNER_TEMP/mkosi-ci-bin" | |
| mkdir -p "$ci_bin" | |
| ln -s "$(command -v mkosi)" "$ci_bin/mkosi" | |
| ln -s "$(command -v nitro-tpm-pcr-compute)" \ | |
| "$ci_bin/nitro-tpm-pcr-compute" | |
| sudo --set-home env \ | |
| "PATH=$ci_bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \ | |
| "SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH" \ | |
| "DSTACK_MR_BIN=$DSTACK_MR_BIN" \ | |
| "JOBS=$JOBS" \ | |
| ./os/mkosi/build.sh image "$RUNNER_TEMP/mkosi-cross-host" | |
| sudo chown -R "$USER:$USER" "$RUNNER_TEMP/mkosi-cross-host/out" | |
| - name: Record canonical rootfs tree | |
| if: always() | |
| run: | | |
| root="$RUNNER_TEMP/mkosi-cross-host/work-prod/rootfs" | |
| test -d "$root" || exit 0 | |
| sudo python3 - "$root" > "$RUNNER_TEMP/rootfs.manifest" <<'PY' | |
| import hashlib | |
| import os | |
| import stat | |
| import sys | |
| root = sys.argv[1] | |
| hardlinks = {} | |
| for base, dirs, files in os.walk(root, topdown=True, followlinks=False): | |
| dirs.sort() | |
| files.sort() | |
| for name in dirs + files: | |
| path = os.path.join(base, name) | |
| rel = os.path.relpath(path, root) | |
| info = os.lstat(path) | |
| mode = stat.S_IMODE(info.st_mode) | |
| if stat.S_ISREG(info.st_mode): | |
| digest = hashlib.sha256() | |
| with open(path, "rb") as stream: | |
| for block in iter(lambda: stream.read(1024 * 1024), b""): | |
| digest.update(block) | |
| key = (info.st_dev, info.st_ino) | |
| first = hardlinks.setdefault(key, rel) | |
| detail = f"sha256={digest.hexdigest()} hardlink={first}" | |
| elif stat.S_ISLNK(info.st_mode): | |
| detail = f"target={os.readlink(path)}" | |
| elif stat.S_ISCHR(info.st_mode) or stat.S_ISBLK(info.st_mode): | |
| detail = f"device={os.major(info.st_rdev)}:{os.minor(info.st_rdev)}" | |
| else: | |
| detail = "-" | |
| print(f"{rel}\t{stat.S_IFMT(info.st_mode):o}\t{mode:o}\t" | |
| f"{info.st_uid}:{info.st_gid}\t{info.st_size}\t{detail}") | |
| PY | |
| - name: Compare with local byte-for-byte baseline | |
| run: | | |
| cd "$RUNNER_TEMP/mkosi-cross-host/out/prod" | |
| tar -xOf dstack-0.6.0.tar.gz dstack-0.6.0/sha256sum.txt | |
| tar -xOf dstack-0.6.0.tar.gz dstack-0.6.0/metadata.json | |
| sha256sum dstack-0.6.0.tar.gz dstack-0.6.0-uki.tar.gz \ | |
| | tee cross-host.sha256 | |
| printf '%s %s\n' "$BARE_SHA256" dstack-0.6.0.tar.gz \ | |
| | sha256sum --check --strict | |
| printf '%s %s\n' "$UKI_SHA256" dstack-0.6.0-uki.tar.gz \ | |
| | sha256sum --check --strict | |
| - name: Upload verification record | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: mkosi-cross-host-sha256 | |
| path: | | |
| ${{ runner.temp }}/mkosi-cross-host/out/prod/cross-host.sha256 | |
| ${{ runner.temp }}/rootfs.manifest | |
| retention-days: 30 |