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.
95///
96/// Coordinates come from each plan's published entry;
97/// `min_glibc_by_reference` carries the floor the publish stage measured
98/// on each plan's outputs — `None` for an artifact with no glibc
99/// requirement.
100///
101/// # Errors
102/// Returns an error when a plan's `oci_reference` has no published entry
103/// or no measured floor — a record must never register as unmeasured.
104pub fn build_artifact_records(
105    plans: &[PlannedArtifact],
106    published_by_reference: &BTreeMap<String, PublishedArtifact>,
107    min_glibc_by_reference: &BTreeMap<String, Option<crate::glibc::GlibcVersion>>,
108) -> crate::error::Result<Vec<ArtifactRecord>> {
109    let mut records = Vec::with_capacity(plans.len());
110
111    for plan in plans {
112        let Some(published) = published_by_reference.get(&plan.oci_reference) else {
113            return Err(crate::stow_error!(
114                "missing published coordinates for reference {}",
115                plan.oci_reference
116            ));
117        };
118        let Some(min_glibc) = min_glibc_by_reference.get(&plan.oci_reference) else {
119            return Err(crate::stow_error!(
120                "missing measured glibc floor for reference {}",
121                plan.oci_reference
122            ));
123        };
124
125        records.push(ArtifactRecord {
126            compile_key: plan.compile_key.clone(),
127            c_metadata: plan.c_metadata.clone(),
128            extra_filename: plan.extra_filename.clone(),
129            target: plan.target.clone(),
130            rustc_version: plan.rustc_version.clone(),
131            profile: plan.profile.clone(),
132            emit: plan.emit.clone(),
133            crate_name: plan.crate_name.clone(),
134            version: plan.crate_version.clone(),
135            features_json: plan.features_json.clone(),
136            dependency_c_metadata_json: plan.dependency_c_metadata_json.clone(),
137            oci_reference: plan.oci_reference.clone(),
138            oci_digest: published.oci_digest.clone(),
139            has_native: plan.native.is_some(),
140            artifact_kind: plan.kind.clone(),
141            crate_types: plan.crate_types.clone(),
142            artifact_size: plan.artifact_size,
143            bundle_digest: published.bundle_digest.clone(),
144            bundle_size: published.bundle_size,
145            compile_millis: plan.compile_millis,
146            min_glibc: *min_glibc,
147        });
148    }
149
150    Ok(records)
151}
152
153/// The identity inputs hashed into a compile key.
154///
155/// A compile key binds an artifact to one exact rustc invocation identity —
156/// crate coordinates, toolchain, profile, emit set, and dependency
157/// identities — so two invocations sharing a key produce interchangeable
158/// artifacts.
159#[derive(Debug)]
160pub struct CompileKeyInputs<'a> {
161    /// crates.io package name.
162    pub crate_name: &'a str,
163    /// Crate version string.
164    pub crate_version: &'a str,
165    /// Compilation target triple.
166    pub target: &'a str,
167    /// rustc version string.
168    pub rustc_version: &'a str,
169    /// Normalized compile profile.
170    pub profile: &'a Profile,
171    /// Declared crate types.
172    pub crate_types: &'a [RustCrateType],
173    /// Sorted, deduplicated `--emit` kinds.
174    pub emit: &'a [String],
175    /// Canonical JSON-encoded features list.
176    pub features_json: &'a str,
177    /// Canonical JSON-encoded dependency `c_metadata` identities.
178    pub dependency_c_metadata_json: &'a str,
179    /// Primary artifact kind.
180    pub kind: &'a ArtifactKind,
181    /// `-Z embed-metadata` value when the invocation carried the flag
182    /// (nightly cargo emits it on every unit). `None` hashes to the same
183    /// key invocations produced before the flag was modeled.
184    pub embed_metadata: Option<bool>,
185    /// Sorted, deduplicated `--cfg` values other than `feature="…"`
186    /// (build-script `cargo:rustc-cfg` output). An empty list hashes to
187    /// the same key invocations produced before cfgs were modeled.
188    pub cfgs: &'a [String],
189    /// Whether the object files carry LLVM bitcode (`-C embed-bitcode`
190    /// absent or `yes`). `false` — the value cargo passes to every unit no
191    /// LTO consumer needs bitcode from — hashes to the same key invocations
192    /// produced before the flag was modeled.
193    pub embed_bitcode: bool,
194    /// Sorted link-steering `-C` options that reach a link step for this
195    /// unit. Empty for every rlib, because rustc never runs a linker to
196    /// produce one, and empty for a linked unit that chose no link options
197    /// — so an empty list hashes to the same key invocations produced
198    /// before the linker was modeled.
199    pub link_options: &'a [String],
200}
201
202/// Compute the BLAKE3 compile key over an invocation's identity inputs.
203///
204/// # Errors
205/// Returns an error when `profile`, `crate_types`, or `emit` fail to
206/// serialize for hashing.
207pub fn compute_compile_key(inputs: &CompileKeyInputs<'_>) -> crate::error::Result<String> {
208    let mut hasher = Hasher::new();
209    hasher.update(b"stow-compile-key-v1");
210    update_str(&mut hasher, inputs.crate_name);
211    update_str(&mut hasher, inputs.crate_version);
212    update_str(&mut hasher, inputs.target);
213    update_str(&mut hasher, inputs.rustc_version);
214    update_str(&mut hasher, inputs.features_json);
215    update_str(&mut hasher, inputs.dependency_c_metadata_json);
216    update_str(&mut hasher, inputs.kind.as_str());
217    update_str(
218        &mut hasher,
219        &serde_json::to_string(inputs.profile).map_err(|error| {
220            crate::stow_error!(
221                "serialize compile profile for {} {}: {error}",
222                inputs.crate_name,
223                inputs.crate_version
224            )
225        })?,
226    );
227    update_str(
228        &mut hasher,
229        &serde_json::to_string(inputs.crate_types).map_err(|error| {
230            crate::stow_error!(
231                "serialize crate types for {} {}: {error}",
232                inputs.crate_name,
233                inputs.crate_version
234            )
235        })?,
236    );
237    update_str(
238        &mut hasher,
239        &serde_json::to_string(inputs.emit).map_err(|error| {
240            crate::stow_error!(
241                "serialize emit kinds for {} {}: {error}",
242                inputs.crate_name,
243                inputs.crate_version
244            )
245        })?,
246    );
247    if let Some(embed_metadata) = inputs.embed_metadata {
248        update_str(&mut hasher, if embed_metadata { "yes" } else { "no" });
249    }
250    if !inputs.link_options.is_empty() {
251        update_str(&mut hasher, "link-options");
252        update_str(
253            &mut hasher,
254            &serde_json::to_string(inputs.link_options).map_err(|error| {
255                crate::stow_error!(
256                    "serialize link options for {} {}: {error}",
257                    inputs.crate_name,
258                    inputs.crate_version
259                )
260            })?,
261        );
262    }
263    if !inputs.cfgs.is_empty() {
264        update_str(&mut hasher, "cfgs");
265        update_str(
266            &mut hasher,
267            &serde_json::to_string(inputs.cfgs).map_err(|error| {
268                crate::stow_error!(
269                    "serialize cfgs for {} {}: {error}",
270                    inputs.crate_name,
271                    inputs.crate_version
272                )
273            })?,
274        );
275    }
276    if inputs.embed_bitcode {
277        update_str(&mut hasher, "embed-bitcode=yes");
278    }
279    Ok(hasher.finalize().to_hex().to_string())
280}
281
282fn update_str(hasher: &mut Hasher, value: &str) {
283    let len = u32::try_from(value.len()).expect("hash input string length exceeds u32 range");
284    hasher.update(&len.to_le_bytes());
285    hasher.update(value.as_bytes());
286}