Stop reinstalling rootfs and initramfs on every sdk install - #185
Conversation
There was a problem hiding this comment.
Pull request overview
This PR optimizes avocado sdk install by preventing unnecessary rootfs/initramfs reinstalls when inputs haven’t changed, and by fixing erroneous “packages removed” detection that caused sysroot wipes and ineffective lockfile pinning.
Changes:
- Fix sysroot removal detection to compare against the effective package set (config packages plus auto-appended kernel/module packages), returning concrete removed names.
- Add batched stamp reading (
read_stamps_batch/StampBatch) and use sysroot install stamps to short-circuit rootfs/initramfs installs when current. - Strengthen sysroot install stamp hashing to include effective packages, SDK feed identity/resolver flags, and a digest of lockfile pins; update
cleanto invalidate install stamps.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/stamps.rs | Adds sysroot stamp input plumbing (effective package hashing + lock pin digest) and extends hash coverage; updates/expands unit tests. |
| src/utils/prerequisites.rs | Introduces StampBatch + read_stamps_batch to run one container invocation for multiple stamp reads (local or --runs-on). |
| src/commands/sdk/package.rs | Switches SDK prerequisite stamp checking to read_stamps_batch. |
| src/commands/sdk/install.rs | Prefetches rootfs/initramfs install stamps in one batched read and passes them into sysroot install to enable skipping. |
| src/commands/sdk/clean.rs | Switches SDK prerequisite stamp checking to read_stamps_batch. |
| src/commands/rootfs/install.rs | Implements sysroot install skipping via prefetched stamp + stronger inputs; fixes removal detection; refactors clean; adjusts lock saving responsibility. |
| src/commands/rootfs/clean.rs | Ensures clean removes the sysroot’s install stamp directory as well as the sysroot. |
| src/commands/initramfs/install.rs | Adds prefetched install-stamp read and moves lock saving responsibility to the command. |
| src/commands/ext/clean.rs | Switches SDK prerequisite stamp checking to read_stamps_batch. |
| src/commands/ext/checkout.rs | Switches stamp checking to read_stamps_batch for prerequisites. |
| src/commands/build.rs | Switches prerequisite stamp checking to read_stamps_batch. |
| CHANGELOG.md | Documents the fixed reinstall behavior, stamp-based skipping, and lock pin correctness. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@jetm this is ready for review. All four review threads are addressed and resolved:
Verified locally on the pushed tip: One thing worth a second opinion, since it's the actual behavioral heart of the PR: the skip short-circuit means an unchanged project does no container work for rootfs/initramfs. The escape hatches are |
jetm
left a comment
There was a problem hiding this comment.
Reviewed this at b9dc588 and built it in a scratch worktree - cargo test --no-run exits 0. Twelve findings inline, most severe first. The findings themselves are code-traced rather than build-derived, except where a comment says otherwise.
The one that matters most is the first: the sdk install lock merge copies entry.rootfs but not cleared_sections, and because this PR also removed the save() that used to run on the clone (base install.rs:791), a kernel-pin-change clean is no longer authoritative on disk. The result is that avocado.lock permanently regains the old kver's keys, so every later run reads stale and wipes both sysroots - which is the exact loop this PR exists to remove, on the path it is named for.
A pattern worth noting across findings 3, 4, 7 and 9: moving the lock save after the stamp block introduced several windows where an install fully succeeds on disk but its pins or its teardown are skipped. Individually each is a narrow failure; together they mean a partial failure now tends to leave the stamp claiming success while the state behind it is wrong, and the recovery the error text names ("run rootfs install") is the command that then answers "up to date" and exits 0.
Two of the twelve I marked lower-confidence rather than dropping: the latched failed wipe (I could not stage a forced non-zero exit from the clean invocation) and the sdk.image omission from the sysroot stamp (I could not name two images that yield different sysroot contents for identical feed plus config, so treat it as a question about which inputs belong in that hash).
Things I checked and decided are NOT problems, so nobody re-derives them: avocado build --runs-on reading stamps from the local volume is byte-identical to pre-PR behaviour and purely pre-existing; the rm -rf "$AVOCADO_PREFIX/..." shell is not an injection risk because sysroot_dir is a compile-time literal and the prefix is exported unconditionally by the entrypoint; and STAMP_VERSION not being bumped is safe in this direction, since an old stamp cannot match the wider hash and so reads stale.
Addresses jetm's review on #185 (items 1, 2, 6, 7, 8, 11). - lockfile: remove_packages_from_sysroot now marks the section cleared, like clear_rootfs. Without it save()'s disk merge resurrected the dropped pin on the very next save, so a removal never converged -- every later run detected the same removal, wiped, and reinstalled, with avocado.lock permanently listing a package that wasn't installed. - sdk/install: carry cleared_sections across the clone merge. It is #[serde(skip)], so it lived only on the clone install_sysroot ran against; dropping it meant a kernel repin never stuck and both sysroots reinstalled forever. - sdk/clean: remove .stamps/{rootfs,initramfs} with the sysroots, as rootfs/initramfs clean already do. Otherwise sdk clean && sdk install is a silent no-op that leaves two empty sysroots behind an "up to date" message. - rootfs+initramfs install: the two new `?` sites bypassed the "always teardown" block, leaking the local NFS server and the remote mount on a --runs-on failure. Both are captured into `result` the way the original install call already was. - stamps: fold --dnf-args into the sysroot stamp hash. They are interpolated into the transaction, so an up-to-date sysroot silently discarded them. --force stays out: it selects -y and interactivity, not content. - rootfs install: removal_detection_is_noop_on_first_install passes under inverted difference operands (the empty lock early-returns first), which the repo's own test-integrity rule forbids. Added a seeded test that pins the direction; verified it fails when the operands are swapped.
…be as absent Addresses jetm's review on #185 (items 4, 5) plus a doc misattachment found while fixing them. - The stamp write is now gated on the same conditions as the lock update: installed versions actually read, and kernel staging succeeded. Both could fail without failing the install -- query_installed_packages returns Ok(empty) on error and staging only print_error'd -- so a current stamp got written over a sysroot with no staged kernel. Every later run then reported "up to date" while `avocado kernel image` hard-failed telling the user to run the install that was refusing to do anything. Skipping the stamp now says so, and the next run reinstalls. - package_exists_in_target_repo returns Option<bool>: Some means the probe answered, None means it failed. The old `|| true` made a feed blip indistinguishable from "not in the feed", and answering absent dropped the module packagegroup, which then read as a removal, wiped the sysroot, and reinstalled a kernel with no modules -- permanently, since the next healthy run's skip check fires before the probe. A failed probe now keeps the name: if it really is absent, dnf fails loudly instead. - install_sysroot's doc comment was sitting above package_exists_in_target_repo, so install_sysroot shipped undocumented and the probe carried prose about a different function. Both reattached.
Addresses jetm's review on #185 (items 3, 9, 10, 12). - A stamp-write failure no longer discards the install's lock pins. The pins are recorded in memory before the stamp block, and the caller only persists them when install_sysroot returns Ok, so `?`-ing out of a stamp write threw away pins for an install that had already landed -- the next run re-resolved the kernel against feed head with no prev_pinned_kver and installed additively. The stamp write is now a helper whose failure warns instead of propagating; a missing stamp is the benign outcome, since the next run reinstalls. - clean_sysroot reports whether the wipe succeeded instead of discarding it with .ok(). dnf installs additively, so a failed wipe leaves the old kernel's modules and image in place -- and writing a current stamp over that latched the mixed state permanently. It now feeds the same stamp gate as the other two integrity conditions. - compute_sysroot_install_input_hash folds sdk.image, which compute_sdk_input_hash already did. The image is what runs the install, so repointing it while feed and config are byte-identical must not keep a sysroot the old image produced. Answering jetm's question as raised: the two hashes disagreeing about what counts as an input was the actual defect. - StampBatch no longer stores the raw batch output alongside the parsed map purely to re-parse it on every validate(). validate_stamps_batch keeps its string signature for its ~20 callers and now delegates to a parsed-map variant that StampBatch calls directly.
|
@jetm all twelve are addressed in e052539 (three commits), with per-thread replies inline. Short version: Fixed as reported (9): the Both lower-confidence ones were real (2). The latched failed wipe I fixed on reasoning rather than a reproduction, same as you — Cleanup (1): Two things worth pulling out of the threads: The two The failure modes now fall on the harmless side, consistently. A stamp that couldn't be written is benign — its absence makes the next run reinstall. A lock that wasn't saved is not benign — the sysroot moved without it. So the stamp write is now a helper whose failure warns rather than propagating, and the stamp is gated on all three integrity conditions ( One thing found while fixing yours: New tests: removal direction (verified it fails under inverted operands, per the On CI: nothing was actually wrong with it. The two red |
## Problem
`avocado sdk install` redid the rootfs and initramfs installs in full on
every invocation, even when nothing had changed. Two independent defects
caused it.
**1. False "packages removed" detection wiped both sysroots every run.**
`detect_sysroot_package_removals` compared *config-declared* package
names against *locked* names and returned true — meaning `rm -rf
$AVOCADO_PREFIX/<sysroot>` plus a from-scratch reinstall — whenever the
lock held names the config didn't.
But the install auto-appends two packages the config never declares, then
records them in the lock:
- `packagegroup-avocado-{rootfs,initramfs}-modules-<kver>`
- `kernel-image-<kver>` (rootfs only)
From the second run onward those always read as removed. Because
`get_rootfs_packages` / `get_initramfs_packages` default to the single
meta-package, this fired on *every* project, not only ones overriding
`rootfs.packages`. Confirmed against every real lockfile in the tree —
for example `references/jetson-trt/avocado.lock`:
| | locked names | config names |
|---|---|---|
| rootfs | `avocado-pkg-rootfs`, `kernel-image-6.8.12-…`, `packagegroup-avocado-rootfs-modules-6.8.12-…` | `avocado-pkg-rootfs` |
| initramfs | `avocado-pkg-initramfs`, `packagegroup-avocado-initramfs-modules-6.8.12-…` | `avocado-pkg-initramfs` |
**Second-order effect:** the same path called
`remove_packages_from_sysroot`, dropping those packages' version pins
from the in-memory lock. `build_package_spec_with_lock` then fell back to
a bare package name, so dnf resolved the kernel image and module
packagegroup to *newest available* instead of the pinned NVR.
`avocado.lock` looked pinned on disk (it is rewritten post-install) but
the pin never applied.
**2. The install stamps were written but never read.** `install_sysroot`
wrote `rootfs/install.stamp` and `initramfs/install.stamp`, but nothing
consulted them to skip work — across the whole CLI, stamps were only
prerequisite gates. So even with defect 1 fixed, each run still paid a
kernel repoquery, off-kernel exclude computation, a repoquery existence
probe, the dnf transaction, `query_installed_packages`, kernel-sysroot
staging, and the stamp write.
## Solution
An unchanged config now does zero container work for rootfs/initramfs; a
changed one reinstalls exactly the sysroot(s) affected; `avocado.lock`
pins actually bind.
## Key changes
- **Compare removals against the effective set.** `install_sysroot`
resolves the kernel and computes the auto-appended names *before* removal
detection, and `detect_sysroot_package_removals` now takes the effective
name set (config packages ∪ auto-appends) and returns the removed names
rather than a bool. That set is built once and reused as the post-install
lock query list, replacing a second, duplicate computation. A kver change
falls out of the same mechanism, so the dedicated kernel-pin-change block
became the *reason* for the clean rather than a second code path, and the
two verbatim inline `rm -rf` RunConfig blocks collapse into one
`clean_sysroot` helper built from the existing `clean_sysroot_command`.
- **Skip the install when the stamp is current.** New
`read_stamps_batch` / `StampBatch` in `utils/prerequisites.rs` wraps
`generate_batch_read_stamps_script` + one container run + parsing. `sdk
install` reads both sysroot stamps in a single invocation before the
parallel phase and passes them in via `SysrootInstallParams`; the
standalone `rootfs install` / `initramfs install` read their own through
the same helper. `install_sysroot` short-circuits before any container
call. Stamps live in the `/opt/_avocado` named volume rather than a host
bind mount, so a container run is unavoidable — hence batching.
`check_prerequisites` and five sites that hand-rolled the same RunConfig
(`build.rs`, `sdk/{clean,package}.rs`, `ext/{clean,checkout}.rs`) are
ported to the helper.
- **Make the hash trustworthy enough to skip on.** The two per-sysroot
hash functions collapse into one core that now hashes the *effective*
package map (so an absent `rootfs.packages` and an explicit default hash
identically), plus `sdk.repo_url`, `sdk.repo_release` — the hook that
makes a snapshot bump or `avocado update` land — and
`sdk.disable_weak_dependencies`. `StampInputs.package_list_hash` is
populated with a digest of that sysroot's lockfile pins, so a hand-edited
`avocado.lock` or an `avocado unlock` goes stale automatically. An empty
pin set hashes to its own value rather than to `None`, because
`Stamp::is_current` only compares that field when both sides carry one —
otherwise a cleared lock would compare equal by omission.
`STAMP_VERSION` is deliberately *not* bumped: that would invalidate every
SDK/ext/runtime stamp. Changing only these two hash bodies makes existing
rootfs/initramfs stamps mismatch naturally, costing one reinstall.
- **Make `clean` invalidate its stamp.** `clean_sysroot_command` also
removes `$AVOCADO_PREFIX/.stamps/<sysroot>`, mirroring `ext clean`.
Without it, `rootfs clean && sdk install` would skip against a current
stamp and leave an empty sysroot.
- **Drop the mid-flight lockfile saves.** `install_sysroot` and
`stage_kernel_sysroot_from_rootfs` saved their lockfile *clone* while the
rootfs and initramfs tasks ran concurrently on separate clones —
last-writer-wins, healed only by the caller's final save. Saving is now
the caller's job throughout, which removes the race.
Escape hatches are unchanged in spirit: `--no-stamps` disables the read
along with the write, so it always reinstalls, and `avocado rootfs clean`
remains the hard reset.
## Reviewer notes
`cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, and the
full `cargo test` suite are green (1124 lib tests plus the integration
suites, no failures).
New unit tests:
- `detect_sysroot_package_removals` reports nothing for the exact
`references/jetson-trt/avocado.lock` shape — a default config with both
auto-appended kernel packages locked. This is the regression test for the
bug.
- It still reports a genuinely config-dropped package, and reports the
previous kver's auto-appends as stale after a repin.
- The rootfs hash is stable across absent-vs-explicit-default
`rootfs.packages`, and moves on an added package, on `repo_url`,
`repo_release` and `disable_weak_dependencies`, and on a lock re-pin.
- `package_list_hash` is order-independent (pins come out of a `HashMap`),
and an `avocado unlock` invalidates `is_current`.
- A rootfs-only config edit does not invalidate the initramfs hash.
End-to-end verification against a live feed is in progress separately.
The three new stamp-read RunConfigs passed verbose: self.verbose, which sets AVOCADO_VERBOSE=1 for what is only an 'echo ... | tr' script. The five pre-existing read_stamps_batch callers (build, ext/checkout, ext/clean, sdk/clean, sdk/package) all leave verbose at its false default; match them. No behavior change: the stamp read's output is captured rather than echoed on success, and parse_batch_stamps_output ignores any non-':::' line.
Addresses jetm's review on #185 (items 1, 2, 6, 7, 8, 11). - lockfile: remove_packages_from_sysroot now marks the section cleared, like clear_rootfs. Without it save()'s disk merge resurrected the dropped pin on the very next save, so a removal never converged -- every later run detected the same removal, wiped, and reinstalled, with avocado.lock permanently listing a package that wasn't installed. - sdk/install: carry cleared_sections across the clone merge. It is #[serde(skip)], so it lived only on the clone install_sysroot ran against; dropping it meant a kernel repin never stuck and both sysroots reinstalled forever. - sdk/clean: remove .stamps/{rootfs,initramfs} with the sysroots, as rootfs/initramfs clean already do. Otherwise sdk clean && sdk install is a silent no-op that leaves two empty sysroots behind an "up to date" message. - rootfs+initramfs install: the two new `?` sites bypassed the "always teardown" block, leaking the local NFS server and the remote mount on a --runs-on failure. Both are captured into `result` the way the original install call already was. - stamps: fold --dnf-args into the sysroot stamp hash. They are interpolated into the transaction, so an up-to-date sysroot silently discarded them. --force stays out: it selects -y and interactivity, not content. - rootfs install: removal_detection_is_noop_on_first_install passes under inverted difference operands (the empty lock early-returns first), which the repo's own test-integrity rule forbids. Added a seeded test that pins the direction; verified it fails when the operands are swapped.
…be as absent Addresses jetm's review on #185 (items 4, 5) plus a doc misattachment found while fixing them. - The stamp write is now gated on the same conditions as the lock update: installed versions actually read, and kernel staging succeeded. Both could fail without failing the install -- query_installed_packages returns Ok(empty) on error and staging only print_error'd -- so a current stamp got written over a sysroot with no staged kernel. Every later run then reported "up to date" while `avocado kernel image` hard-failed telling the user to run the install that was refusing to do anything. Skipping the stamp now says so, and the next run reinstalls. - package_exists_in_target_repo returns Option<bool>: Some means the probe answered, None means it failed. The old `|| true` made a feed blip indistinguishable from "not in the feed", and answering absent dropped the module packagegroup, which then read as a removal, wiped the sysroot, and reinstalled a kernel with no modules -- permanently, since the next healthy run's skip check fires before the probe. A failed probe now keeps the name: if it really is absent, dnf fails loudly instead. - install_sysroot's doc comment was sitting above package_exists_in_target_repo, so install_sysroot shipped undocumented and the probe carried prose about a different function. Both reattached.
Addresses jetm's review on #185 (items 3, 9, 10, 12). - A stamp-write failure no longer discards the install's lock pins. The pins are recorded in memory before the stamp block, and the caller only persists them when install_sysroot returns Ok, so `?`-ing out of a stamp write threw away pins for an install that had already landed -- the next run re-resolved the kernel against feed head with no prev_pinned_kver and installed additively. The stamp write is now a helper whose failure warns instead of propagating; a missing stamp is the benign outcome, since the next run reinstalls. - clean_sysroot reports whether the wipe succeeded instead of discarding it with .ok(). dnf installs additively, so a failed wipe leaves the old kernel's modules and image in place -- and writing a current stamp over that latched the mixed state permanently. It now feeds the same stamp gate as the other two integrity conditions. - compute_sysroot_install_input_hash folds sdk.image, which compute_sdk_input_hash already did. The image is what runs the install, so repointing it while feed and config are byte-identical must not keep a sysroot the old image produced. Answering jetm's question as raised: the two hashes disagreeing about what counts as an input was the actual defect. - StampBatch no longer stores the raw batch output alongside the parsed map purely to re-parse it on every validate(). validate_stamps_batch keeps its string signature for its ~20 callers and now delegates to a parsed-map variant that StampBatch calls directly.
e052539 to
3ccdae6
Compare
Problem
avocado sdk installredid the rootfs and initramfs installs in full onevery invocation, even when nothing had changed. Two independent defects
caused it.
1. False "packages removed" detection wiped both sysroots every run.
detect_sysroot_package_removalscompared config-declared packagenames against locked names and returned true — meaning
rm -rf $AVOCADO_PREFIX/<sysroot>plus a from-scratch reinstall — whenever thelock held names the config didn't.
But the install auto-appends two packages the config never declares, then
records them in the lock:
packagegroup-avocado-{rootfs,initramfs}-modules-<kver>kernel-image-<kver>(rootfs only)From the second run onward those always read as removed. Because
get_rootfs_packages/get_initramfs_packagesdefault to the singlemeta-package, this fired on every project, not only ones overriding
rootfs.packages. Confirmed against every real lockfile in the tree —for example
references/jetson-trt/avocado.lock:avocado-pkg-rootfs,kernel-image-6.8.12-…,packagegroup-avocado-rootfs-modules-6.8.12-…avocado-pkg-rootfsavocado-pkg-initramfs,packagegroup-avocado-initramfs-modules-6.8.12-…avocado-pkg-initramfsSecond-order effect: the same path called
remove_packages_from_sysroot, dropping those packages' version pinsfrom the in-memory lock.
build_package_spec_with_lockthen fell back toa bare package name, so dnf resolved the kernel image and module
packagegroup to newest available instead of the pinned NVR.
avocado.locklooked pinned on disk (it is rewritten post-install) butthe pin never applied.
2. The install stamps were written but never read.
install_sysrootwrote
rootfs/install.stampandinitramfs/install.stamp, but nothingconsulted them to skip work — across the whole CLI, stamps were only
prerequisite gates. So even with defect 1 fixed, each run still paid a
kernel repoquery, off-kernel exclude computation, a repoquery existence
probe, the dnf transaction,
query_installed_packages, kernel-sysrootstaging, and the stamp write.
Solution
An unchanged config now does zero container work for rootfs/initramfs; a
changed one reinstalls exactly the sysroot(s) affected;
avocado.lockpins actually bind.
Key changes
Compare removals against the effective set.
install_sysrootresolves the kernel and computes the auto-appended names before removal
detection, and
detect_sysroot_package_removalsnow takes the effectivename set (config packages ∪ auto-appends) and returns the removed names
rather than a bool. That set is built once and reused as the post-install
lock query list, replacing a second, duplicate computation. A kver change
falls out of the same mechanism, so the dedicated kernel-pin-change block
became the reason for the clean rather than a second code path, and the
two verbatim inline
rm -rfRunConfig blocks collapse into oneclean_sysroothelper built from the existingclean_sysroot_command.Skip the install when the stamp is current. New
read_stamps_batch/StampBatchinutils/prerequisites.rswrapsgenerate_batch_read_stamps_script+ one container run + parsing.sdk installreads both sysroot stamps in a single invocation before theparallel phase and passes them in via
SysrootInstallParams; thestandalone
rootfs install/initramfs installread their own throughthe same helper.
install_sysrootshort-circuits before any containercall. Stamps live in the
/opt/_avocadonamed volume rather than a hostbind mount, so a container run is unavoidable — hence batching.
check_prerequisitesand five sites that hand-rolled the same RunConfig(
build.rs,sdk/{clean,package}.rs,ext/{clean,checkout}.rs) areported to the helper.
Make the hash trustworthy enough to skip on. The two per-sysroot
hash functions collapse into one core that now hashes the effective
package map (so an absent
rootfs.packagesand an explicit default hashidentically), plus
sdk.repo_url,sdk.repo_release— the hook thatmakes a snapshot bump or
avocado updateland — andsdk.disable_weak_dependencies.StampInputs.package_list_hashispopulated with a digest of that sysroot's lockfile pins, so a hand-edited
avocado.lockor anavocado unlockgoes stale automatically. An emptypin set hashes to its own value rather than to
None, becauseStamp::is_currentonly compares that field when both sides carry one —otherwise a cleared lock would compare equal by omission.
STAMP_VERSIONis deliberately not bumped: that would invalidate everySDK/ext/runtime stamp. Changing only these two hash bodies makes existing
rootfs/initramfs stamps mismatch naturally, costing one reinstall.
Make
cleaninvalidate its stamp.clean_sysroot_commandalsoremoves
$AVOCADO_PREFIX/.stamps/<sysroot>, mirroringext clean.Without it,
rootfs clean && sdk installwould skip against a currentstamp and leave an empty sysroot.
Drop the mid-flight lockfile saves.
install_sysrootandstage_kernel_sysroot_from_rootfssaved their lockfile clone while therootfs and initramfs tasks ran concurrently on separate clones —
last-writer-wins, healed only by the caller's final save. Saving is now
the caller's job throughout, which removes the race.
Escape hatches are unchanged in spirit:
--no-stampsdisables the readalong with the write, so it always reinstalls, and
avocado rootfs cleanremains the hard reset.
Reviewer notes
cargo fmt --check,cargo clippy --all-targets -- -D warnings, and thefull
cargo testsuite are green (1124 lib tests plus the integrationsuites, no failures).
New unit tests:
detect_sysroot_package_removalsreports nothing for the exactreferences/jetson-trt/avocado.lockshape — a default config with bothauto-appended kernel packages locked. This is the regression test for the
bug.
previous kver's auto-appends as stale after a repin.
rootfs.packages, and moves on an added package, onrepo_url,repo_releaseanddisable_weak_dependencies, and on a lock re-pin.package_list_hashis order-independent (pins come out of aHashMap),and an
avocado unlockinvalidatesis_current.End-to-end verification against a live feed is in progress separately.