Skip to main content

stow_types/
upload_plan.rs

1//! Upload planning: the `PlannedArtifact` list CI produces after a build, and
2//! the compile-key hash that binds a cached artifact to one exact rustc
3//! invocation identity.
4
5use std::collections::BTreeMap;
6use std::path::PathBuf;
7
8use blake3::Hasher;
9use serde::{Deserialize, Serialize};
10
11use crate::api::ArtifactRecord;
12use crate::artifact::{ArtifactKind, NativeArtifacts, RustCrateType};
13use crate::bundle::ArtifactBundleFile;
14use crate::identity::{
15    CMetadata, CrateName, CrateVersion, DependencyCMetadataJson, FeaturesJson, TargetTriple,
16    WireRustcVersion,
17};
18use crate::platform::Profile;
19
20/// One artifact CI plans to upload after a build.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct PlannedArtifact {
23    /// Stable hash of the rustc invocation identity.
24    pub compile_key: String,
25    /// Crate name.
26    pub crate_name: CrateName,
27    /// Crate version.
28    pub crate_version: CrateVersion,
29    /// Cargo `-C metadata` value.
30    pub c_metadata: CMetadata,
31    /// Cargo `-C extra-filename` suffix.
32    pub extra_filename: String,
33    /// Canonical features list.
34    pub features_json: FeaturesJson,
35    /// Sorted dependency identities driving the cache key.
36    pub dependency_c_metadata_json: DependencyCMetadataJson,
37    /// JSON-encoded compile keys of dependencies.
38    pub dependency_compile_keys_json: String,
39    /// Compilation target triple.
40    pub target: TargetTriple,
41    /// Stable rustc version.
42    pub rustc_version: WireRustcVersion,
43    /// Cargo profile.
44    pub profile: Profile,
45    /// Sorted, deduplicated emit modes.
46    pub emit: Vec<String>,
47    /// OCI reference where this artifact will be pushed.
48    pub oci_reference: String,
49    /// Artifact kind (rlib / dylib / proc-macro).
50    pub kind: ArtifactKind,
51    /// Declared crate types.
52    pub crate_types: Vec<RustCrateType>,
53    /// Size in bytes.
54    pub artifact_size: u64,
55    /// Wall-clock milliseconds the captured rustc invocation took.
56    pub compile_millis: u64,
57    /// Files that will be packaged into the bundle.
58    pub outputs: Vec<PlannedArtifactOutput>,
59    /// Optional native (C/C++) artifacts captured from the build script.
60    pub native: Option<NativeArtifacts>,
61    /// The packed `OUT_DIR` tree for `native`, pushed as an extra OCI layer
62    /// after `outputs`.
63    #[serde(default)]
64    pub native_archive: Option<PlannedArtifactOutput>,
65}
66
67/// One output file from a planned artifact.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct PlannedArtifactOutput {
70    /// Filesystem path to the output.
71    pub path: PathBuf,
72    /// Bundle metadata for this file.
73    pub bundle_file: ArtifactBundleFile,
74}
75
76/// Build `ArtifactRecord` rows for D1 registration from upload plans.
77///
78/// # Errors
79/// Returns an error when a plan's `oci_reference` has no entry in
80/// `digests_by_reference` — the OCI push produced no manifest digest for a
81/// planned artifact.
82/// A published artifact's registry coordinates: the OCI manifest digest of
83/// the signed artifact and the digest and size of its `<tag>.bundle` layer.
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct PublishedArtifact {
86    /// OCI manifest digest (`sha256:…`) of the signed artifact.
87    pub oci_digest: String,
88    /// Digest (`sha256:…`) of the bundle tar layer.
89    pub bundle_digest: String,
90    /// Size in bytes of the bundle tar.
91    pub bundle_size: u64,
92}
93
94/// Build the records the register endpoint stores, one per plan, from the
95/// coordinates each plan was published under.
96///
97/// # Errors
98/// Returns an error when a plan's `oci_reference` has no published entry.
99pub fn build_artifact_records(
100    plans: &[PlannedArtifact],
101    published_by_reference: &BTreeMap<String, PublishedArtifact>,
102) -> crate::error::Result<Vec<ArtifactRecord>> {
103    let mut records = Vec::with_capacity(plans.len());
104
105    for plan in plans {
106        let Some(published) = published_by_reference.get(&plan.oci_reference) else {
107            return Err(crate::stow_error!(
108                "missing published coordinates for reference {}",
109                plan.oci_reference
110            ));
111        };
112
113        records.push(ArtifactRecord {
114            compile_key: plan.compile_key.clone(),
115            c_metadata: plan.c_metadata.clone(),
116            extra_filename: plan.extra_filename.clone(),
117            target: plan.target.clone(),
118            rustc_version: plan.rustc_version.clone(),
119            profile: plan.profile.clone(),
120            emit: plan.emit.clone(),
121            crate_name: plan.crate_name.clone(),
122            version: plan.crate_version.clone(),
123            features_json: plan.features_json.clone(),
124            dependency_c_metadata_json: plan.dependency_c_metadata_json.clone(),
125            oci_reference: plan.oci_reference.clone(),
126            oci_digest: published.oci_digest.clone(),
127            has_native: plan.native.is_some(),
128            artifact_kind: plan.kind.clone(),
129            crate_types: plan.crate_types.clone(),
130            artifact_size: plan.artifact_size,
131            bundle_digest: published.bundle_digest.clone(),
132            bundle_size: published.bundle_size,
133            compile_millis: plan.compile_millis,
134        });
135    }
136
137    Ok(records)
138}
139
140/// The identity inputs hashed into a compile key.
141///
142/// A compile key binds an artifact to one exact rustc invocation identity —
143/// crate coordinates, toolchain, profile, emit set, and dependency
144/// identities — so two invocations sharing a key produce interchangeable
145/// artifacts.
146#[derive(Debug)]
147pub struct CompileKeyInputs<'a> {
148    /// crates.io package name.
149    pub crate_name: &'a str,
150    /// Crate version string.
151    pub crate_version: &'a str,
152    /// Compilation target triple.
153    pub target: &'a str,
154    /// rustc version string.
155    pub rustc_version: &'a str,
156    /// Normalized compile profile.
157    pub profile: &'a Profile,
158    /// Declared crate types.
159    pub crate_types: &'a [RustCrateType],
160    /// Sorted, deduplicated `--emit` kinds.
161    pub emit: &'a [String],
162    /// Canonical JSON-encoded features list.
163    pub features_json: &'a str,
164    /// Canonical JSON-encoded dependency `c_metadata` identities.
165    pub dependency_c_metadata_json: &'a str,
166    /// Primary artifact kind.
167    pub kind: &'a ArtifactKind,
168    /// `-Z embed-metadata` value when the invocation carried the flag
169    /// (nightly cargo emits it on every unit). `None` hashes to the same
170    /// key invocations produced before the flag was modeled.
171    pub embed_metadata: Option<bool>,
172    /// Sorted, deduplicated `--cfg` values other than `feature="…"`
173    /// (build-script `cargo:rustc-cfg` output). An empty list hashes to
174    /// the same key invocations produced before cfgs were modeled.
175    pub cfgs: &'a [String],
176    /// Whether the object files carry LLVM bitcode (`-C embed-bitcode`
177    /// absent or `yes`). `false` — the value cargo passes to every unit no
178    /// LTO consumer needs bitcode from — hashes to the same key invocations
179    /// produced before the flag was modeled.
180    pub embed_bitcode: bool,
181}
182
183/// Compute the BLAKE3 compile key over an invocation's identity inputs.
184///
185/// # Errors
186/// Returns an error when `profile`, `crate_types`, or `emit` fail to
187/// serialize for hashing.
188pub fn compute_compile_key(inputs: &CompileKeyInputs<'_>) -> crate::error::Result<String> {
189    let mut hasher = Hasher::new();
190    hasher.update(b"stow-compile-key-v1");
191    update_str(&mut hasher, inputs.crate_name);
192    update_str(&mut hasher, inputs.crate_version);
193    update_str(&mut hasher, inputs.target);
194    update_str(&mut hasher, inputs.rustc_version);
195    update_str(&mut hasher, inputs.features_json);
196    update_str(&mut hasher, inputs.dependency_c_metadata_json);
197    update_str(&mut hasher, inputs.kind.as_str());
198    update_str(
199        &mut hasher,
200        &serde_json::to_string(inputs.profile).map_err(|error| {
201            crate::stow_error!(
202                "serialize compile profile for {} {}: {error}",
203                inputs.crate_name,
204                inputs.crate_version
205            )
206        })?,
207    );
208    update_str(
209        &mut hasher,
210        &serde_json::to_string(inputs.crate_types).map_err(|error| {
211            crate::stow_error!(
212                "serialize crate types for {} {}: {error}",
213                inputs.crate_name,
214                inputs.crate_version
215            )
216        })?,
217    );
218    update_str(
219        &mut hasher,
220        &serde_json::to_string(inputs.emit).map_err(|error| {
221            crate::stow_error!(
222                "serialize emit kinds for {} {}: {error}",
223                inputs.crate_name,
224                inputs.crate_version
225            )
226        })?,
227    );
228    if let Some(embed_metadata) = inputs.embed_metadata {
229        update_str(&mut hasher, if embed_metadata { "yes" } else { "no" });
230    }
231    if !inputs.cfgs.is_empty() {
232        update_str(&mut hasher, "cfgs");
233        update_str(
234            &mut hasher,
235            &serde_json::to_string(inputs.cfgs).map_err(|error| {
236                crate::stow_error!(
237                    "serialize cfgs for {} {}: {error}",
238                    inputs.crate_name,
239                    inputs.crate_version
240                )
241            })?,
242        );
243    }
244    if inputs.embed_bitcode {
245        update_str(&mut hasher, "embed-bitcode=yes");
246    }
247    Ok(hasher.finalize().to_hex().to_string())
248}
249
250fn update_str(hasher: &mut Hasher, value: &str) {
251    let len = u32::try_from(value.len()).expect("hash input string length exceeds u32 range");
252    hasher.update(&len.to_le_bytes());
253    hasher.update(value.as_bytes());
254}