Skip to main content

stow_types/
platform.rs

1//! Toolchain and target descriptions: target triples, rustc versions,
2//! compile profiles, and panic strategy.
3
4use std::fmt;
5
6use serde::{Deserialize, Serialize};
7
8/// Compilation target triple (e.g., "x86_64-unknown-linux-gnu").
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct Target(pub String);
11
12impl Target {
13    /// Short form for use in OCI tags (e.g., "x86_64-linux" from "x86_64-unknown-linux-gnu").
14    #[must_use]
15    pub fn short(&self) -> String {
16        let parts: Vec<&str> = self.0.split('-').collect();
17        match parts.as_slice() {
18            [arch, _vendor, os, ..] => format!("{arch}-{os}"),
19            [arch, os] => format!("{arch}-{os}"),
20            _ => self.0.clone(),
21        }
22    }
23}
24
25impl fmt::Display for Target {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        f.write_str(&self.0)
28    }
29}
30
31/// Identifies the exact rustc toolchain used for compilation.
32#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
33pub struct RustcVersion {
34    /// Semantic version (e.g., 1.83.0)
35    pub version: semver::Version,
36    /// Short commit hash from `rustc --version --verbose`
37    pub commit_hash: String,
38    /// LLVM version string (e.g., "19.1.4")
39    pub llvm_version: String,
40}
41
42impl RustcVersion {
43    /// Short form for OCI tags: "1.83.0"
44    #[must_use]
45    pub fn short(&self) -> String {
46        self.version.to_string()
47    }
48}
49
50impl fmt::Display for RustcVersion {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        write!(f, "{} ({})", self.version, self.commit_hash)
53    }
54}
55
56/// Compilation profile settings observed from actual rustc arguments.
57#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, utoipa::ToSchema)]
58pub struct Profile {
59    /// `-C opt-level` value as rustc saw it (`"0"`–`"3"`, `"s"`, `"z"`).
60    pub opt_level: String,
61    /// Debug info level normalized to 0 (none), 1 (line tables), or 2 (full).
62    pub debuginfo: u32,
63    /// Whether `-C debug-assertions` was enabled.
64    pub debug_assertions: bool,
65    /// Whether `-C overflow-checks` was enabled.
66    pub overflow_checks: bool,
67    /// `-C panic` strategy.
68    pub panic: PanicStrategy,
69    /// `-C strip` level. Stripping happens at link time, so it only changes
70    /// the bytes of linked artifacts; `normalized_cache_profile` pins it to
71    /// `None` for rlibs. The canonical JSON omits `none`, which is the only
72    /// level artifacts registered before the field existed could carry.
73    #[serde(default, skip_serializing_if = "StripLevel::is_none")]
74    pub strip: StripLevel,
75}
76
77impl Profile {
78    /// Name the fields that differ between a cached artifact's profile
79    /// (`self`) and the one a compile requests, as two parallel `k=v`
80    /// lists. `None` when the two profiles are equal.
81    ///
82    /// A cached artifact only serves a compile that asks for the same
83    /// profile, so a machine whose `[profile.dev]` diverges from the one
84    /// the public cache is built with gets no hits at all. Which knob
85    /// diverged is the whole diagnosis, and nothing downstream can
86    /// reconstruct it.
87    #[must_use]
88    pub fn divergence(&self, requested: &Self) -> Option<(String, String)> {
89        let mut cached = Vec::new();
90        let mut wanted = Vec::new();
91        let mut note = |field: &str, mine: String, theirs: String| {
92            if mine != theirs {
93                cached.push(format!("{field}={mine}"));
94                wanted.push(format!("{field}={theirs}"));
95            }
96        };
97        note(
98            "opt-level",
99            self.opt_level.clone(),
100            requested.opt_level.clone(),
101        );
102        note(
103            "debuginfo",
104            self.debuginfo.to_string(),
105            requested.debuginfo.to_string(),
106        );
107        note(
108            "debug-assertions",
109            self.debug_assertions.to_string(),
110            requested.debug_assertions.to_string(),
111        );
112        note(
113            "overflow-checks",
114            self.overflow_checks.to_string(),
115            requested.overflow_checks.to_string(),
116        );
117        note(
118            "panic",
119            format!("{:?}", self.panic).to_lowercase(),
120            format!("{:?}", requested.panic).to_lowercase(),
121        );
122        note(
123            "strip",
124            format!("{:?}", self.strip).to_lowercase(),
125            format!("{:?}", requested.strip).to_lowercase(),
126        );
127        if cached.is_empty() {
128            return None;
129        }
130        Some((cached.join(", "), wanted.join(", ")))
131    }
132
133    /// Returns true if this is a debug profile (`opt_level` "0" with `debug_assertions`).
134    #[must_use]
135    pub fn is_debug(&self) -> bool {
136        self.opt_level == "0" && self.debug_assertions
137    }
138}
139
140/// Panic strategy used during compilation.
141#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, utoipa::ToSchema)]
142pub enum PanicStrategy {
143    /// `panic=unwind` — rustc's default.
144    Unwind,
145    /// `panic=abort`.
146    Abort,
147}
148
149impl PanicStrategy {
150    /// The `-C panic` value for this strategy.
151    #[must_use]
152    pub const fn as_str(&self) -> &str {
153        match self {
154            Self::Unwind => "unwind",
155            Self::Abort => "abort",
156        }
157    }
158}
159
160impl fmt::Display for PanicStrategy {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        f.write_str(self.as_str())
163    }
164}
165
166/// `-C strip` level as cargo passes it to rustc.
167///
168/// Cargo sets `debuginfo` on its own whenever a profile turns `debug` off,
169/// so this is part of the compile identity rather than a reason to exclude
170/// an invocation.
171#[derive(
172    Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, utoipa::ToSchema,
173)]
174#[serde(rename_all = "lowercase")]
175pub enum StripLevel {
176    /// `strip=none` — rustc's default.
177    #[default]
178    None,
179    /// `strip=debuginfo`.
180    Debuginfo,
181    /// `strip=symbols`.
182    Symbols,
183}
184
185impl StripLevel {
186    /// The `-C strip` value for this level.
187    #[must_use]
188    pub const fn as_str(&self) -> &str {
189        match self {
190            Self::None => "none",
191            Self::Debuginfo => "debuginfo",
192            Self::Symbols => "symbols",
193        }
194    }
195
196    /// Whether this is rustc's default level, omitted from canonical JSON.
197    #[must_use]
198    pub const fn is_none(&self) -> bool {
199        matches!(self, Self::None)
200    }
201}
202
203impl fmt::Display for StripLevel {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        f.write_str(self.as_str())
206    }
207}
208
209#[cfg(test)]
210mod profile_divergence_tests {
211    use super::{PanicStrategy, Profile, StripLevel};
212
213    fn dev() -> Profile {
214        Profile {
215            opt_level: "0".to_owned(),
216            debuginfo: 2,
217            debug_assertions: true,
218            overflow_checks: true,
219            panic: PanicStrategy::Unwind,
220            strip: StripLevel::None,
221        }
222    }
223
224    #[test]
225    fn equal_profiles_do_not_diverge() {
226        assert_eq!(dev().divergence(&dev()), None);
227    }
228
229    #[test]
230    fn only_the_diverging_fields_are_named() {
231        let requested = Profile {
232            debuginfo: 1,
233            ..dev()
234        };
235        assert_eq!(
236            dev().divergence(&requested),
237            Some(("debuginfo=2".to_owned(), "debuginfo=1".to_owned()))
238        );
239    }
240
241    #[test]
242    fn several_diverging_fields_stay_in_parallel() {
243        let requested = Profile {
244            opt_level: "3".to_owned(),
245            debuginfo: 0,
246            ..dev()
247        };
248        assert_eq!(
249            dev().divergence(&requested),
250            Some((
251                "opt-level=0, debuginfo=2".to_owned(),
252                "opt-level=3, debuginfo=0".to_owned()
253            ))
254        );
255    }
256}