diff --git a/build_system/builder/gate/debproof.py b/build_system/builder/gate/debproof.py index c9ec8616e..280220e6b 100644 --- a/build_system/builder/gate/debproof.py +++ b/build_system/builder/gate/debproof.py @@ -164,6 +164,25 @@ def _start(self, runtime: VmDeviceRuntime) -> None: interval=self._install.systemd_ready_interval_seconds, sleep=self._sleep, ) + self._claim_paths() + + def _claim_paths(self) -> None: + """Create and own the writable staging layout before staging writes to it. + + The sealed image dockerignores `cache/`, so the only `/src/cache` in + this container is the mountpoint chain Docker materialized as root for + the read-only package mount. On a host whose uid is not the guest's, + staging as the guest then dies at `mkdir cache/target/tests` -- the + exact shape `InstallContainer._claim_paths` already repairs for the + install gate, applied here to the proof container it never covered. + """ + guest = self._install.guest_user.name + owned = self._install.layout.owned_paths(self._install.mount) + self._docker.exec(self._proof.container, ["mkdir", "-p", *owned]) + self._docker.exec(self._proof.container, ["chown", "-R", f"{guest}:{guest}", *owned]) + parents = self._install.layout.owned_parent_paths(self._install.mount) + self._docker.exec(self._proof.container, ["chown", f"{guest}:{guest}", *parents]) + def _prepare_handoff(self, package: str, version: str) -> None: """Author the exact local graph before the package's postinst runs.""" layout = self._install.layout @@ -262,6 +281,11 @@ def _verify_release(self, expected: str) -> None: f"{guest.home}/{self._install.capsem_home}", "--manifest-url", manifest, + # The handoff hydrated the bytes; the baked URL is what the + # postinst preserves in manifest-metadata for a preverified + # payload. The verifier models exactly this split. + "--metadata-manifest-url", + self.manifest_url, "--channel", self.channel, "--package-version", diff --git a/build_system/builder/gate/releasegraph.py b/build_system/builder/gate/releasegraph.py index f7f0f58ad..40cfb7862 100644 --- a/build_system/builder/gate/releasegraph.py +++ b/build_system/builder/gate/releasegraph.py @@ -29,6 +29,8 @@ from __future__ import annotations +from pathlib import PurePosixPath + from . import config as gate_config from .docker import Docker from .errors import GateError @@ -101,7 +103,19 @@ def record(manifest: str) -> None: manifest = author_binary_graph(assets_manifest, build=build, record=record) self.build_site(dist=out_dir) self.check_channel(admin, channel=channel, dist=out_dir, manifest=manifest) - self.hand_off(manifest) + self.hand_off(manifest, channel=channel) + + def _authored_graph(self, *, channel: str, out_dir: str) -> str: + """Where `assets channel build --channel ` writes its graph. + + `capsem-admin` lays the distribution out as + `/assets//`. The configured + `graph_manifest` names that location for the install gate's `local` + channel; any other channel -- the package proof's `stable`, say -- + replaces the channel segment, not the shape. + """ + name = PurePosixPath(self._config.graph_manifest).name + return f"{out_dir}/assets/{channel}/{name}" def extract_admin(self, package: str) -> str: """Unpack the package without installing it, and return its admin binary. @@ -206,7 +220,7 @@ def build_channel( user=self._config.guest_user.name, cwd=self._mount, ) - return f"{out_dir}/{self._config.graph_manifest}" + return self._authored_graph(channel=channel, out_dir=out_dir) def build_site(self, *, dist: str) -> None: """Render the release site over the generated distribution.""" @@ -241,7 +255,7 @@ def check_channel(self, admin: str, *, channel: str, dist: str, manifest: str) - # -- the handoff ------------------------------------------------------- - def hand_off(self, manifest: str) -> None: + def hand_off(self, manifest: str, *, channel: str) -> None: """Point the package's postinst at the graph just authored. Refuses two mistakes the installer cannot report. A target that does @@ -251,8 +265,9 @@ def hand_off(self, manifest: str) -> None: an install that looks fine and carries the wrong manifest. """ absolute = manifest if manifest.startswith("/") else f"{self._mount}/{manifest}" + authored = f"/assets/{channel}/{PurePosixPath(self._config.graph_manifest).name}" if absolute.endswith(f"/{self._config.legacy_projection}") and not absolute.endswith( - f"/{self._config.graph_manifest}" + authored ): raise GateError( f"the install handoff must select the authoritative release graph, " diff --git a/build_system/tests/gate/test_gate_debproof.py b/build_system/tests/gate/test_gate_debproof.py index 5aa4c9c3a..3c00b4624 100644 --- a/build_system/tests/gate/test_gate_debproof.py +++ b/build_system/tests/gate/test_gate_debproof.py @@ -196,7 +196,7 @@ def test_exact_package_graph_is_checked_and_handed_off_before_dpkg( handoff = transcript.index("install-manifest-request.sh write") install = transcript.index("dpkg -i") assert extract < first_build < record < second_build < check < handoff < install - authoritative = f"{CONFIG.install.layout.channel}/{CONFIG.install.graph_manifest}" + authoritative = f"{CONFIG.install.layout.channel}/assets/nightly/manifest.json" record_command = runner.matching(r"assets channel record-binary")[0] assert f"--manifest-path {authoritative}" in record_command assert f"--source-commit {SOURCE_COMMIT}" in transcript @@ -215,7 +215,7 @@ def test_read_only_content_is_staged_before_record_binary_mutates_the_generated_ assert f":{CONFIG.install.proof_assets_mount}:ro" in started assert f":{CONFIG.install.proof_config_mount}:ro" in started record = runner.matching(r"assets channel record-binary")[0] - authoritative = f"{CONFIG.install.layout.channel}/{CONFIG.install.graph_manifest}" + authoritative = f"{CONFIG.install.layout.channel}/assets/nightly/manifest.json" assert f"--manifest-path {authoritative}" in record assert ( f"--manifest-path {CONFIG.install.proof_assets_mount}/{CONFIG.install.manifest_name}" diff --git a/build_system/tests/gate/test_gate_install_ordering.py b/build_system/tests/gate/test_gate_install_ordering.py index 2be665271..01c34eec2 100644 --- a/build_system/tests/gate/test_gate_install_ordering.py +++ b/build_system/tests/gate/test_gate_install_ordering.py @@ -415,7 +415,7 @@ def test_the_legacy_runtime_projection_is_refused(tmp_path: Path) -> None: graph = ReleaseGraph(Docker(runner), CONFIG, source_commit=SOURCE_COMMIT) with pytest.raises(GateError, match="not the legacy runtime projection"): - graph.hand_off(f"{LAYOUT.assets}/manifest.json") + graph.hand_off(f"{LAYOUT.assets}/manifest.json", channel=INSTALL.channel) assert not runner.ran(r"install-manifest-request"), ( "the refusal must happen before anything is written" @@ -430,7 +430,7 @@ def test_a_handoff_target_that_does_not_exist_is_refused(tmp_path: Path) -> None graph = ReleaseGraph(Docker(runner), CONFIG, source_commit=SOURCE_COMMIT) with pytest.raises(GateError, match="would find no request"): - graph.hand_off(AUTHORITATIVE) + graph.hand_off(AUTHORITATIVE, channel=INSTALL.channel) def test_clearing_a_handoff_that_was_never_written_does_nothing(tmp_path: Path) -> None: