Skip to content

os: add experimental reproducible Debian/mkosi backend #24

os: add experimental reproducible Debian/mkosi backend

os: add experimental reproducible Debian/mkosi backend #24

Workflow file for this run

# 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