Skip to main content

cabin_driver/
dialect.rs

1//! Compiler command-line dialects and their artifact conventions.
2
3use cabin_core::CompilerKind;
4
5/// A compiler command-line family.  Selected from the detected C++
6/// compiler and threaded through planning and Ninja generation so
7/// every artifact name and command line speaks one dialect.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum Dialect {
10    /// GCC / Clang.  Object files end in `.o`, static libraries are
11    /// `lib<name>.a`, executables have no extension, and header
12    /// dependencies are tracked with GCC-style depfiles.
13    GnuLike,
14    /// Microsoft Visual C++ (`cl.exe` / `lib.exe`).  Object files end
15    /// in `.obj`, static libraries are `<name>.lib`, executables end
16    /// in `.exe`, and header dependencies are tracked with
17    /// `/showIncludes`.
18    Msvc,
19}
20
21impl Dialect {
22    /// Pick the dialect a compiler family speaks.  MSVC-dialect
23    /// compilers (`cl.exe` and Clang's `clang-cl` driver) drive the
24    /// MSVC dialect; every other recognized (or unrecognized)
25    /// compiler drives the GCC/Clang dialect, which is also the
26    /// safe default for hosts where detection has not run.
27    #[must_use]
28    pub fn from_compiler_kind(kind: CompilerKind) -> Self {
29        if kind.speaks_msvc_dialect() {
30            Dialect::Msvc
31        } else {
32            Dialect::GnuLike
33        }
34    }
35
36    /// The dialect to assume for the host when no compiler detection
37    /// has run.  Used by tooling paths (e.g. `cabin tidy`) that resolve
38    /// but do not probe the toolchain: Windows defaults to MSVC, every
39    /// other host to GCC/Clang, matching the default toolchain each
40    /// host resolves.
41    #[must_use]
42    pub fn host_default() -> Self {
43        if cfg!(windows) {
44            Dialect::Msvc
45        } else {
46            Dialect::GnuLike
47        }
48    }
49
50    /// Extension (without the leading dot) for compiled object files.
51    #[must_use]
52    pub fn object_extension(self) -> &'static str {
53        match self {
54            Dialect::GnuLike => "o",
55            Dialect::Msvc => "obj",
56        }
57    }
58
59    /// File name of the static library built from target `stem`.
60    #[must_use]
61    pub fn static_library_name(self, stem: &str) -> String {
62        match self {
63            Dialect::GnuLike => format!("lib{stem}.a"),
64            Dialect::Msvc => format!("{stem}.lib"),
65        }
66    }
67
68    /// File name of the executable built from target `stem`.
69    #[must_use]
70    pub fn executable_name(self, stem: &str) -> String {
71        match self {
72            Dialect::GnuLike => stem.to_owned(),
73            Dialect::Msvc => format!("{stem}.exe"),
74        }
75    }
76
77    /// How Ninja should discover header dependencies for compiles in
78    /// this dialect.
79    #[must_use]
80    pub fn ninja_deps(self) -> NinjaDeps {
81        match self {
82            Dialect::GnuLike => NinjaDeps::Gcc,
83            // cl.exe prints each included header to stdout prefixed by
84            // this string under `/showIncludes`.  The prefix is
85            // localized by the compiler UI language; the value below
86            // matches an English `cl`, which is what Cabin's CI uses.
87            // A mismatch only degrades incremental rebuild precision,
88            // never correctness of a clean build.
89            Dialect::Msvc => NinjaDeps::Msvc {
90                prefix: "Note: including file:",
91            },
92        }
93    }
94}
95
96/// Ninja's header-dependency discovery mode for a dialect's compile
97/// rules.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum NinjaDeps {
100    /// `deps = gcc`, paired with a Makefile `depfile`.
101    Gcc,
102    /// `deps = msvc`, paired with the `/showIncludes` stdout `prefix`.
103    Msvc {
104        /// Value for Ninja's `msvc_deps_prefix`.
105        prefix: &'static str,
106    },
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn msvc_compiler_selects_msvc_dialect() {
115        assert_eq!(
116            Dialect::from_compiler_kind(CompilerKind::Msvc),
117            Dialect::Msvc
118        );
119    }
120
121    #[test]
122    fn clang_cl_selects_msvc_dialect() {
123        // `clang-cl` is Clang under the hood but speaks the MSVC
124        // command line, so it must drive the MSVC dialect.
125        assert_eq!(
126            Dialect::from_compiler_kind(CompilerKind::ClangCl),
127            Dialect::Msvc
128        );
129    }
130
131    #[test]
132    fn gcc_clang_unknown_select_gnu_dialect() {
133        for kind in [
134            CompilerKind::Clang,
135            CompilerKind::AppleClang,
136            CompilerKind::Gcc,
137            CompilerKind::Unknown,
138        ] {
139            assert_eq!(Dialect::from_compiler_kind(kind), Dialect::GnuLike);
140        }
141    }
142
143    #[test]
144    fn artifact_names_follow_the_dialect() {
145        assert_eq!(Dialect::GnuLike.object_extension(), "o");
146        assert_eq!(Dialect::Msvc.object_extension(), "obj");
147
148        assert_eq!(Dialect::GnuLike.static_library_name("greet"), "libgreet.a");
149        assert_eq!(Dialect::Msvc.static_library_name("greet"), "greet.lib");
150
151        assert_eq!(Dialect::GnuLike.executable_name("app"), "app");
152        assert_eq!(Dialect::Msvc.executable_name("app"), "app.exe");
153    }
154
155    #[test]
156    fn ninja_deps_mode_follows_the_dialect() {
157        assert_eq!(Dialect::GnuLike.ninja_deps(), NinjaDeps::Gcc);
158        assert_eq!(
159            Dialect::Msvc.ninja_deps(),
160            NinjaDeps::Msvc {
161                prefix: "Note: including file:"
162            }
163        );
164    }
165}