Skip to main content

cabin_core/compiler/
capabilities.rs

1//! Capability model and capability derivation from tool identity.
2
3use serde::{Deserialize, Serialize};
4
5use super::identity::{
6    ArchiverIdentity, ArchiverKind, CompilerIdentity, CompilerKind, CompilerVersion,
7};
8
9/// Where one capability decision came from. Recorded so
10/// `cabin metadata` can show whether Cabin trusted the version
11/// alone, ran a probe, or fell back to a conservative default.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "kebab-case")]
14pub enum CapabilitySource {
15    /// Inferred from a recognized compiler kind/version.
16    Version,
17    /// Conservative default applied when the compiler kind is
18    /// `Unknown` or detection failed.
19    AssumedDefault,
20    /// The selected tool is recognizably unable to provide this
21    /// capability (e.g. MSVC asked for GCC-style flags).
22    Unsupported,
23}
24
25impl CapabilitySource {
26    pub fn as_key(self) -> &'static str {
27        match self {
28            CapabilitySource::Version => "version",
29            CapabilitySource::AssumedDefault => "assumed-default",
30            CapabilitySource::Unsupported => "unsupported",
31        }
32    }
33}
34
35/// One typed capability decision: whether the tool supports it,
36/// and where the answer came from.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38pub struct Capability {
39    pub supported: bool,
40    pub source: CapabilitySource,
41}
42
43impl Capability {
44    pub fn supported_from(source: CapabilitySource) -> Self {
45        Self {
46            supported: true,
47            source,
48        }
49    }
50    pub fn unsupported_from(source: CapabilitySource) -> Self {
51        Self {
52            supported: false,
53            source,
54        }
55    }
56}
57
58/// Capability set for a C/C++ compiler. Every field is decided
59/// during detection so the planner can compare its required set
60/// against the resolved set without re-running parsing logic.
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62pub struct CompilerCapabilities {
63    /// Accepts GCC-style `-O<n>`, `-DNAME`, `-Idir`, `-c`, `-o`.
64    pub gcc_style_flags: Capability,
65    /// Accepts MSVC-style `/O<n>`, `/DNAME`, `/I dir`. Detection-
66    /// only; the current backend never emits these.
67    pub msvc_style_flags: Capability,
68    /// Accepts `-MMD -MF <file>` to write a make-style depfile.
69    pub depfile_mmd_mf: Capability,
70    /// Accepts `-std=c++NN`.
71    pub std_flag: Capability,
72    /// Accepts `-std=c++17` specifically (the planner's current
73    /// fixed C++ standard).
74    pub cxx_standard_17: Capability,
75    /// Accepts `-std=c11` specifically (the planner's current fixed
76    /// C standard). For MSVC this is the `/std:c11` switch, which is
77    /// only available from VS2019 16.8 (`cl` 19.28) onward.
78    pub c_standard_11: Capability,
79}
80
81/// Capability set for a static-library archiver.
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83pub struct ArchiverCapabilities {
84    /// Accepts the `crs` mode flags (the planner's archive form).
85    pub ar_crs: Capability,
86    /// Produces a `.a` static library archive.
87    pub static_library_output: Capability,
88}
89
90/// Derive a [`CompilerCapabilities`] set from the detected
91/// identity. Decisions are made from the recognized compiler
92/// kind, with conservative defaults for [`CompilerKind::Unknown`].
93/// No probe commands are run from this function — the caller's
94/// detection layer already gathered everything we need.
95/// Decide a version-gated capability for an MSVC `cl` whose minimum
96/// supporting version is `(min_major, min_minor)`. A parsed `cl`
97/// version at or above the threshold is `supported`; below it,
98/// `unsupported`. An unparsed version (`None`) is `supported` as an
99/// assumed default — a real `cl` always reports a version, so a parse
100/// miss must not reject an otherwise-modern compiler (mirrors the GCC
101/// `cxx_standard_17` gate's `None` policy).
102fn msvc_versioned_capability(
103    version: Option<&CompilerVersion>,
104    min_major: u32,
105    min_minor: u32,
106) -> Capability {
107    match version.map(|v| (v.major, v.minor.unwrap_or(0))) {
108        Some((major, minor)) if major > min_major || (major == min_major && minor >= min_minor) => {
109            Capability::supported_from(CapabilitySource::Version)
110        }
111        Some(_) => Capability::unsupported_from(CapabilitySource::Version),
112        None => Capability::supported_from(CapabilitySource::AssumedDefault),
113    }
114}
115
116pub fn derive_cxx_capabilities(identity: &CompilerIdentity) -> CompilerCapabilities {
117    let gcc_style = if identity.kind.supports_gcc_style_command_line() {
118        Capability::supported_from(CapabilitySource::Version)
119    } else if identity.kind.speaks_msvc_dialect() {
120        Capability::unsupported_from(CapabilitySource::Unsupported)
121    } else {
122        Capability::unsupported_from(CapabilitySource::AssumedDefault)
123    };
124    let msvc_style = if identity.kind.speaks_msvc_dialect() {
125        Capability::supported_from(CapabilitySource::Version)
126    } else {
127        Capability::unsupported_from(CapabilitySource::AssumedDefault)
128    };
129    let depfile_mmd_mf = if identity.kind.supports_gcc_style_command_line() {
130        Capability::supported_from(CapabilitySource::Version)
131    } else if identity.kind.speaks_msvc_dialect() {
132        // MSVC-dialect compilers discover headers with `/showIncludes`,
133        // not a make-style depfile.
134        Capability::unsupported_from(CapabilitySource::Unsupported)
135    } else {
136        Capability::unsupported_from(CapabilitySource::AssumedDefault)
137    };
138    let std_flag = if identity.kind.supports_gcc_style_command_line() {
139        Capability::supported_from(CapabilitySource::Version)
140    } else if identity.kind.speaks_msvc_dialect() {
141        Capability::unsupported_from(CapabilitySource::Unsupported)
142    } else {
143        Capability::unsupported_from(CapabilitySource::AssumedDefault)
144    };
145    // Every Clang we recognize supports `-std=c++17` / `/std:c++17`
146    // regardless of its reported version, including `clang-cl` (whose
147    // banner is a clang version, not a `cl` version). Any GCC modern
148    // enough to print a major version supports it too (`g++ -std=c++17`
149    // arrived in GCC 5). `cl` is version-gated separately below.
150    let cxx_standard_17 = match identity.kind {
151        CompilerKind::Clang | CompilerKind::AppleClang | CompilerKind::ClangCl => {
152            Capability::supported_from(CapabilitySource::Version)
153        }
154        CompilerKind::Gcc => match identity.version.as_ref().map(|v| v.major) {
155            Some(m) if m >= 5 => Capability::supported_from(CapabilitySource::Version),
156            Some(_) => Capability::unsupported_from(CapabilitySource::Version),
157            None => Capability::supported_from(CapabilitySource::AssumedDefault),
158        },
159        // `cl /std:c++17` is available from VS2017 15.3 (`cl` 19.11).
160        CompilerKind::Msvc => msvc_versioned_capability(identity.version.as_ref(), 19, 11),
161        CompilerKind::Unknown => Capability::unsupported_from(CapabilitySource::AssumedDefault),
162    };
163    // `-std=c11` (and `clang-cl /std:c11`) has been available far
164    // longer than C++17 in GCC/Clang, so every recognized GCC/Clang
165    // (incl. `clang-cl`) supports it. `cl`'s `/std:c11` is newer:
166    // VS2019 16.8 (`cl` 19.28).
167    let c_standard_11 = match identity.kind {
168        CompilerKind::Clang
169        | CompilerKind::AppleClang
170        | CompilerKind::ClangCl
171        | CompilerKind::Gcc => Capability::supported_from(CapabilitySource::Version),
172        CompilerKind::Msvc => msvc_versioned_capability(identity.version.as_ref(), 19, 28),
173        CompilerKind::Unknown => Capability::unsupported_from(CapabilitySource::AssumedDefault),
174    };
175
176    CompilerCapabilities {
177        gcc_style_flags: gcc_style,
178        msvc_style_flags: msvc_style,
179        depfile_mmd_mf,
180        std_flag,
181        cxx_standard_17,
182        c_standard_11,
183    }
184}
185
186/// Derive an [`ArchiverCapabilities`] set from the detected
187/// identity.
188pub fn derive_ar_capabilities(identity: &ArchiverIdentity) -> ArchiverCapabilities {
189    let ar_crs = if identity.kind.supports_ar_crs() {
190        Capability::supported_from(CapabilitySource::Version)
191    } else if identity.kind == ArchiverKind::Lib {
192        Capability::unsupported_from(CapabilitySource::Unsupported)
193    } else {
194        Capability::unsupported_from(CapabilitySource::AssumedDefault)
195    };
196    // Honest across both dialects: `ar` / `llvm-ar` archive via
197    // `ar crs`, `lib.exe` via `lib /OUT:`. The `ar_crs` capability
198    // above stays GNU-specific (`lib.exe` does not accept `crs`),
199    // but both shapes do produce a static library.
200    let static_library_output = if identity.kind.produces_static_library() {
201        Capability::supported_from(CapabilitySource::Version)
202    } else {
203        Capability::unsupported_from(CapabilitySource::AssumedDefault)
204    };
205    ArchiverCapabilities {
206        ar_crs,
207        static_library_output,
208    }
209}