Files
fds-os/docs/developer/reproducible-builds.md
T
2026-09-23 03:12:45 +08:00

10 KiB

Frozen inputs and offline rebuilds

Development reference and historical context. For current operating instructions, use the user manual. Acceptance applies only to the source and artifacts identified in each record.

Documentation index · Build host · Release signatures

M12 adds a local input snapshot, fresh restore, network-isolated build, and complete artifact comparison. Two fresh network-isolated builds passed with all 27 artifacts byte-identical. The M12 evidence page records the source/input identities, signed local release and actual archive round-trip. This result applies to the recorded inputs and documented build host; it does not claim equivalence across arbitrary host environments.

Normal builds still use Void's signed rolling repositories. A frozen build uses the selected package files and build environment from one explicit snapshot. It does not silently consult a newer repository if an input is missing.

Current builds keep the upstream reference in vendor/void-packages/ and use .host/void-packages/ for overlays and cached build state. New snapshots and restores use this separate layout. Restoring an older snapshot retains its historical layout and saved build scripts; its files and acceptance are not rewritten. See the workspace change for fresh validation of this migration.

What is frozen

tools/frozen-inputs create NEW_DIRECTORY records:

  • Project-owned source, documentation, configuration and package overlays, with a content identity independent of an FDS Git commit. This works before the repository's first commit too; it does not invent a commit identifier.
  • The exact pinned Void checkout in a Git bundle, preserving upstream files and the shallow-history boundary needed to read its commit timestamp after restore.
  • Cargo's locked vendored sources, the selected Rust toolchain and its static ARM target. Cargo credentials and user configuration are not copied.
  • The project-local Void binary build environment, static XBPS host tools and image-tool prefix. Temporary build trees, personal home directories and mount points are excluded.
  • Cached build dependencies, exact third-party packages selected by each of the three rootfs profiles, and kernel/DHCP/hello source archives. Local FDS packages are rebuilt from source instead of seeding the output repository.
  • Pinned Pi firmware, the VM kernel fixture, and the official EEPROM preview inputs. The preview is not a backup of a physical Pi.

lock.json records paths, kinds, modes, lengths, hashes and symlink targets. Added, missing or changed entries fail verification. The snapshot is several gigabytes. The completed offline diagnostic build occupied about 24 GB and its snapshot occupied 6.9 GB. Two restored builds need separate space; retained VM test images add further usage. Check available space before starting and keep headroom beyond these observations, which are not upper bounds. The workstation still supplies the documented Arch/Linux prerequisites, including Python, bubblewrap, Git, Make and compression tools. Their environment is recorded; this is not a replacement for a host OS.

Prepare the snapshot

Complete the first-build setup, then run the complete artifact build from the repository root:

make all

make all runs bootstrap and package construction, builds both normal SYSTEM profiles and independent recovery, then creates the four initramfs formats, production Pi boot volume, complete internal disk, signing tools and production EEPROM preview. It does not write physical media or change EEPROM settings. Use make packages to build only the FDS base packages. Builders perform their normal format, ABI and rootfs checks; the complete runtime acceptance suite remains separate.

Runtime test targets select their required profile explicitly: init, boot, console, cartridge, DATA, media and stress checks use CLI; desktop and power checks use development; recovery checks use recovery. They no longer depend on which rootfs happened to be built last. make rootfs-test intentionally checks the most recently exported archive, so use it immediately after a profile build when validating that specific export.

Fetch all locked crates, including platform-specific sources that Cargo's vendor operation requires even when they are not built for ARM:

cargo fetch --locked
./tools/frozen-inputs create "$PWD/out/inputs-0.1.0"
./tools/frozen-inputs verify "$PWD/out/inputs-0.1.0"

Use a new output directory. The command will not overwrite a previous snapshot. The snapshot operation itself is offline; missing selected packages or locked crate sources fail with an error. The subsequent fresh build establishes whether the complete dependency set was captured. Keep failed outputs for diagnosis and retry into a new directory. Run every build, snapshot and VM workflow sequentially: the normal checkout shares one Void masterdir. Snapshot creation locks rootfs/base-package assembly, but a separate standalone kernel or VM command must not be started concurrently.

Restore into two independent trees

If starting from the signed release's input archive, extract it as the ordinary build user with tar --extract --zstd --same-permissions --no-same-owner and the desired --file and new --directory paths. Mode preservation matters: default extraction can mask writable bits or remove sticky bits and cause the input lock to reject an otherwise intact archive. Verify the extracted snapshot before restoring it. The release's README provides the complete commands.

The destination parent directory must already exist, and the destination itself must not exist. Use paths without whitespace, as required by xbps-src:

./tools/frozen-inputs restore "$PWD/out/inputs-0.1.0" "$PWD/out/rebuild-a"
./tools/frozen-inputs restore "$PWD/out/inputs-0.1.0" "$PWD/out/rebuild-b"

Restoration verifies every locked entry first. Each new tree gets its own Void masterdir, source/download caches, Cargo directory and Rustup directory. It starts without an FDS target directory or generated FDS packages/images.

Run the builds sequentially and retain their full logs:

out/rebuild-a/tools/in-frozen-build make all >out/rebuild-a.log 2>&1
out/rebuild-b/tools/in-frozen-build make all >out/rebuild-b.log 2>&1
./tools/compare-builds out/rebuild-a out/rebuild-b \
  --report out/reproducibility-0.1.0.json

The wrapper changes to the restored project directory, uses its private Rust caches, makes the rest of the workstation read-only, and removes network access for the build and all child processes. Missing input therefore cannot be hidden by a download. FDS's xbps-src wrapper keeps dependency resolution enabled and excludes remote repositories; the custom repository filename includes local so the pinned xbps-src also retains it in its cross sysroot. Rootfs assembly uses the frozen per-profile repository. Setting FDS_OFFLINE=1 alone selects offline inputs but does not provide the wrapper's network isolation.

The comparison checks both rootfs tars, recovery rootfs, all installable images, all four initramfs formats, Pi kernel/DTB, every FDS package and the EEPROM configuration/rollback outputs. It reports each mismatch and exits nonzero if anything differs. Matching files alone do not attest how they were built; retain the isolated build logs alongside the report.

Assemble a local signed release

After both complete builds match, create or choose a signing key as described in Release signatures. Use the assembler in the first restored tree:

out/rebuild-a/tools/assemble-release \
  --inputs "$PWD/out/inputs-0.1.0" \
  --comparison-build "$PWD/out/rebuild-b" \
  --output-directory "$PWD/out/fds-os-0.1.0" \
  --key "$HOME/fds-signing/release-01.key"

The assembler verifies the snapshot again, checks that the first checkout still matches its source identity, compares the complete pair, and verifies recorded rootfs build inputs. It creates versioned image names, source and complete input archives, package inventories, comparison evidence and usage guides. It signs the manifest and immediately verifies every listed file. The private key is never copied into the release, and existing output directories are refused.

The result is a local signed release with physical validation marked deferred; it is not published or installed automatically. The supplied key defines its signing identity. Distribute its fingerprint through an independently trusted channel before someone relies on the bundled public key.

Determinism and troubleshooting

FDS uses the pinned Void commit's timestamp for package and image timestamps. Owned Rust binaries are stripped. make uses tools/cargo-build to map checkout, Rustup and Cargo registry source paths to stable /usr/src/fds paths; otherwise panic messages can embed the builder's absolute directory even in stripped binaries. The helper preserves the target's static linking flags, and its flags participate in Cargo's build cache. Direct cargo build remains useful for local development, but release construction uses the helper. This uses Rust's source-path remapping and Cargo's merged configuration arrays. The kernel has explicit build user/host/time, and filesystem UUIDs derive from content/configuration identities. Rootfs export restores target ownership and capabilities instead of preserving the builder's UID. It removes only the build-specific auxiliary linker cache and fetched XBPS repository indexes; the installed package database, keys and runtime linker cache remain available.

If verification reports changed snapshot entries, restore the original snapshot or create a new one from the intended inputs. Do not edit lock.json to hide a mismatch. If the two builds differ, use the comparison's named artifacts and each build's rootfs/package manifests to identify the first differing stage. Archive compression is not a substitute for comparing the uncompressed images. Physical performance and durability still require the actual Pi and storage.