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}