Skip to main content

cabin_core/compiler/
identity.rs

1//! Compiler / archiver identity, version, and taxonomy types.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7/// Recognized C/C++ compiler family.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "kebab-case")]
10pub enum CompilerKind {
11    /// LLVM Clang.
12    Clang,
13    /// Apple-shipped Clang (`Apple clang version …`). Treated as
14    /// Clang-compatible for capability purposes; tracked separately
15    /// for diagnostics.
16    AppleClang,
17    /// LLVM `clang-cl`: Clang's `cl.exe`-compatible driver. Reports a
18    /// `clang version …` banner like Clang, but accepts the MSVC
19    /// command line (`/std:c++17`, `/showIncludes`, `/Fo…`), so it is
20    /// detected by the invoked name and drives the MSVC dialect.
21    ClangCl,
22    /// GNU GCC / `g++`.
23    Gcc,
24    /// Microsoft Visual C++ (`cl.exe`). Detected so Cabin can
25    /// produce a clear unsupported-backend error; the GCC/Clang
26    /// command pipeline cannot be used with this compiler.
27    Msvc,
28    /// Compiler whose `--version` output Cabin does not recognize.
29    /// Capability detection treats this conservatively.
30    Unknown,
31}
32
33impl CompilerKind {
34    /// Stable lower-case identifier used in metadata output.
35    pub fn as_key(self) -> &'static str {
36        match self {
37            CompilerKind::Clang => "clang",
38            CompilerKind::AppleClang => "apple-clang",
39            CompilerKind::ClangCl => "clang-cl",
40            CompilerKind::Gcc => "gcc",
41            CompilerKind::Msvc => "msvc",
42            CompilerKind::Unknown => "unknown",
43        }
44    }
45
46    /// Inverse of [`Self::as_key`]: parse a stable family id as
47    /// used by `cfg(cc = "...")` / `cfg(cxx = "...")` conditions
48    /// and `cabin metadata`. Returns `None` outside the closed set.
49    pub fn from_key(key: &str) -> Option<Self> {
50        match key {
51            "clang" => Some(CompilerKind::Clang),
52            "apple-clang" => Some(CompilerKind::AppleClang),
53            "clang-cl" => Some(CompilerKind::ClangCl),
54            "gcc" => Some(CompilerKind::Gcc),
55            "msvc" => Some(CompilerKind::Msvc),
56            "unknown" => Some(CompilerKind::Unknown),
57            _ => None,
58        }
59    }
60
61    /// Whether this compiler is part of the Clang family. `clang-cl`
62    /// is Clang under the hood, so it shares Clang's diagnostic and
63    /// response-file capabilities even though it speaks the MSVC
64    /// dialect.
65    pub fn is_clang_like(self) -> bool {
66        matches!(
67            self,
68            CompilerKind::Clang | CompilerKind::AppleClang | CompilerKind::ClangCl
69        )
70    }
71
72    /// Whether this compiler accepts the GCC-style command line
73    /// the current C++ backend emits (`-O<n>`, `-std=c++NN`,
74    /// `-MMD -MF`, `-DNAME`, `-Idir`, …). Note `clang-cl` is
75    /// excluded: it is Clang but parses the MSVC command line.
76    pub fn supports_gcc_style_command_line(self) -> bool {
77        matches!(
78            self,
79            CompilerKind::Clang | CompilerKind::AppleClang | CompilerKind::Gcc
80        )
81    }
82
83    /// Whether this compiler drives the MSVC command-line dialect
84    /// (`/std:…`, `/Fo…`, `/showIncludes`, `<name>.lib` archives):
85    /// `cl.exe` and Clang's `cl`-compatible `clang-cl` driver.
86    pub fn speaks_msvc_dialect(self) -> bool {
87        matches!(self, CompilerKind::Msvc | CompilerKind::ClangCl)
88    }
89}
90
91impl fmt::Display for CompilerKind {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        f.write_str(self.as_key())
94    }
95}
96
97/// Recognized static-library archiver family.
98#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
99#[serde(rename_all = "kebab-case")]
100pub enum ArchiverKind {
101    /// GNU `ar` / BSD `ar`. Accepts the `crs` mode flags Cabin
102    /// emits today.
103    Ar,
104    /// LLVM `llvm-ar`. Accepts the same `crs` mode flags.
105    LlvmAr,
106    /// Microsoft `lib.exe`. The MSVC dialect's archiver, driven as
107    /// `lib /OUT:<lib> <objs>` to produce a `.lib` static library.
108    Lib,
109    /// Archiver whose `--version` output Cabin does not recognize.
110    Unknown,
111}
112
113impl ArchiverKind {
114    pub fn as_key(self) -> &'static str {
115        match self {
116            ArchiverKind::Ar => "ar",
117            ArchiverKind::LlvmAr => "llvm-ar",
118            ArchiverKind::Lib => "lib",
119            ArchiverKind::Unknown => "unknown",
120        }
121    }
122
123    /// Whether this archiver accepts the `crs` mode flags Cabin
124    /// emits today.
125    pub fn supports_ar_crs(self) -> bool {
126        matches!(self, ArchiverKind::Ar | ArchiverKind::LlvmAr)
127    }
128
129    /// Whether this archiver can produce a static library in some
130    /// dialect Cabin drives: GNU `ar` / `llvm-ar` via `ar crs`, or
131    /// MSVC `lib.exe` via `lib /OUT:`. Distinct from
132    /// [`Self::supports_ar_crs`], which is GNU-specific — `lib.exe`
133    /// produces a static library but not via `crs` mode flags.
134    pub fn produces_static_library(self) -> bool {
135        matches!(
136            self,
137            ArchiverKind::Ar | ArchiverKind::LlvmAr | ArchiverKind::Lib
138        )
139    }
140}
141
142impl fmt::Display for ArchiverKind {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        f.write_str(self.as_key())
145    }
146}
147
148/// Decomposed compiler / archiver version (`major.minor.patch`).
149///
150/// `major` is required; `minor` and `patch` are optional because
151/// some versions only report two components. `raw` keeps the
152/// original substring for diagnostics.
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
154pub struct CompilerVersion {
155    pub major: u32,
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub minor: Option<u32>,
158    #[serde(default, skip_serializing_if = "Option::is_none")]
159    pub patch: Option<u32>,
160    pub raw: String,
161}
162
163impl CompilerVersion {
164    /// Parse a `major[.minor[.patch]]` substring into a typed
165    /// [`CompilerVersion`]. Returns `None` when the leading
166    /// component is not a valid `u32`.
167    pub fn parse(raw: &str) -> Option<Self> {
168        let mut parts = raw.split('.');
169        let major: u32 = parts.next()?.parse().ok()?;
170        let minor = parts.next().and_then(|s| s.parse().ok());
171        let patch = parts.next().and_then(|s| s.parse().ok());
172        Some(Self {
173            major,
174            minor,
175            patch,
176            raw: raw.to_owned(),
177        })
178    }
179
180    /// Like [`Self::parse`], but tolerant of a non-numeric suffix
181    /// on each dotted component (`"20.1.0git"`, `"10.0.0-4ubuntu1"`,
182    /// `"19.1.0-rc2"`): a component contributes its leading ASCII
183    /// digits and must start with a digit. Banner parsers opt into
184    /// this where vendors append suffixes; [`Self::parse`] stays
185    /// strict for callers that need exact numeric tokens.
186    pub fn parse_with_suffix(raw: &str) -> Option<Self> {
187        let mut parts = raw.split('.');
188        let major = leading_digits(parts.next()?)?;
189        let minor = parts.next().and_then(leading_digits);
190        let patch = parts.next().and_then(leading_digits);
191        Some(Self {
192            major,
193            minor,
194            patch,
195            raw: raw.to_owned(),
196        })
197    }
198
199    /// Formatted `major.minor.patch` view, omitting unset
200    /// components. Used in metadata JSON and `CABIN_*` env vars.
201    pub fn to_display_string(&self) -> String {
202        match (self.minor, self.patch) {
203            (Some(min), Some(pat)) => format!("{}.{}.{}", self.major, min, pat),
204            (Some(min), None) => format!("{}.{}", self.major, min),
205            _ => self.major.to_string(),
206        }
207    }
208}
209
210impl fmt::Display for CompilerVersion {
211    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212        f.write_str(&self.to_display_string())
213    }
214}
215
216fn leading_digits(part: &str) -> Option<u32> {
217    let end = part
218        .find(|c: char| !c.is_ascii_digit())
219        .unwrap_or(part.len());
220    if end == 0 {
221        return None;
222    }
223    part[..end].parse().ok()
224}
225
226/// Detected identity of one C/C++ compiler.
227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
228pub struct CompilerIdentity {
229    pub kind: CompilerKind,
230    /// Parsed version, when the version-output line was
231    /// recognized. `None` when the compiler emitted output Cabin
232    /// could not parse.
233    #[serde(default, skip_serializing_if = "Option::is_none")]
234    pub version: Option<CompilerVersion>,
235    /// Optional default target triple as the compiler reported it
236    /// (the "Target: …" line from Clang, or an analogous GCC line).
237    #[serde(default, skip_serializing_if = "Option::is_none")]
238    pub target: Option<String>,
239    /// First non-empty line of combined `--version` output, kept
240    /// for diagnostics. Truncated to a sensible length.
241    pub raw_version_line: String,
242}
243
244impl CompilerIdentity {
245    /// Convenience: identity for an unknown / unparsable compiler.
246    pub fn unknown(raw_version_line: impl Into<String>) -> Self {
247        Self {
248            kind: CompilerKind::Unknown,
249            version: None,
250            target: None,
251            raw_version_line: raw_version_line.into(),
252        }
253    }
254
255    /// Compact JSON view used by `cabin metadata`.
256    pub fn as_json(&self) -> serde_json::Value {
257        let mut obj = serde_json::Map::new();
258        obj.insert(
259            "kind".to_owned(),
260            serde_json::Value::String(self.kind.as_key().to_owned()),
261        );
262        if let Some(v) = &self.version {
263            obj.insert(
264                "version".to_owned(),
265                serde_json::Value::String(v.to_display_string()),
266            );
267        }
268        if let Some(t) = &self.target {
269            obj.insert("target".to_owned(), serde_json::Value::String(t.clone()));
270        }
271        obj.insert(
272            "raw_version_line".to_owned(),
273            serde_json::Value::String(self.raw_version_line.clone()),
274        );
275        serde_json::Value::Object(obj)
276    }
277}
278
279/// Detected identity of a static-library archiver.
280#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
281pub struct ArchiverIdentity {
282    pub kind: ArchiverKind,
283    #[serde(default, skip_serializing_if = "Option::is_none")]
284    pub version: Option<CompilerVersion>,
285    pub raw_version_line: String,
286}
287
288impl ArchiverIdentity {
289    pub fn unknown(raw_version_line: impl Into<String>) -> Self {
290        Self {
291            kind: ArchiverKind::Unknown,
292            version: None,
293            raw_version_line: raw_version_line.into(),
294        }
295    }
296
297    pub fn as_json(&self) -> serde_json::Value {
298        let mut obj = serde_json::Map::new();
299        obj.insert(
300            "kind".to_owned(),
301            serde_json::Value::String(self.kind.as_key().to_owned()),
302        );
303        if let Some(v) = &self.version {
304            obj.insert(
305                "version".to_owned(),
306                serde_json::Value::String(v.to_display_string()),
307            );
308        }
309        obj.insert(
310            "raw_version_line".to_owned(),
311            serde_json::Value::String(self.raw_version_line.clone()),
312        );
313        serde_json::Value::Object(obj)
314    }
315}