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    /// Whether this compiler is part of the Clang family. `clang-cl`
47    /// is Clang under the hood, so it shares Clang's diagnostic and
48    /// response-file capabilities even though it speaks the MSVC
49    /// dialect.
50    pub fn is_clang_like(self) -> bool {
51        matches!(
52            self,
53            CompilerKind::Clang | CompilerKind::AppleClang | CompilerKind::ClangCl
54        )
55    }
56
57    /// Whether this compiler accepts the GCC-style command line
58    /// the current C++ backend emits (`-O<n>`, `-std=c++NN`,
59    /// `-MMD -MF`, `-DNAME`, `-Idir`, …). Note `clang-cl` is
60    /// excluded: it is Clang but parses the MSVC command line.
61    pub fn supports_gcc_style_command_line(self) -> bool {
62        matches!(
63            self,
64            CompilerKind::Clang | CompilerKind::AppleClang | CompilerKind::Gcc
65        )
66    }
67
68    /// Whether this compiler drives the MSVC command-line dialect
69    /// (`/std:…`, `/Fo…`, `/showIncludes`, `<name>.lib` archives):
70    /// `cl.exe` and Clang's `cl`-compatible `clang-cl` driver.
71    pub fn speaks_msvc_dialect(self) -> bool {
72        matches!(self, CompilerKind::Msvc | CompilerKind::ClangCl)
73    }
74}
75
76impl fmt::Display for CompilerKind {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        f.write_str(self.as_key())
79    }
80}
81
82/// Recognized static-library archiver family.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
84#[serde(rename_all = "kebab-case")]
85pub enum ArchiverKind {
86    /// GNU `ar` / BSD `ar`. Accepts the `crs` mode flags Cabin
87    /// emits today.
88    Ar,
89    /// LLVM `llvm-ar`. Accepts the same `crs` mode flags.
90    LlvmAr,
91    /// Microsoft `lib.exe`. The MSVC dialect's archiver, driven as
92    /// `lib /OUT:<lib> <objs>` to produce a `.lib` static library.
93    Lib,
94    /// Archiver whose `--version` output Cabin does not recognize.
95    Unknown,
96}
97
98impl ArchiverKind {
99    pub fn as_key(self) -> &'static str {
100        match self {
101            ArchiverKind::Ar => "ar",
102            ArchiverKind::LlvmAr => "llvm-ar",
103            ArchiverKind::Lib => "lib",
104            ArchiverKind::Unknown => "unknown",
105        }
106    }
107
108    /// Whether this archiver accepts the `crs` mode flags Cabin
109    /// emits today.
110    pub fn supports_ar_crs(self) -> bool {
111        matches!(self, ArchiverKind::Ar | ArchiverKind::LlvmAr)
112    }
113
114    /// Whether this archiver can produce a static library in some
115    /// dialect Cabin drives: GNU `ar` / `llvm-ar` via `ar crs`, or
116    /// MSVC `lib.exe` via `lib /OUT:`. Distinct from
117    /// [`Self::supports_ar_crs`], which is GNU-specific — `lib.exe`
118    /// produces a static library but not via `crs` mode flags.
119    pub fn produces_static_library(self) -> bool {
120        matches!(
121            self,
122            ArchiverKind::Ar | ArchiverKind::LlvmAr | ArchiverKind::Lib
123        )
124    }
125}
126
127impl fmt::Display for ArchiverKind {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        f.write_str(self.as_key())
130    }
131}
132
133/// Decomposed compiler / archiver version (`major.minor.patch`).
134///
135/// `major` is required; `minor` and `patch` are optional because
136/// some versions only report two components. `raw` keeps the
137/// original substring for diagnostics.
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139pub struct CompilerVersion {
140    pub major: u32,
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub minor: Option<u32>,
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub patch: Option<u32>,
145    pub raw: String,
146}
147
148impl CompilerVersion {
149    /// Parse a `major[.minor[.patch]]` substring into a typed
150    /// [`CompilerVersion`]. Returns `None` when the leading
151    /// component is not a valid `u32`.
152    pub fn parse(raw: &str) -> Option<Self> {
153        let mut parts = raw.split('.');
154        let major: u32 = parts.next()?.parse().ok()?;
155        let minor = parts.next().and_then(|s| s.parse().ok());
156        let patch = parts.next().and_then(|s| s.parse().ok());
157        Some(Self {
158            major,
159            minor,
160            patch,
161            raw: raw.to_owned(),
162        })
163    }
164
165    /// Formatted `major.minor.patch` view, omitting unset
166    /// components. Used in metadata JSON and `CABIN_*` env vars.
167    pub fn to_display_string(&self) -> String {
168        match (self.minor, self.patch) {
169            (Some(min), Some(pat)) => format!("{}.{}.{}", self.major, min, pat),
170            (Some(min), None) => format!("{}.{}", self.major, min),
171            _ => self.major.to_string(),
172        }
173    }
174}
175
176impl fmt::Display for CompilerVersion {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        f.write_str(&self.to_display_string())
179    }
180}
181
182/// Detected identity of one C/C++ compiler.
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184pub struct CompilerIdentity {
185    pub kind: CompilerKind,
186    /// Parsed version, when the version-output line was
187    /// recognized. `None` when the compiler emitted output Cabin
188    /// could not parse.
189    #[serde(default, skip_serializing_if = "Option::is_none")]
190    pub version: Option<CompilerVersion>,
191    /// Optional default target triple as the compiler reported it
192    /// (the "Target: …" line from Clang, or an analogous GCC line).
193    #[serde(default, skip_serializing_if = "Option::is_none")]
194    pub target: Option<String>,
195    /// First non-empty line of combined `--version` output, kept
196    /// for diagnostics. Truncated to a sensible length.
197    pub raw_version_line: String,
198}
199
200impl CompilerIdentity {
201    /// Convenience: identity for an unknown / unparsable compiler.
202    pub fn unknown(raw_version_line: impl Into<String>) -> Self {
203        Self {
204            kind: CompilerKind::Unknown,
205            version: None,
206            target: None,
207            raw_version_line: raw_version_line.into(),
208        }
209    }
210
211    /// Compact JSON view used by `cabin metadata`.
212    pub fn as_json(&self) -> serde_json::Value {
213        let mut obj = serde_json::Map::new();
214        obj.insert(
215            "kind".to_owned(),
216            serde_json::Value::String(self.kind.as_key().to_owned()),
217        );
218        if let Some(v) = &self.version {
219            obj.insert(
220                "version".to_owned(),
221                serde_json::Value::String(v.to_display_string()),
222            );
223        }
224        if let Some(t) = &self.target {
225            obj.insert("target".to_owned(), serde_json::Value::String(t.clone()));
226        }
227        obj.insert(
228            "raw_version_line".to_owned(),
229            serde_json::Value::String(self.raw_version_line.clone()),
230        );
231        serde_json::Value::Object(obj)
232    }
233}
234
235/// Detected identity of a static-library archiver.
236#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
237pub struct ArchiverIdentity {
238    pub kind: ArchiverKind,
239    #[serde(default, skip_serializing_if = "Option::is_none")]
240    pub version: Option<CompilerVersion>,
241    pub raw_version_line: String,
242}
243
244impl ArchiverIdentity {
245    pub fn unknown(raw_version_line: impl Into<String>) -> Self {
246        Self {
247            kind: ArchiverKind::Unknown,
248            version: None,
249            raw_version_line: raw_version_line.into(),
250        }
251    }
252
253    pub fn as_json(&self) -> serde_json::Value {
254        let mut obj = serde_json::Map::new();
255        obj.insert(
256            "kind".to_owned(),
257            serde_json::Value::String(self.kind.as_key().to_owned()),
258        );
259        if let Some(v) = &self.version {
260            obj.insert(
261                "version".to_owned(),
262                serde_json::Value::String(v.to_display_string()),
263            );
264        }
265        obj.insert(
266            "raw_version_line".to_owned(),
267            serde_json::Value::String(self.raw_version_line.clone()),
268        );
269        serde_json::Value::Object(obj)
270    }
271}