Skip to content

ci: key the guest images and VMM tests on the guest binaries' sources - #710

Merged
Pedro Henrique Penna (ppenna) merged 3 commits into
devfrom
agents/key-guest-images-on-guest-sources
Oct 21, 2026
Merged

Pedro Henrique Penna (ppenna) merged 3 commits into
devfrom
agents/key-guest-images-on-guest-sources

Conversation

@ppenna

@ppenna Pedro Henrique Penna (ppenna) commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Closes #699.
Part of #430.

Summary

The guest image cache keys and the OpenVMM VMM-test classification covered the whole nvx crate in nvx-rs/, although the guest images carry only its nine Rust guest binaries. This PR keys both on the files that the guest binaries' build reads, so that a change to the crate's host side or tests neither rebuilds the guest images nor runs the VMM tests, while a change to any file that a guest binary compiles does both. The NVX microVM tests still run for every nvx-rs change, since they run the nvx-rs CLI.

Every guest build now checks that set against Cargo's dep-info, which is authoritative, and fails when the build reads a file outside it, so the narrower keys stay fail-safe.

Why

What the guest build reads

A guest build of this tree (build-guest-binaries on Linux, Rust 1.95.0) has nine Cargo dep-info files that list 97 files: build.rs and 96 of the 168 files under nvx-rs/src. The other 72 are the host (62) and testing (2) modules, the nvx-rs and nvx-fake-openvmm binaries (2), the protocol test modules (5), and guest/managed_agent/loopback.rs, which only host-featured test builds compile (1). Cargo also reads the manifest, the lockfile, and any Cargo configuration in the crate, none of which dep-info lists.

GuestRustBuildConstants.SOURCE_PATTERN selects exactly those inputs:

  • nvx-rs/Cargo.toml, Cargo.lock, build.rs, rust-toolchain.toml, and the build_constants.py and guest_binaries.py modules that define the build, as before;
  • the crate's Cargo configuration under nvx-rs/.cargo/, which the identity didn't cover;
  • every file under nvx-rs/src except the host side (the host and testing modules and the binaries other than the nine guest ones) and the test modules (tests.rs files, tests directories, and TEST_MODULES, today the loopback harness).

A new module elsewhere counts by default, so new shared or guest modules need no change; the dep-info check catches the other direction.

Why not a separate guest crate? Moving the guest binaries into a crate of their own (#356, #619, #648) would still leave them depending on the shared protocol, net, contract, error, and hypervisor modules, so it would need a three-way split of nvx, and it would conflict with the ~20 open nvx-rs PRs (#652–#689) that edit those sources. The dep-info check gives the same exactness without moving a file.

Changes

Three commits:

  1. build: check the guest build's dep-info against its source identity. build_guest_binaries reads Cargo's dep-info for each of the nine executables after the build and fails, naming the files, unless the source identity covers every file that it lists: each source file that rustc read, include_str!/include_bytes! targets included, and each path that the build script watches. A watched directory must have every file covered, and an empty one never is, since a later file there could change the build without changing the identity; missing paths and paths outside the checkout never are either, and relative paths are rejected. Only files generated in the build's own fresh target directory, such as OUT_DIR, are exempt. The Docker guest stage, the nvx-rs job, native builds, and verify-guest-determinism all build through this routine.
  2. build: limit the guest binaries' source identity to their build inputs. The identity, which the initramfs provenance, the package manifests, the Ubuntu converter digest, and the Azure Linux input digest all take, now uses SOURCE_PATTERN. SOURCE_DIGEST_FORMAT moves to 2, so every guest image key changes once.
  3. ci: key the guest images and VMM tests on the guest binaries' sources. The Alpine, Ubuntu, and Azure Linux hashFiles drop nvx-rs/**, !nvx-rs/target/**, and rust-toolchain.toml, so that the identity at the end of each key alone covers them. OPENVMM_VMM_TEST_INPUT_PATHS replaces nvx-rs/ with SOURCE_PATTERN; CI_TEST_INPUT_PATHS keeps all of nvx-rs/. doc/ci.md, doc/usage.md, and the nvx-rs README describe the inputs.

Tests

  • Acceptance (test_only_guest_binary_inputs_rebuild_guests_and_run_vmm_tests): every file under nvx-rs/ (but target) selects the microVM tests, and selects the VMM tests if and only if it is in the identity; no hashFiles pattern of an image key matches any of them, and each image key ends with the identity. For changes to the host side, the CLI and fake binaries, protocol tests, the loopback harness, integration tests, and artifacts.json, pushes and pull requests run the microVM tests but skip the VMM tests, and the identity, so every image key, is unchanged. Changes to the manifest, lockfile, build script, lib.rs, a protocol module, a guest module, and a guest binary run both and change the identity. New modules count unless they belong to the host side or the tests.
  • Drift guard: the VMM tests' derived inputs now include every file of the identity, and the guard checks that the Alpine key ends with it.
  • Identity: it holds every source file but the host side and the test modules, checked against an independent statement of that rule over the tree; it changes with each input and with new guest, top-level, and Cargo-configuration files, and not with host, test, or README changes.
  • Dep-info check: covered and generated files pass; a host module, a test module, another file or a directory of the checkout, a missing path, a file outside the checkout, and a relative path each fail the build without installing an output; an empty watched directory fails the check until it holds a covered file; a missing dep-info file fails the build; the parser reads escaped spaces, continuations, comments, and phony rules.
  • Updated: the classification table (host-only and guest cases), the shared-module and Azure Linux key tests, and the release tests' fixture checkout, which now copies the nvx-rs CLI's sources explicitly instead of through the guest identity.

Seeded regressions fail the new tests: selecting the VMM tests by all of nvx-rs/, dropping the guest inputs or the anchor from the VMM pattern, hashing nvx-rs/** or nvx-rs/src/host/** in an image key, dropping the identity from the Azure Linux key, removing the host, test, or loopback exclusions, and disabling the dep-info check.

Validation

  • Windows, Python 3.14: all validate-nvx unit-test modules, test_adversarial.py, test_hosts.py, compileall, and the CLI --help smoke checks pass, except 18 test_setup_azure subtests that also fail at origin/dev on this host (they run Linux shell scripts through WSL's bash with Windows paths); CI runs them. ruff check, ruff format --check, pyright for Linux and Windows, and check_markdown_links.py pass. After rebasing onto 5418be9, which changed only guest modules and performance data, test_guest_binaries.py and the CI configuration, build, and release test classes pass again.
  • WSL Ubuntu, Rust 1.95.0, on this head: nvx.py build-guest-binaries builds the nine executables with the check (identity 623237a0…, 102 files: the 97 that dep-info lists and the 5 that it can't). Seeding include_str!("../host/mod.rs") into guest/exit.rs fails the same build with GuestRustBuildConstants … must cover: nvx-rs/src/host/mod.rs. test_guest_binaries.py and the Specula tests pass.
  • CI (run 39626645238, head 6d40e1e99): every job passed. Because the identity's format changed, Build shared artifacts missed every guest image cache, built the guest binaries in Docker with the dep-info check (identity 8f5aa59e…, the suffix of the image keys), and ran verify-guest-determinism; the three platform jobs checked the guest provenance against their checkouts, the WHP one on Windows; and both OpenVMM suites ran, because ci.py and build_constants.py are inputs of both.

Expected effect

  • A host-only nvx-rs merge, such as the 11 above, restores the guest images from the cache (about 0.4 instead of 5.3 minutes before the jobs that boot guests start) and skips the three VMM-test jobs (about 47 runner-minutes, 21 on WHP), so the WHP microVM job becomes its last job.
  • Merges that change guest-compiled files behave as before.
  • Every guest image key changes once, so the first dev push after this PR rebuilds the guest images, as this PR's run does.

Post-merge verification

  • On the first dev push or pull request after this merges that changes only host-side nvx-rs files, confirm in the change detector's summary that the VMM tests are skipped and the microVM tests run, and that Build shared artifacts restores every guest image from the cache.

Copilot AI balanced review requested due to automatic review settings October 21, 2026 15:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The dependency guard accepts empty untracked directories, allowing later additions to leave guest-image cache keys unchanged.

1 open issue
What changed in this PR

Narrows guest-image cache keys and OpenVMM VMM-test selection to guest build inputs, reducing unnecessary CI work for host-only changes.

Changes:

  • Shares one source definition between guest identities and VMM-test selection.
  • Adds Cargo dependency checks and regression coverage.
  • Documents the narrower inputs and cache behavior.
File Description
scripts/​test_nvx_tools.py Tests cache keys, classification, and release fixtures.
scripts/​test_guest_binaries.py Tests source identities and dependency validation.
scripts/​nvx_tools/​guest_binaries.py Filters inputs and validates Cargo dependencies.
scripts/​nvx_tools/​ci.py Narrows VMM-test triggers.
scripts/​nvx_tools/​build_constants.py Defines guest inputs and bumps digest format.
nvx-rs/​README.md Documents guest build dependencies.
doc/​usage.md Explains identity and validation behavior.
doc/​ci.md Documents cache and test-selection changes.
.github/​actions/​build-guest-artifacts/​action.yml Removes whole-crate hashing from image keys.

Review effort: Balanced

Comment thread scripts/nvx_tools/guest_binaries.py
The guest image cache keys end with the source identity of the Rust
guest binaries, a digest of the paths and contents of the files that
GuestRustBuildConstants lists, which the package manifests also record.
Nothing tied that list to what the build reads, though: a module that
the build compiled from outside nvx-rs/src, an include_str! target
there, or a file that the build script watches would change the
executables without changing the keys, and CI would reuse stale guest
images.

Cargo writes a dep-info file beside each executable that lists every
source file of the crate that rustc read, the targets of include_str!
and include_bytes! included, and every path that the build script
watches. build_guest_binaries now reads it for each of the nine
executables after the build, and fails, naming the files, unless the
source identity covers each of them. Each file of a directory that the
build script watches must be covered. An empty directory never is,
because a file added to it later could change the build without
changing the identity, and neither is a path that does not exist or
lies outside the checkout; a relative path is rejected. Files that the
build generates in its fresh target directory, such as those in
OUT_DIR, derive from the others and are exempt. The Docker guest stage,
the nvx-rs job, native guest builds, and verify-guest-determinism all
build through this routine, so each of them runs the check.

The identity covers the build today: the nine dep-info files of a guest
build of this tree list build.rs and 96 files under nvx-rs/src. Tests
run the build against a stand-in Cargo whose dep-info lists covered
files and a generated one. They check that a file, a directory, a
missing path, a file outside the checkout, and a relative path that the
identity omits each fail the build without installing an output, that
a missing dep-info file fails it, that an empty watched directory fails
the check until it holds a covered file, and that the parser reads
escaped spaces, continuation lines, comments, and phony rules.
doc/usage.md describes the check.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The source identity of the Rust guest binaries, which the guest image
cache keys end with and their package manifests and provenance record,
covered every file under nvx-rs/src. The guest build compiles only part
of it, though. Of the 168 tracked files there, the nine executables of
a guest build of this tree read 96, as their Cargo dep-info shows. The
host and testing modules, the nvx-rs and nvx-fake-openvmm binaries, the
test modules of protocol, and the managed agent's loopback harness,
which only host-featured test builds compile, make up the other 72. A
change to any of them changed the identity without changing a guest
binary.

GuestRustBuildConstants.SOURCE_PATTERN now selects the inputs: the
manifest, lockfile, and build script, the toolchain pin, and the two
Python modules that define the build, as before; the crate's Cargo
configuration under nvx-rs/.cargo, which a build in the crate reads and
the identity didn't cover; and every file under nvx-rs/src but the host
side and the test modules. The host side is the host and testing
modules and the binaries other than the guest ones; the test modules
are the tests.rs files, the tests directories, and the modules that
TEST_MODULES lists, today the loopback harness. A new module elsewhere
counts, so a new shared or guest module needs no change here, and the
dep-info check that every guest build runs fails the build if an
excluded file becomes an input. For this tree, the identity is the 97
files that the dep-info lists and the five that it can't: Cargo.toml,
Cargo.lock, rust-toolchain.toml, and the two Python modules. A
directory named target under nvx-rs/src no longer drops out of it;
nothing builds there.

SOURCE_DIGEST_FORMAT moves to 2, because the identity's definition
changed, so every guest image cache key changes once. The initramfs
provenance, the package manifests, the Ubuntu converter digest, and the
Azure Linux input digest take the same files. The guest image keys
still hash all of nvx-rs, so CI rebuilds the images as before; a
following change narrows them.

Tests check that the identity holds every source file but the host side
and the test modules, against an independent statement of that rule
over the tree; that changes to the host side, the tests, the README, or
artifacts.json leave it as it is, while a change to any input, a new
guest or top-level module, or Cargo configuration changes it; and that
a guest build whose dep-info lists a host or test module fails. The
release tests' fixture checkout now also copies the sources of the
nvx-rs CLI, which packaging checks its provenance against and which the
fixture took from the guest identity before. doc/usage.md describes the
inputs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Alpine, Ubuntu, Ubuntu distro, and Azure Linux cache keys hashed
all of nvx-rs but its target directory, and the toolchain pin, and the
OpenVMM VMM tests took every nvx-rs path as an input, because they boot
the Alpine initramfs. The images carry only the crate's Rust guest
binaries, though, so a change to its host side or tests rebuilt every
guest image and ran the VMM tests, the longest WHP jobs. Of the 42 dev
merges through #659, the VMM tests ran on 33, 26 of them only because
nvx-rs changed, and each of the 27 that changed nvx-rs changed every
guest image key. A cold guest-artifact job takes a median 5.3 minutes
instead of 0.4, and every job that boots a guest waits for it.

The keys already end with the guest binaries' source identity, which
now covers exactly the files that their build reads, and which every
guest build checks against Cargo's dep-info. Drop nvx-rs and
rust-toolchain.toml from the hashFiles of the image keys, so that the
identity alone covers them, and select the VMM tests by the same
pattern, GuestRustBuildConstants.SOURCE_PATTERN, instead of by every
nvx-rs path. run-tests still covers all of nvx-rs, because the NVX
microVM tests run the nvx-rs CLI. Replaying the classifier over those
42 merges, 11 of the 26 no longer run the VMM tests, and 12 of the 27
keep every guest image key: they changed only the host side, protocol
tests, integration tests, or the loopback harness. The other 15 changed
files that the guest binaries compile, such as guest and protocol
modules, lib.rs, Cargo.toml, and Cargo.lock, and still run both.

The drift guard that derives the VMM tests' inputs from the guest cache
keys now also takes every file of the identity, and checks that the
Alpine key ends with it. A new test checks every file under nvx-rs:
those in the identity select the VMM tests, the others neither select
them nor match a hashFiles pattern of an image key, and all of them
select the microVM tests. New modules count unless they belong to the
host side or the tests. For changes to the host side, the CLI and fake
binaries, protocol tests, the loopback harness, integration tests, and
artifacts.json, it checks that pushes and pull requests run the microVM
tests but skip the VMM tests, and that the identity, and with it every
image key, stays as it is. For changes to the manifest, lockfile, build
script, lib.rs, protocol, a guest module, and a guest binary, it checks
that they run both and change the identity. doc/ci.md, doc/usage.md,
and the nvx-rs README describe the inputs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 21, 2026 15:32
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/key-guest-images-on-guest-sources branch from af768a7 to 6d40e1e Compare October 21, 2026 15:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Cold guest-image builds and dependency-guard CI validation need confirmation before final human approval.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci: key the guest images and OpenVMM VMM tests on the guest binaries' sources, not all of nvx-rs

2 participants