Skip to main content

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}