feat(image): derive AVOCADO_OS_BUILD_ID from the assembled work tree - #211
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the OS/initramfs build-id derivation to be based on a deterministic content hash of the assembled work tree (folded alongside the existing NEVRA-based package hash), so that any rootfs-affecting change (permissions, overlay, post_install, etc.) reliably changes AVOCADO_OS_BUILD_ID and therefore triggers OTA/deploy behavior correctly.
Changes:
- Switch
permissions.users/permissions.groupstoBTreeMapto guarantee deterministic provisioning order (and stable auto-assigned UID/GIDs). - Add a shared
render_build_id_block(&BuildIdSpec)used by both rootfs and initramfs to deriveuuid5(ns, "$PKG_HASH:$TREE_HASH"). - Add/extend unit tests to enforce ordering and the build-id hashing/strip/append sequencing contract.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
src/utils/permissions.rs |
Rename helper to mapping_from_map, update to accept BTreeMap, and add a determinism/unit test for sorted provisioning order. |
src/utils/config.rs |
Change PermissionsConfig.users/groups from HashMap to BTreeMap and document why determinism is required. |
src/commands/runtime/build.rs |
Update call sites to use mapping_from_map after the config type change. |
src/commands/rootfs/image.rs |
Introduce render_build_id_block + tree-hash pruning and wire it into the rootfs image build script; add tests for structure and ordering. |
src/commands/initramfs/image.rs |
Reuse the shared build-id derivation block for initramfs and add a test asserting shared tree-hash usage and canonicalization. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The build id enumerated its inputs — the package NEVRA set, and in #208 the auth files — so anything off that list was invisible to the OTA gate and the same bug recurred per input (permissions, overlay, post_install). Replace the component list with a content hash of the assembled work tree, folded in beside the NEVRA hash as uuid5(ns, "$PKG_HASH:$TREE_HASH") through a shared render_build_id_block so rootfs and initramfs can't diverge on the id logic (ENG-2441). TREE_HASH covers exactly what the image carries — sorted path, type, mode, symlink target, file content — and excludes what mkfs.erofs normalizes out (mtime via -T, ownership via --all-root) or what is fs-dependent (directory sizes). It prunes var/lib/rpm (the rpmdb embeds install timestamps; PKG_HASH covers packages deterministically) plus the dnf caches/logs, and strips the self-referential AVOCADO_* os-release fields before hashing, so the id can't depend on a prior build's id or the possibly-unpinned runtime version. Prerequisite, in the same change: store PermissionsConfig users/groups in a BTreeMap (was HashMap) so provisioning order — and the auto-assigned UIDs — are deterministic; a random per-process tree order would otherwise churn the id every build. No config-format impact. Because the id formula changes, every existing image re-ids once on the first rebuild after this lands (a single OTA); steady state is churn-free. Byte-level reproducibility of a real SDK build must be verified before merge — see the determinism runbook.
009d77a to
416c263
Compare
|
Ran the pre-merge determinism runbook on a real SDK build box (qemux86-64, public 2026/edge feed, this branch's release binary):
Not run: item 2 (cross-machine — one box only), item 5 (on-device OTA). Item 3 (permissions reorder) is subsumed by item 1 under BTreeMap. Also, changes made while rebasing this over today's merges (#203/#204/#208 landed underneath it):
|
mobileoverlord
left a comment
There was a problem hiding this comment.
Design matches ENG-2441 on every hard requirement (shared render_build_id_block, hash before identity append with the AVOCADO_* strip test-pinned, BTreeMap prerequisite, mtime/ownership excluded to match mkfs.erofs). The PKG_HASH retention alongside TREE_HASH is the right deviation given the rpmdb prune. Determinism runbook items 1 and 4 pass on a real SDK build (evidence in comments). One migration note stands: every existing image re-ids once on first rebuild after this lands.
Fixes ENG-2441. Follow-up to ENG-2437 / #208.
Problem
The build id enumerated its inputs — the package NEVRA set, and in #208 the auth files — so anything off that list was invisible to the OTA gate, and the same bug recurred once per input (permissions, then overlay, then
post_install). The list only grows.Change
Replace the component list with a content hash of the assembled work tree, folded in beside the NEVRA hash:
via a shared
render_build_id_block(&BuildIdSpec)so rootfs and initramfs can't diverge on the correctness-critical id logic.TREE_HASHmoves iff the image bytes move → subsumes packages, permissions, overlay,post_install, and anything future.What
TREE_HASHhashes (and deliberately doesn't)Covers exactly what the image carries — sorted path, type, mode (
%m), symlink target (%l), and file content. Excludes:mkfs.erofsnormalizes these out (-T "${SOURCE_DATE_EPOCH:-0}",--all-root), so hashing them would churn the id on noise the image never carries.var/lib/rpm— the rpmdb sqlite embeds install timestamps (exactly why package identity is the NEVRAPKG_HASH, not these bytes). Pruned;PKG_HASHcovers packages deterministically, including a version-only bump with an identical file payload.var/cache,var/log— dnf caches/logs, build-varying.Ordering (a hard requirement, enforced by a test)
The hash is taken after permissions/
post_installand before the os-release identity append, and the derivation strips theAVOCADO_*fields from the identity files first. Taking it after the append would make the id depend on itself and on the possibly-unpinned runtime version (runtimes.<name>.versiondefaults to a per-builduuid_v4()fragment — see ENG-2444) → an OTA on every build.Prerequisite (same PR):
HashMap→BTreeMapPermissionsConfig.users/groupswere aHashMap, whose per-process iteration order reshuffled the/etc/passwdappends and the auto-assigned UID/GIDs every build. Withid = f(tree)that would churn the id every build, so this switches toBTreeMap(key-sorted, deterministic). No config-format impact — aBTreeMapdeserializes from a YAML mapping identically.Migration note
The id formula changes, so every existing image re-ids once on the first rebuild after this lands (a single OS OTA even if nothing else changed). Steady state is churn-free thereafter.
Tests
Unit tests: id =
$PKG_HASH:$TREE_HASH; prunes present; no mtime/owner directives; strip → tree-hash → derive → append ordering; both images sharerender_build_id_block; BTreeMap provisions in sorted order. Full lib suite green; fmt/clippy clean (only the pre-existingOption::zipwarning).Empirically validated the exact GNU pipeline in a Linux container: determinism across two builds with differing mtimes/rpmdb bytes ✓, mtime ignored ✓, owner ignored ✓, rpmdb-churn pruned ✓, content/mode/symlink changes detected ✓.
Unit tests prove the script structure; only building twice proves the tree is reproducible. Draft until these pass:
clean && prune && install -f && buildtwice on the same machine → sameAVOCADO_OS_BUILD_ID. If not, diff the two work trees (metadata viafind … -printf '%y %m %P\t%l\n' | sort; content viafind -type f | sort | xargs sha256sum). Prime suspects:usr/etc/ld.so.cache, systemd preset*.wants/*symlinks,usr/lib/opkg/alternatives, depmodmodules.*. Fix at source or add toBUILD_ID_TREE_PRUNES.users:/groups:in YAML → id unchanged (BTreeMap check).overlay/file, editpost_install, add/remove arootfs.packagesentry} → a different id.OS already at target version … skipping; permissions-only change → device downloads + applies the OS bundle.I could not run 1/2 here (no SDK container/target).
Sequencing
Depends on nothing, but per the ticket: land #208, then ENG-2440 (stamp correctness), then this. Also relates to ENG-2444 (unpinned runtime version).