stow_cli/build_consume.rs
1//! The verified cache-consumption machinery `stow-build` reuses: the same
2//! signed-index → digest-check → cosign-verify → inject chain the user CLI
3//! runs, as a narrow facade over the CLI's internal modules. One
4//! implementation serves both callers, so the builder cannot drift from the
5//! verification a stranger's machine gets.
6
7use std::path::Path;
8
9use stow_types::index::ArtifactIndexRow;
10use stow_types::rustc::ParsedRustcArgs;
11
12use crate::artifact_cache::{self, CachedArtifactBundle};
13use crate::fetch::{self, BundleRef};
14use crate::inject;
15use crate::verify;
16
17use crate::config::StowConfig;
18
19pub use crate::index::IndexSlice;
20
21/// The edge/registry/verify-mode configuration the consumption path loads
22/// the same way the CLI does — opaque so the builder goes through the same
23/// entry points a user invocation would.
24#[derive(Debug)]
25pub struct ConsumeConfig(StowConfig);
26
27impl ConsumeConfig {
28 /// Load the config from the environment exactly as the CLI does.
29 ///
30 /// # Errors
31 ///
32 /// Returns an error when the environment config is invalid.
33 pub fn load() -> stow_types::error::Result<Self> {
34 StowConfig::load().map(Self)
35 }
36}
37
38/// Pull and signature-verify the index slice for `(target, rustc_version)`
39/// — the same fetch the resolver runs before any lookup.
40///
41/// # Errors
42///
43/// Returns an error when no usable slice can be produced; the caller
44/// treats that as consumption being unavailable, never as data.
45pub async fn ensure_slice(
46 config: &ConsumeConfig,
47 target: &str,
48 rustc_version: &str,
49) -> stow_types::error::Result<IndexSlice> {
50 crate::index::ensure_slice(&config.0, target, rustc_version).await
51}
52
53/// A verified published bundle staged for a build task's sandbox, with the
54/// two identity fields the capture wrapper cross-checks before injecting.
55#[derive(Debug)]
56pub struct ServedBundle {
57 /// The stable compile key the bundle was published under.
58 pub compile_key: String,
59 /// The crate name the bundle's identity was verified against.
60 pub crate_name: String,
61 inner: CachedArtifactBundle,
62}
63
64/// Why a row the signed index names could not be staged.
65///
66/// The two are not the same failure and must not be handled the same way.
67/// Bytes that never arrived are the ordinary state of a cache: the crate
68/// has not been built for this slice yet, the edge is unreachable, GHCR
69/// returned a 404. Bytes that arrived and then failed the digest,
70/// identity or signature check are not ordinary at all — the signed index
71/// vouched for that artifact, so either the publisher produced something
72/// it cannot stand behind or someone has write access to the registry
73/// they should not have. Compiling quietly past that would hide the one
74/// class of bug the whole verification chain exists to surface, and it
75/// would hide it on the machine that produces what every user installs.
76#[derive(Debug)]
77pub enum StageFailure {
78 /// The bundle never arrived. The unit compiles; nothing is wrong.
79 Unavailable(stow_types::error::Error),
80 /// The bundle arrived and did not verify against what the signed
81 /// index vouches for.
82 Unverifiable(stow_types::error::Error),
83}
84
85impl std::fmt::Display for StageFailure {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 match self {
88 Self::Unavailable(error) | Self::Unverifiable(error) => error.fmt(f),
89 }
90 }
91}
92
93/// Download, digest-check, cosign-verify and stage the bundle `row` names
94/// under `entry_dir`, for the build sandbox's read-only grant.
95///
96/// The chain is the wrapper's own: bytes stream from the edge byte path
97/// digest-checked against the row's `bundle_digest`, the bundle's identity
98/// fields must byte-match the signature-covered `oci/config.json`, and the
99/// cosign signature must verify against the pinned `build-crate.yml`
100/// identity — anything less and the call errors before a byte lands.
101///
102/// # Errors
103///
104/// [`StageFailure::Unavailable`] when the bundle could not be fetched,
105/// [`StageFailure::Unverifiable`] when it arrived and failed any check.
106pub async fn stage_verified_bundle(
107 config: &ConsumeConfig,
108 slice: &IndexSlice,
109 row: &ArtifactIndexRow,
110 entry_dir: &Path,
111) -> Result<(), StageFailure> {
112 let target = slice.index.header.target.as_str();
113 let rustc_version = slice.index.header.rustc_version.as_str();
114 let bundle_ref = BundleRef::from_index_row(target, rustc_version, row);
115 let bytes = fetch::download_bundle_bytes(&config.0, &bundle_ref)
116 .await
117 .map_err(|error| {
118 StageFailure::Unavailable(stow_types::error::Error::msg(format!(
119 "fetch bundle for `{}` {}: {error}",
120 row.crate_name, row.version
121 )))
122 })?;
123 // Past this point the bytes are in hand and the index vouched for
124 // them, so every remaining failure is a statement about the artifact
125 // rather than about reachability.
126 let bundle = fetch::parse_downloaded_bundle(bytes)
127 .await
128 .map_err(StageFailure::Unverifiable)?;
129 fetch::validate_bundle_identity(
130 &bundle,
131 row.crate_name.as_str(),
132 row.c_metadata.as_str(),
133 target,
134 rustc_version,
135 )
136 .map_err(StageFailure::Unverifiable)?;
137 verify::verify_bundle_signature(&config.0, &bundle)
138 .await
139 .map_err(StageFailure::Unverifiable)?;
140 artifact_cache::store_bundle_entry_dir(entry_dir, &bundle)
141 .map_err(StageFailure::Unverifiable)?;
142 Ok(())
143}
144
145/// Load a bundle [`stage_verified_bundle`] staged under `store_dir`, when
146/// the entry exists and parses — a miss is exactly a cold cache and the
147/// calling unit compiles.
148///
149/// # Errors
150///
151/// Returns an error when the entry exists but its manifest is unreadable.
152pub fn load_served_bundle(
153 store_dir: &Path,
154 lease_dir: &Path,
155 compile_key: &str,
156) -> stow_types::error::Result<Option<ServedBundle>> {
157 let Some(bundle) = artifact_cache::load_bundle_entry_dir(
158 &store_dir.join(compile_key),
159 lease_dir,
160 compile_key,
161 )?
162 else {
163 return Ok(None);
164 };
165 Ok(Some(ServedBundle {
166 compile_key: bundle.compile_key.clone(),
167 crate_name: bundle.crate_name.clone(),
168 inner: bundle,
169 }))
170}
171
172/// Inject a served bundle's outputs as the artifacts `parsed` requested —
173/// the same write the user's wrapper performs on a cache hit.
174///
175/// # Errors
176///
177/// Returns an error when an output cannot be materialized.
178pub async fn serve_bundle_outputs(
179 parsed: &ParsedRustcArgs,
180 bundle: &ServedBundle,
181) -> stow_types::error::Result<()> {
182 inject::write_artifacts(
183 parsed,
184 &bundle.inner,
185 inject::OutputDirWriters::UntrustedCodeToo,
186 )
187 .await
188}