stow_types/artifact.rs
1//! Artifact classification and identity: rustc crate types, artifact kinds,
2//! the semantic [`ArtifactKey`], and the bundle metadata CI records alongside
3//! each uploaded artifact.
4
5use std::collections::BTreeMap;
6
7use serde::{Deserialize, Serialize};
8
9use crate::crate_info::{CrateId, FeatureSet};
10use crate::platform::{Profile, RustcVersion, Target};
11
12/// One element of rustc's `--crate-type` list.
13///
14/// Serializes kebab-case (`rlib`, `proc-macro`, …). `Ord` is defined over the
15/// wire string rather than declaration order so producers sorting
16/// `crate_types` agree with validators comparing serialized strings.
17#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, utoipa::ToSchema)]
18#[serde(rename_all = "kebab-case")]
19pub enum RustCrateType {
20 /// `lib` — a Rust library in whichever form rustc picks.
21 Lib,
22 /// `rlib` — a Rust static library.
23 Rlib,
24 /// `dylib` — a Rust dynamic library.
25 Dylib,
26 /// `cdylib` — a C-ABI dynamic library.
27 Cdylib,
28 /// `staticlib` — a C-ABI static library.
29 Staticlib,
30 /// `proc-macro` — a procedural macro crate.
31 ProcMacro,
32}
33
34// Order by the wire string, not declaration order: producers sort
35// `crate_types` lists with this `Ord` while validators compare the
36// serialized strings, and the two must agree ("cdylib" < "rlib" even
37// though `Rlib` is declared first).
38impl Ord for RustCrateType {
39 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
40 self.as_str().cmp(other.as_str())
41 }
42}
43
44impl PartialOrd for RustCrateType {
45 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
46 Some(self.cmp(other))
47 }
48}
49
50impl RustCrateType {
51 /// The wire string rustc uses for this crate type.
52 #[must_use]
53 pub const fn as_str(&self) -> &'static str {
54 match self {
55 Self::Lib => "lib",
56 Self::Rlib => "rlib",
57 Self::Dylib => "dylib",
58 Self::Cdylib => "cdylib",
59 Self::Staticlib => "staticlib",
60 Self::ProcMacro => "proc-macro",
61 }
62 }
63}
64
65/// The kind of artifact we're caching.
66#[derive(
67 Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, utoipa::ToSchema,
68)]
69pub enum ArtifactKind {
70 /// rlib + rmeta for library crates (compiled for TARGET).
71 Rlib,
72 /// Dynamic library for dylib crates (compiled for TARGET).
73 Dylib,
74 /// Dynamic library for proc-macro crates (compiled for HOST).
75 /// Contains .so (Linux), .dylib (macOS), or .dll (Windows).
76 ProcMacro,
77}
78
79impl ArtifactKind {
80 /// The lowercase wire string for this kind (`rlib`, `dylib`,
81 /// `proc-macro`).
82 #[must_use]
83 pub const fn as_str(&self) -> &str {
84 match self {
85 Self::Rlib => "rlib",
86 Self::Dylib => "dylib",
87 Self::ProcMacro => "proc-macro",
88 }
89 }
90
91 /// The kind whose [`as_str`](Self::as_str) is `value` — how the
92 /// `artifacts.artifact_kind` column is read back.
93 #[must_use]
94 pub fn parse(value: &str) -> Option<Self> {
95 [Self::Rlib, Self::Dylib, Self::ProcMacro]
96 .into_iter()
97 .find(|kind| kind.as_str() == value)
98 }
99}
100
101/// Semantic artifact identity — used for BUILD PLANNING and ANALYTICS only.
102///
103/// NOT the cache lookup key! Cache lookup uses the composite key
104/// `(c_metadata, target, rustc_version)` where `c_metadata` comes from
105/// cargo's `-C metadata` flag.
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107pub struct ArtifactKey {
108 /// Crate name and version from crates.io.
109 pub crate_id: CrateId,
110 /// Feature set the artifact was built with.
111 pub features: FeatureSet,
112 /// Crate types rustc was asked to emit.
113 pub crate_types: Vec<RustCrateType>,
114 /// For Rlib: compilation target. For `ProcMacro`: HOST triple.
115 pub target: Target,
116 /// Toolchain the artifact was built with.
117 pub rustc_version: RustcVersion,
118 /// Observed from actual rustc args, not assumed.
119 pub profile: Profile,
120 /// Primary artifact kind.
121 pub kind: ArtifactKind,
122}
123
124/// Metadata stored in OCI manifest alongside the artifact.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct ArtifactMetadata {
127 /// Semantic identity of the artifact.
128 pub key: ArtifactKey,
129 /// SHA-256 of the `.rlib` payload, hex-encoded.
130 pub rlib_sha256: String,
131 /// SHA-256 of the `.rmeta` payload when the artifact carries one.
132 pub rmeta_sha256: Option<String>,
133 /// Whether the bundle includes native (C/C++) build-script outputs.
134 pub has_native_artifacts: bool,
135 /// Timestamp of when the trusted build produced the artifact.
136 pub built_at: String,
137 /// GitHub Actions run id of the producing workflow.
138 pub builder_run_id: u64,
139 /// Version of stow that produced the bundle.
140 pub stow_version: String,
141}
142
143/// Build script outputs for crates with C/C++ dependencies.
144///
145/// This is what makes `-sys` crate caching possible. All fields are
146/// captured from the build script output directory in CI and replayed
147/// on the client machine.
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149pub struct NativeArtifacts {
150 /// Static libraries (.a / .lib files) produced by the build script.
151 pub static_libs: Vec<NativeLib>,
152 /// All `cargo:` directives from the build script output.
153 /// Includes rustc-link-lib, rustc-link-search, rustc-cfg, rustc-env, etc.
154 /// Excludes `rerun-if-*` directives (irrelevant for cached artifacts).
155 pub cargo_directives: Vec<String>,
156 /// `DEP_CRATENAME_KEY=VALUE` environment variables for downstream crates.
157 pub dep_env_vars: BTreeMap<String, String>,
158 /// Generated files from the build script's `OUT_DIR`.
159 /// Stored as (`relative_path`, contents) pairs.
160 pub out_dir_files: Vec<OutDirFile>,
161}
162
163/// A static library produced by a build script.
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165pub struct NativeLib {
166 /// Library name (e.g., "ring-core").
167 pub name: String,
168 /// SHA-256 of the library file bytes.
169 pub bytes_sha256: String,
170}
171
172/// One file from the build script's `OUT_DIR`, listed by path and digest.
173///
174/// The bytes live in the bundle's native archive layer, not here. They used to
175/// be hex-encoded inline in [`crate::bundle::ArtifactBlobConfig`], which the
176/// edge writes twice per bundle and never compresses: jemalloc-sys' ~333 MB
177/// `OUT_DIR` became a 1.27 GB download, against 136.8 MB for every compiled
178/// output of fd's entire dependency graph put together.
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
180pub struct OutDirFile {
181 /// Path relative to `OUT_DIR`.
182 pub relative_path: String,
183 /// SHA-256 of the file's bytes, hex-encoded.
184 pub sha256: String,
185}
186
187#[cfg(test)]
188mod tests {
189 use super::{ArtifactKind, RustCrateType};
190
191 #[test]
192 fn artifact_kind_round_trips_through_its_wire_string() {
193 for kind in [
194 ArtifactKind::Rlib,
195 ArtifactKind::Dylib,
196 ArtifactKind::ProcMacro,
197 ] {
198 assert_eq!(ArtifactKind::parse(kind.as_str()), Some(kind));
199 }
200 assert_eq!(ArtifactKind::parse("proc_macro"), None);
201 assert_eq!(ArtifactKind::parse("Rlib"), None);
202 }
203
204 #[test]
205 fn crate_type_ordering_matches_wire_strings() {
206 let mut all = [
207 RustCrateType::Lib,
208 RustCrateType::Rlib,
209 RustCrateType::Dylib,
210 RustCrateType::Cdylib,
211 RustCrateType::Staticlib,
212 RustCrateType::ProcMacro,
213 ];
214 all.sort();
215 let strings: Vec<&str> = all.iter().map(RustCrateType::as_str).collect();
216 let mut sorted_strings = strings.clone();
217 sorted_strings.sort_unstable();
218 assert_eq!(
219 strings, sorted_strings,
220 "producers sort crate_types with Ord while validators compare wire strings; the orders must agree"
221 );
222 }
223}