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        identity_json(
258            self.kind.as_key(),
259            self.version.as_ref(),
260            self.target.as_deref(),
261            &self.raw_version_line,
262        )
263    }
264}
265
266/// Shared JSON shape for detected tool identities: `kind`, optional
267/// `version`, optional `target`, and `raw_version_line`, in that
268/// insertion order.
269fn identity_json(
270    kind: &str,
271    version: Option<&CompilerVersion>,
272    target: Option<&str>,
273    raw_version_line: &str,
274) -> serde_json::Value {
275    let mut obj = serde_json::Map::new();
276    obj.insert(
277        "kind".to_owned(),
278        serde_json::Value::String(kind.to_owned()),
279    );
280    if let Some(v) = version {
281        obj.insert(
282            "version".to_owned(),
283            serde_json::Value::String(v.to_display_string()),
284        );
285    }
286    if let Some(t) = target {
287        obj.insert("target".to_owned(), serde_json::Value::String(t.to_owned()));
288    }
289    obj.insert(
290        "raw_version_line".to_owned(),
291        serde_json::Value::String(raw_version_line.to_owned()),
292    );
293    serde_json::Value::Object(obj)
294}
295
296/// Detected identity of a static-library archiver.
297#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
298pub struct ArchiverIdentity {
299    pub kind: ArchiverKind,
300    #[serde(default, skip_serializing_if = "Option::is_none")]
301    pub version: Option<CompilerVersion>,
302    pub raw_version_line: String,
303}
304
305impl ArchiverIdentity {
306    pub fn unknown(raw_version_line: impl Into<String>) -> Self {
307        Self {
308            kind: ArchiverKind::Unknown,
309            version: None,
310            raw_version_line: raw_version_line.into(),
311        }
312    }
313
314    pub fn as_json(&self) -> serde_json::Value {
315        identity_json(
316            self.kind.as_key(),
317            self.version.as_ref(),
318            None,
319            &self.raw_version_line,
320        )
321    }
322}