Skip to main content

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}