Skip to main content

cabin_driver/
lower.rs

1//! Lower the semantic [`BuildAction`] IR into concrete command argv
2//! for a [`Dialect`].
3//!
4//! This is the single point where compile/archive/link intent becomes
5//! a real command line. [`lower()`] dispatches on the dialect; the
6//! GNU/Clang and MSVC spellings live side by side below.  The planner,
7//! the IR, and the Ninja writer never spell a flag themselves - they
8//! call [`lower()`] (or [`compile_argv`] for the compilation database).
9
10use camino::Utf8PathBuf;
11
12#[cfg(test)]
13use cabin_core::{CStandard, CxxStandard};
14use cabin_core::{LanguageStandard, OptLevel, SourceLanguage};
15
16use crate::action::{ArchiveAction, BuildAction, CompileAction, CompileMode, LinkAction};
17use crate::dialect::Dialect;
18
19/// A fully-lowered action: the backend artifact the Ninja writer
20/// renders.
21///
22/// Mirrors the IR action shape - argv plus the metadata Ninja needs -
23/// but is *produced* by lowering, never authored directly by the
24/// planner.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct LoweredAction {
27    /// Categorization the backend switches on to pick a rule.
28    pub kind: LoweredActionKind,
29    /// Inputs that participate in the command.
30    pub inputs: Vec<Utf8PathBuf>,
31    /// Inputs the action implicitly depends on but that are not
32    /// arguments.
33    pub implicit_inputs: Vec<Utf8PathBuf>,
34    /// Files this action produces.
35    pub outputs: Vec<Utf8PathBuf>,
36    /// Optional Makefile-style depfile path.  Only the GNU/Clang
37    /// dialect populates this; the MSVC dialect tracks dependencies
38    /// through Ninja's `deps = msvc` and leaves it `None`.
39    pub depfile: Option<Utf8PathBuf>,
40    /// Argv-style command, ready to be shell-quoted by the backend.
41    pub command: Vec<String>,
42    /// Short, human-readable description for build output.
43    pub description: String,
44}
45
46/// Categorization of a lowered action.  A closed set; new variants
47/// require explicit handling by every backend.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum LoweredActionKind {
50    /// Compile a single C translation unit (`.c`) to an object file.
51    CompileC,
52    /// Compile a single C++ translation unit to an object file.
53    CompileCpp,
54    /// Parse + semantic-check a C translation unit.  Produces no
55    /// object file; a stamp output records success.
56    SyntaxCheckC,
57    /// Parse + semantic-check a C++ translation unit.  Produces no
58    /// object file; a stamp output records success.
59    SyntaxCheckCpp,
60    /// Archive a set of object files into a static library.
61    ArchiveStaticLibrary,
62    /// Link object files plus static archives into an executable.
63    LinkExecutable,
64}
65
66/// Lower one semantic [`BuildAction`] for `dialect`.
67///
68/// Infallible: every path in the semantic IR is already
69/// [`camino::Utf8Path`], so embedding it in a command line cannot
70/// fail.
71#[must_use]
72pub fn lower(dialect: Dialect, action: &BuildAction) -> LoweredAction {
73    match action {
74        BuildAction::Compile(compile) => lower_compile(dialect, compile),
75        BuildAction::Archive(archive) => lower_archive(dialect, archive),
76        BuildAction::Link(link) => lower_link(dialect, link),
77    }
78}
79
80/// Build the unwrapped compiler argv for a compile action in
81/// `dialect` - the form recorded in `compile_commands.json` (no
82/// compiler wrapper). [`lower()`] prepends the wrapper on top of
83/// this for the run command.
84#[must_use]
85pub fn compile_argv(dialect: Dialect, compile: &CompileAction) -> Vec<String> {
86    match dialect {
87        Dialect::GnuLike => compile_argv_gnu(compile),
88        Dialect::Msvc => compile_argv_msvc(compile),
89    }
90}
91
92fn lower_compile(dialect: Dialect, compile: &CompileAction) -> LoweredAction {
93    // The compiler wrapper is a run-command-only prefix and is
94    // applied here, never in the shared argv builder (so
95    // `compile_commands.json` keeps the underlying compiler).
96    let mut command = compile_argv(dialect, compile);
97    if let Some(wrapper) = &compile.compiler_wrapper {
98        command.insert(0, wrapper.to_string());
99    }
100    let (kind, outputs) = match &compile.mode {
101        CompileMode::Object => {
102            let kind = match compile.standard.language() {
103                SourceLanguage::C => LoweredActionKind::CompileC,
104                SourceLanguage::Cxx => LoweredActionKind::CompileCpp,
105            };
106            (kind, vec![compile.object.clone()])
107        }
108        CompileMode::SyntaxOnly { stamp } => {
109            let kind = match compile.standard.language() {
110                SourceLanguage::C => LoweredActionKind::SyntaxCheckC,
111                SourceLanguage::Cxx => LoweredActionKind::SyntaxCheckCpp,
112            };
113            (kind, vec![stamp.clone()])
114        }
115    };
116    // Only the GNU/Clang dialect emits a Makefile depfile; MSVC
117    // tracks headers via `/showIncludes` (Ninja `deps = msvc`).
118    let depfile = match dialect {
119        Dialect::GnuLike => compile.depfile.clone(),
120        Dialect::Msvc => None,
121    };
122    LoweredAction {
123        kind,
124        inputs: vec![compile.source.clone()],
125        implicit_inputs: compile.implicit_inputs.clone(),
126        outputs,
127        depfile,
128        command,
129        description: compile.description.clone(),
130    }
131}
132
133// ---------------------------------------------------------------
134// GNU / Clang dialect.
135// ---------------------------------------------------------------
136
137fn gnu_std_flag(standard: LanguageStandard, gnu_extensions: bool) -> String {
138    if gnu_extensions {
139        // The GNU spelling of every ISO level swaps the leading `c`
140        // for `gnu`: `c++20` → `gnu++20`, `c17` → `gnu17`.  The data
141        // model carries ISO levels only; this spelling exists at
142        // lowering time alone.
143        format!("-std=gnu{}", &standard.as_str()[1..])
144    } else {
145        format!("-std={standard}")
146    }
147}
148
149/// GNU/Clang compile argv.  The layout is fixed so it reproduces the
150/// historic command lines byte-for-byte: driver, standard, profile
151/// (`-O<n>` / `-g` / `-DNDEBUG`), the `-MD -MF <depfile>` (plus
152/// `-MT <stamp>` in syntax-only mode) dependency block, defines,
153/// includes, system includes, escape-hatch flags, and the
154/// mode-specific tail.
155fn compile_argv_gnu(compile: &CompileAction) -> Vec<String> {
156    let args = &compile.arguments;
157    let mut out: Vec<String> = Vec::new();
158    out.push(compile.compiler.to_string());
159    out.push(gnu_std_flag(compile.standard, compile.gnu_extensions));
160    out.push(args.opt_level.as_flag().to_owned());
161    if args.debug_info {
162        out.push("-g".to_owned());
163    }
164    if args.define_ndebug {
165        out.push("-DNDEBUG".to_owned());
166    }
167    if let Some(depfile) = &compile.depfile {
168        // `-MD`, not `-MMD`: `-MMD` omits headers found through
169        // system include dirs, so an edit under an `-isystem` path
170        // (a foundation port, an extracted registry package, a
171        // pkg-config dir) would stop invalidating rebuilds.
172        out.push("-MD".to_owned());
173        out.push("-MF".to_owned());
174        out.push(depfile.to_string());
175        // In syntax-only mode the depfile records the stamp (not an
176        // object) as its target, so header edits still invalidate the
177        // check via Ninja's `deps = gcc` machinery.
178        if let CompileMode::SyntaxOnly { stamp } = &compile.mode {
179            out.push("-MT".to_owned());
180            out.push(stamp.to_string());
181        }
182    }
183    for define in &args.defines {
184        out.push(format!("-D{define}"));
185    }
186    for include in &args.include_dirs {
187        out.push("-I".to_owned());
188        out.push(include.to_string());
189    }
190    // System include dirs come after the user dirs: `-isystem`
191    // paths are searched after every `-I` path, so spelling them
192    // last keeps argv order aligned with the actual search order.
193    for include in &args.system_include_dirs {
194        out.push("-isystem".to_owned());
195        out.push(include.to_string());
196    }
197    out.extend(args.extra_flags.iter().cloned());
198    match &compile.mode {
199        CompileMode::Object => {
200            out.push("-c".to_owned());
201            out.push(compile.source.to_string());
202            out.push("-o".to_owned());
203            out.push(compile.object.to_string());
204        }
205        CompileMode::SyntaxOnly { .. } => {
206            out.push(compile.source.to_string());
207            out.push("-fsyntax-only".to_owned());
208        }
209    }
210    out
211}
212
213fn lower_archive_gnu(archive: &ArchiveAction) -> Vec<String> {
214    // GNU `ar` archives with the `crs` mode flags (create, replace,
215    // write index): `ar crs <lib> <obj>...`.
216    let mut command = vec![
217        archive.archiver.to_string(),
218        "crs".to_owned(),
219        archive.output.to_string(),
220    ];
221    for input in &archive.inputs {
222        command.push(input.to_string());
223    }
224    command
225}
226
227fn lower_link_gnu(link: &LinkAction) -> Vec<String> {
228    // `<driver> <inputs...> <ldflags...> -l<lib>... -o <exe>`.
229    // System libraries follow the archives so a static library's
230    // dependencies resolve left-to-right under GNU `ld`.
231    let mut command = vec![link.linker.to_string()];
232    for input in &link.inputs {
233        command.push(input.to_string());
234    }
235    command.extend(link.arguments.iter().cloned());
236    for lib in &link.link_libs {
237        command.push(format!("-l{lib}"));
238    }
239    command.push("-o".to_owned());
240    command.push(link.output.to_string());
241    command
242}
243
244// ---------------------------------------------------------------
245// MSVC (`cl.exe` / `lib.exe`) dialect.
246// ---------------------------------------------------------------
247
248fn msvc_std_flag(standard: LanguageStandard) -> &'static str {
249    standard.msvc_spelling().unwrap_or_else(|| {
250        unreachable!(
251            "the planner validates MSVC-dialect standards before lowering; `{standard}` has no stable /std: flag"
252        )
253    })
254}
255
256/// The `cl.exe` flag that forces the source's language, prepended to the
257/// file name (`/Tp<file>` for C++, `/Tc<file>` for C).
258///
259/// `cl` infers language from extension and only defaults `.cpp` / `.cxx`
260/// to C++; Cabin also classifies `.cc`, `.c++`, and `.C` as C++ (and
261/// `.c` as C).  Driving the language explicitly makes the translation
262/// unit follow Cabin's own source classification rather than `cl`'s
263/// extension table, so every supported extension compiles as the
264/// language Cabin intends.
265fn msvc_source_flag(language: SourceLanguage) -> &'static str {
266    match language {
267        SourceLanguage::C => "/Tc",
268        SourceLanguage::Cxx => "/Tp",
269    }
270}
271
272fn msvc_opt_flag(opt: OptLevel) -> &'static str {
273    match opt {
274        OptLevel::O0 => "/Od",
275        OptLevel::O1 | OptLevel::S | OptLevel::Z => "/O1",
276        // cl.exe has no `/O3`; `/O2` is its maximum speed setting.
277        OptLevel::O2 | OptLevel::O3 => "/O2",
278    }
279}
280
281/// MSVC `cl.exe` compile argv.  Mirrors the GNU layout with MSVC
282/// spellings: `/std:` standard, `/utf-8` source/execution charset,
283/// `/EHsc` for C++ exceptions, `/O` optimization, `/Z7` debug info
284/// (embedded in the object so parallel compiles never contend on a
285/// shared PDB), `/showIncludes` for dependency discovery (no
286/// Makefile depfile), `/D` defines, `/I` includes, escape-hatch
287/// flags, and the mode-specific tail (`/c /Tp<src> /Fo<obj>` or
288/// `/Tp<src> /Zs`, with `/Tc` for C).
289fn compile_argv_msvc(compile: &CompileAction) -> Vec<String> {
290    debug_assert!(
291        !compile.gnu_extensions,
292        "the planner rejects `gnu-extensions` on the MSVC dialect before lowering"
293    );
294    let args = &compile.arguments;
295    let mut out: Vec<String> = vec![compile.compiler.to_string(), "/nologo".to_owned()];
296    // GCC and Clang interpret source files as UTF-8 by default, while
297    // `cl` falls back to the machine's active code page unless told
298    // otherwise.  Pinning `/utf-8` makes the dialects agree on what a
299    // source file means, and lets UTF-8-requiring headers (e.g.
300    // {fmt}'s `static_assert` on the literal encoding) compile out of
301    // the box.  Every `cl` new enough to pass Cabin's `/std:`
302    // validation (19.11+) understands the flag, as does `clang-cl`.
303    out.push("/utf-8".to_owned());
304    out.push(msvc_std_flag(compile.standard).to_owned());
305    if compile.standard.language() == SourceLanguage::Cxx {
306        out.push("/EHsc".to_owned());
307    }
308    out.push(msvc_opt_flag(args.opt_level).to_owned());
309    if args.debug_info {
310        out.push("/Z7".to_owned());
311    }
312    if args.define_ndebug {
313        out.push("/DNDEBUG".to_owned());
314    }
315    // `/showIncludes` drives Ninja's `deps = msvc`.  Emitted whenever
316    // the planner asked for dependency tracking, matching the GNU
317    // dialect's `-MD -MF` condition.
318    if compile.depfile.is_some() {
319        out.push("/showIncludes".to_owned());
320    }
321    for define in &args.defines {
322        out.push(format!("/D{define}"));
323    }
324    for include in &args.include_dirs {
325        out.push("/I".to_owned());
326        out.push(include.to_string());
327    }
328    // `/external:I` marks the directory as external; `/external:W0`
329    // (emitted once, ahead of the block) silences warnings inside
330    // those headers, matching the GNU dialect's `-isystem`
331    // semantics.  The planner only populates the system bucket when
332    // the detected `cl` / `clang-cl` understands `/external:`.
333    if !args.system_include_dirs.is_empty() {
334        out.push("/external:W0".to_owned());
335    }
336    for include in &args.system_include_dirs {
337        out.push("/external:I".to_owned());
338        out.push(include.to_string());
339    }
340    out.extend(args.extra_flags.iter().cloned());
341    let source = format!(
342        "{}{}",
343        msvc_source_flag(compile.standard.language()),
344        compile.source.as_str()
345    );
346    match &compile.mode {
347        CompileMode::Object => {
348            out.push("/c".to_owned());
349            out.push(source);
350            out.push(format!("/Fo{}", compile.object));
351        }
352        CompileMode::SyntaxOnly { .. } => {
353            out.push(source);
354            out.push("/Zs".to_owned());
355        }
356    }
357    out
358}
359
360fn lower_archive_msvc(archive: &ArchiveAction) -> Vec<String> {
361    // `lib /nologo /OUT:<lib> <obj>...`.
362    let mut command = vec![
363        archive.archiver.to_string(),
364        "/nologo".to_owned(),
365        format!("/OUT:{}", archive.output),
366    ];
367    for input in &archive.inputs {
368        command.push(input.to_string());
369    }
370    command
371}
372
373fn lower_link_msvc(link: &LinkAction) -> Vec<String> {
374    // `<driver> /nologo <inputs...> <lib>.lib... /Fe<exe> [/link <ldflags...>]`.
375    // cl.exe consumes object and `.lib` inputs positionally and
376    // forwards `/link` options to the linker, so system libraries are
377    // spelled `<name>.lib` and passed as positional inputs after the
378    // archives rather than as GNU `-l<name>` flags.
379    let mut command = vec![link.linker.to_string(), "/nologo".to_owned()];
380    for input in &link.inputs {
381        command.push(input.to_string());
382    }
383    for lib in &link.link_libs {
384        command.push(format!("{lib}.lib"));
385    }
386    command.push(format!("/Fe{}", link.output));
387    if !link.arguments.is_empty() {
388        command.push("/link".to_owned());
389        command.extend(link.arguments.iter().cloned());
390    }
391    command
392}
393
394// ---------------------------------------------------------------
395// Archive / link dispatch.
396// ---------------------------------------------------------------
397
398fn lower_archive(dialect: Dialect, archive: &ArchiveAction) -> LoweredAction {
399    let command = match dialect {
400        Dialect::GnuLike => lower_archive_gnu(archive),
401        Dialect::Msvc => lower_archive_msvc(archive),
402    };
403    LoweredAction {
404        kind: LoweredActionKind::ArchiveStaticLibrary,
405        inputs: archive.inputs.clone(),
406        implicit_inputs: Vec::new(),
407        outputs: vec![archive.output.clone()],
408        depfile: None,
409        command,
410        description: archive.description.clone(),
411    }
412}
413
414fn lower_link(dialect: Dialect, link: &LinkAction) -> LoweredAction {
415    let command = match dialect {
416        Dialect::GnuLike => lower_link_gnu(link),
417        Dialect::Msvc => lower_link_msvc(link),
418    };
419    LoweredAction {
420        kind: LoweredActionKind::LinkExecutable,
421        inputs: link.inputs.clone(),
422        implicit_inputs: link.implicit_inputs.clone(),
423        outputs: vec![link.output.clone()],
424        depfile: None,
425        command,
426        description: link.description.clone(),
427    }
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433    use crate::action::CompileArguments;
434
435    fn strs(v: &[&str]) -> Vec<String> {
436        v.iter().map(|&s| s.to_string()).collect()
437    }
438
439    /// A C++ compile exercising every argv segment: optimization +
440    /// debug + NDEBUG, depfile, a define, an include dir, a system
441    /// include dir, and an escape-hatch flag.
442    fn cxx_compile(mode: CompileMode) -> CompileAction {
443        CompileAction {
444            standard: LanguageStandard::Cxx(CxxStandard::Cxx17),
445            gnu_extensions: false,
446            source: Utf8PathBuf::from("/abs/src/main.cc"),
447            object: Utf8PathBuf::from("/abs/build/main.o"),
448            mode,
449            implicit_inputs: vec![Utf8PathBuf::from("/abs/build/generated.h")],
450            depfile: Some(Utf8PathBuf::from("/abs/build/main.o.d")),
451            compiler: Utf8PathBuf::from("/usr/bin/g++"),
452            compiler_wrapper: None,
453            arguments: CompileArguments {
454                opt_level: OptLevel::O0,
455                debug_info: false,
456                define_ndebug: false,
457                include_dirs: vec![Utf8PathBuf::from("/abs/include")],
458                system_include_dirs: vec![Utf8PathBuf::from("/abs/dep/include")],
459                defines: strs(&["FOO=1"]),
460                extra_flags: strs(&["-Wall"]),
461            },
462            description: "CXX /abs/build/main.o".to_owned(),
463        }
464    }
465
466    #[test]
467    fn gnu_object_mode_lowers_to_historic_compile_argv() {
468        let lowered = lower(
469            Dialect::GnuLike,
470            &BuildAction::Compile(cxx_compile(CompileMode::Object)),
471        );
472        assert_eq!(lowered.kind, LoweredActionKind::CompileCpp);
473        assert_eq!(
474            lowered.outputs,
475            vec![Utf8PathBuf::from("/abs/build/main.o")]
476        );
477        assert_eq!(lowered.inputs, vec![Utf8PathBuf::from("/abs/src/main.cc")]);
478        assert_eq!(
479            lowered.depfile,
480            Some(Utf8PathBuf::from("/abs/build/main.o.d"))
481        );
482        assert_eq!(
483            lowered.command,
484            strs(&[
485                "/usr/bin/g++",
486                "-std=c++17",
487                "-O0",
488                "-MD",
489                "-MF",
490                "/abs/build/main.o.d",
491                "-DFOO=1",
492                "-I",
493                "/abs/include",
494                "-isystem",
495                "/abs/dep/include",
496                "-Wall",
497                "-c",
498                "/abs/src/main.cc",
499                "-o",
500                "/abs/build/main.o",
501            ])
502        );
503    }
504
505    #[test]
506    fn gnu_dialect_spells_declared_standards() {
507        let mut compile = cxx_compile(CompileMode::Object);
508        compile.standard = LanguageStandard::Cxx(CxxStandard::Cxx20);
509        let lowered = lower(Dialect::GnuLike, &BuildAction::Compile(compile));
510        assert_eq!(lowered.command[1], "-std=c++20");
511
512        let mut compile = cxx_compile(CompileMode::Object);
513        compile.standard = LanguageStandard::C(CStandard::C99);
514        let lowered = lower(Dialect::GnuLike, &BuildAction::Compile(compile));
515        assert_eq!(lowered.kind, LoweredActionKind::CompileC);
516        assert_eq!(lowered.command[1], "-std=c99");
517    }
518
519    #[test]
520    fn gnu_dialect_spells_gnu_extensions_from_the_iso_level() {
521        // `gnu-extensions` swaps the spelling only; the IR still
522        // carries the ISO level.
523        let mut compile = cxx_compile(CompileMode::Object);
524        compile.standard = LanguageStandard::Cxx(CxxStandard::Cxx20);
525        compile.gnu_extensions = true;
526        let lowered = lower(Dialect::GnuLike, &BuildAction::Compile(compile));
527        assert_eq!(lowered.command[1], "-std=gnu++20");
528
529        let mut compile = cxx_compile(CompileMode::Object);
530        compile.standard = LanguageStandard::C(CStandard::C17);
531        compile.gnu_extensions = true;
532        let lowered = lower(Dialect::GnuLike, &BuildAction::Compile(compile));
533        assert_eq!(lowered.kind, LoweredActionKind::CompileC);
534        assert_eq!(lowered.command[1], "-std=gnu17");
535
536        // Every level maps 1:1 onto its GNU spelling.
537        for (standard, flag) in [
538            (LanguageStandard::C(CStandard::C89), "-std=gnu89"),
539            (LanguageStandard::C(CStandard::C23), "-std=gnu23"),
540            (LanguageStandard::Cxx(CxxStandard::Cxx98), "-std=gnu++98"),
541            (LanguageStandard::Cxx(CxxStandard::Cxx26), "-std=gnu++26"),
542        ] {
543            let mut compile = cxx_compile(CompileMode::Object);
544            compile.standard = standard;
545            compile.gnu_extensions = true;
546            let lowered = lower(Dialect::GnuLike, &BuildAction::Compile(compile));
547            assert_eq!(lowered.command[1], flag);
548        }
549    }
550
551    #[test]
552    fn gnu_extensions_is_strictly_per_compile() {
553        // Two compiles in one build may differ in both level and
554        // `gnu-extensions`; lowering reads each action alone.
555        let mut with_extensions = cxx_compile(CompileMode::Object);
556        with_extensions.standard = LanguageStandard::Cxx(CxxStandard::Cxx20);
557        with_extensions.gnu_extensions = true;
558        let mut without = cxx_compile(CompileMode::Object);
559        without.standard = LanguageStandard::Cxx(CxxStandard::Cxx17);
560        let lowered_with = lower(Dialect::GnuLike, &BuildAction::Compile(with_extensions));
561        let lowered_without = lower(Dialect::GnuLike, &BuildAction::Compile(without));
562        assert_eq!(lowered_with.command[1], "-std=gnu++20");
563        assert_eq!(lowered_without.command[1], "-std=c++17");
564    }
565
566    #[test]
567    fn msvc_dialect_spells_declared_standards() {
568        let mut compile = cxx_compile(CompileMode::Object);
569        compile.standard = LanguageStandard::Cxx(CxxStandard::Cxx20);
570        let lowered = lower(Dialect::Msvc, &BuildAction::Compile(compile));
571        // `/std:` sits after `cl` `/nologo` `/utf-8`.
572        assert_eq!(lowered.command[3], "/std:c++20");
573
574        let mut compile = cxx_compile(CompileMode::Object);
575        compile.standard = LanguageStandard::C(CStandard::C17);
576        let lowered = lower(Dialect::Msvc, &BuildAction::Compile(compile));
577        assert_eq!(lowered.kind, LoweredActionKind::CompileC);
578        assert_eq!(lowered.command[3], "/std:c17");
579        // The C compile keeps /EHsc off and forces /Tc.
580        assert!(!lowered.command.iter().any(|a| a == "/EHsc"));
581        assert!(lowered.command.iter().any(|a| a.starts_with("/Tc")));
582    }
583
584    #[test]
585    fn gnu_debug_and_ndebug_flags_follow_std_and_opt() {
586        let mut c = cxx_compile(CompileMode::Object);
587        c.arguments.opt_level = OptLevel::O3;
588        c.arguments.debug_info = true;
589        c.arguments.define_ndebug = true;
590        let lowered = lower(Dialect::GnuLike, &BuildAction::Compile(c));
591        assert_eq!(
592            &lowered.command[1..5],
593            &strs(&["-std=c++17", "-O3", "-g", "-DNDEBUG"])[..]
594        );
595    }
596
597    #[test]
598    fn gnu_syntax_only_mode_lowers_to_historic_check_argv() {
599        let stamp = Utf8PathBuf::from("/abs/build/main.o.check");
600        let lowered = lower(
601            Dialect::GnuLike,
602            &BuildAction::Compile(cxx_compile(CompileMode::SyntaxOnly {
603                stamp: stamp.clone(),
604            })),
605        );
606        assert_eq!(lowered.kind, LoweredActionKind::SyntaxCheckCpp);
607        assert_eq!(lowered.outputs, vec![stamp]);
608        assert_eq!(
609            lowered.command,
610            strs(&[
611                "/usr/bin/g++",
612                "-std=c++17",
613                "-O0",
614                "-MD",
615                "-MF",
616                "/abs/build/main.o.d",
617                "-MT",
618                "/abs/build/main.o.check",
619                "-DFOO=1",
620                "-I",
621                "/abs/include",
622                "-isystem",
623                "/abs/dep/include",
624                "-Wall",
625                "/abs/src/main.cc",
626                "-fsyntax-only",
627            ])
628        );
629    }
630
631    #[test]
632    fn c_compile_lowers_to_c_kinds_and_c_standard() {
633        let mut c = cxx_compile(CompileMode::Object);
634        c.standard = LanguageStandard::C(CStandard::C11);
635        let lowered = lower(Dialect::GnuLike, &BuildAction::Compile(c.clone()));
636        assert_eq!(lowered.kind, LoweredActionKind::CompileC);
637        assert_eq!(lowered.command[1], "-std=c11");
638        c.mode = CompileMode::SyntaxOnly {
639            stamp: Utf8PathBuf::from("/abs/build/main.o.check"),
640        };
641        assert_eq!(
642            lower(Dialect::GnuLike, &BuildAction::Compile(c)).kind,
643            LoweredActionKind::SyntaxCheckC
644        );
645    }
646
647    #[test]
648    fn compiler_wrapper_prefixes_only_the_run_command() {
649        let mut c = cxx_compile(CompileMode::Object);
650        c.compiler_wrapper = Some(Utf8PathBuf::from("/usr/local/bin/ccache"));
651        let lowered = lower(Dialect::GnuLike, &BuildAction::Compile(c.clone()));
652        assert_eq!(lowered.command[0], "/usr/local/bin/ccache");
653        assert_eq!(lowered.command[1], "/usr/bin/g++");
654        // The shared argv builder (used for compile_commands) never
655        // sees the wrapper.
656        let unwrapped = compile_argv(Dialect::GnuLike, &c);
657        assert_eq!(unwrapped[0], "/usr/bin/g++");
658        assert!(!unwrapped.iter().any(|a| a == "/usr/local/bin/ccache"));
659    }
660
661    #[test]
662    fn gnu_archive_lowers_to_ar_crs_command() {
663        let action = BuildAction::Archive(ArchiveAction {
664            archiver: Utf8PathBuf::from("/usr/bin/ar"),
665            output: Utf8PathBuf::from("/abs/build/libfoo.a"),
666            inputs: vec![
667                Utf8PathBuf::from("/abs/build/a.o"),
668                Utf8PathBuf::from("/abs/build/b.o"),
669            ],
670            description: "AR /abs/build/libfoo.a".to_owned(),
671        });
672        let lowered = lower(Dialect::GnuLike, &action);
673        assert_eq!(lowered.kind, LoweredActionKind::ArchiveStaticLibrary);
674        assert_eq!(
675            lowered.command,
676            strs(&[
677                "/usr/bin/ar",
678                "crs",
679                "/abs/build/libfoo.a",
680                "/abs/build/a.o",
681                "/abs/build/b.o",
682            ])
683        );
684    }
685
686    #[test]
687    fn gnu_link_lowers_to_driver_inputs_ldflags_output() {
688        let action = BuildAction::Link(LinkAction {
689            linker: Utf8PathBuf::from("/usr/bin/g++"),
690            output: Utf8PathBuf::from("/abs/build/app"),
691            inputs: vec![
692                Utf8PathBuf::from("/abs/build/main.o"),
693                Utf8PathBuf::from("/abs/build/libfoo.a"),
694            ],
695            implicit_inputs: vec![],
696            arguments: strs(&["-Wl,--as-needed"]),
697            link_libs: vec![],
698            description: "LINK /abs/build/app".to_owned(),
699        });
700        let lowered = lower(Dialect::GnuLike, &action);
701        assert_eq!(lowered.kind, LoweredActionKind::LinkExecutable);
702        assert_eq!(
703            lowered.command,
704            strs(&[
705                "/usr/bin/g++",
706                "/abs/build/main.o",
707                "/abs/build/libfoo.a",
708                "-Wl,--as-needed",
709                "-o",
710                "/abs/build/app",
711            ])
712        );
713    }
714
715    // -----------------------------------------------------------
716    // MSVC dialect.
717    // -----------------------------------------------------------
718
719    /// The MSVC analogue of [`cxx_compile`], with MSVC-spelled
720    /// escape-hatch flags and `cl` paths.
721    fn msvc_cxx_compile(mode: CompileMode) -> CompileAction {
722        CompileAction {
723            standard: LanguageStandard::Cxx(CxxStandard::Cxx17),
724            gnu_extensions: false,
725            source: Utf8PathBuf::from("C:/src/main.cc"),
726            object: Utf8PathBuf::from("C:/build/main.obj"),
727            mode,
728            implicit_inputs: vec![],
729            depfile: Some(Utf8PathBuf::from("C:/build/main.obj.d")),
730            compiler: Utf8PathBuf::from("cl.exe"),
731            compiler_wrapper: None,
732            arguments: CompileArguments {
733                opt_level: OptLevel::O2,
734                debug_info: true,
735                define_ndebug: true,
736                include_dirs: vec![Utf8PathBuf::from("C:/include")],
737                system_include_dirs: vec![Utf8PathBuf::from("C:/dep/include")],
738                defines: strs(&["FOO=1"]),
739                extra_flags: strs(&["/W4"]),
740            },
741            description: "CXX C:/build/main.obj".to_owned(),
742        }
743    }
744
745    #[test]
746    fn msvc_object_mode_lowers_to_cl_argv_without_depfile() {
747        let lowered = lower(
748            Dialect::Msvc,
749            &BuildAction::Compile(msvc_cxx_compile(CompileMode::Object)),
750        );
751        assert_eq!(lowered.kind, LoweredActionKind::CompileCpp);
752        // MSVC tracks headers via `/showIncludes`, so no Makefile
753        // depfile is carried on the lowered action.
754        assert_eq!(lowered.depfile, None);
755        assert_eq!(
756            lowered.command,
757            strs(&[
758                "cl.exe",
759                "/nologo",
760                "/utf-8",
761                "/std:c++17",
762                "/EHsc",
763                "/O2",
764                "/Z7",
765                "/DNDEBUG",
766                "/showIncludes",
767                "/DFOO=1",
768                "/I",
769                "C:/include",
770                "/external:W0",
771                "/external:I",
772                "C:/dep/include",
773                "/W4",
774                "/c",
775                "/TpC:/src/main.cc",
776                "/FoC:/build/main.obj",
777            ])
778        );
779    }
780
781    #[test]
782    fn system_include_markers_are_omitted_when_no_system_dirs_exist() {
783        // `/external:W0` must not appear on a command with no external
784        // include block, and the GNU spelling must not emit a stray
785        // `-isystem`.
786        let mut gnu = cxx_compile(CompileMode::Object);
787        gnu.arguments.system_include_dirs = Vec::new();
788        let gnu_argv = compile_argv(Dialect::GnuLike, &gnu);
789        assert!(!gnu_argv.iter().any(|a| a == "-isystem"));
790
791        let mut msvc = msvc_cxx_compile(CompileMode::Object);
792        msvc.arguments.system_include_dirs = Vec::new();
793        let msvc_argv = compile_argv(Dialect::Msvc, &msvc);
794        assert!(!msvc_argv.iter().any(|a| a.starts_with("/external:")));
795    }
796
797    #[test]
798    fn msvc_syntax_only_uses_zs_and_no_output() {
799        let stamp = Utf8PathBuf::from("C:/build/main.obj.check");
800        let lowered = lower(
801            Dialect::Msvc,
802            &BuildAction::Compile(msvc_cxx_compile(CompileMode::SyntaxOnly {
803                stamp: stamp.clone(),
804            })),
805        );
806        assert_eq!(lowered.kind, LoweredActionKind::SyntaxCheckCpp);
807        assert_eq!(lowered.outputs, vec![stamp]);
808        let tail = &lowered.command[lowered.command.len() - 2..];
809        assert_eq!(tail, &strs(&["/TpC:/src/main.cc", "/Zs"])[..]);
810        assert!(!lowered.command.iter().any(|a| a.starts_with("/Fo")));
811    }
812
813    #[test]
814    fn msvc_c_compile_uses_c_standard_and_no_ehsc() {
815        let mut c = msvc_cxx_compile(CompileMode::Object);
816        c.standard = LanguageStandard::C(CStandard::C11);
817        let lowered = lower(Dialect::Msvc, &BuildAction::Compile(c));
818        assert_eq!(lowered.kind, LoweredActionKind::CompileC);
819        assert!(lowered.command.iter().any(|a| a == "/std:c11"));
820        assert!(!lowered.command.iter().any(|a| a == "/EHsc"));
821        // The UTF-8 charset pin is language-independent.
822        assert!(lowered.command.iter().any(|a| a == "/utf-8"));
823    }
824
825    #[test]
826    fn msvc_forces_source_language_per_classification() {
827        // `cl` only defaults `.cpp`/`.cxx` to C++; Cabin drives the
828        // language explicitly so any supported extension compiles as the
829        // language Cabin classified it.  The source token carries `/Tp`
830        // for C++ and `/Tc` for C, with no bare source argument.
831        let cxx = lower(
832            Dialect::Msvc,
833            &BuildAction::Compile(msvc_cxx_compile(CompileMode::Object)),
834        );
835        assert!(cxx.command.iter().any(|a| a == "/TpC:/src/main.cc"));
836        assert!(!cxx.command.iter().any(|a| a == "C:/src/main.cc"));
837
838        let mut c_action = msvc_cxx_compile(CompileMode::Object);
839        c_action.standard = LanguageStandard::C(CStandard::C11);
840        let c = lower(Dialect::Msvc, &BuildAction::Compile(c_action));
841        assert!(c.command.iter().any(|a| a == "/TcC:/src/main.cc"));
842        assert!(!c.command.iter().any(|a| a == "/TpC:/src/main.cc"));
843    }
844
845    #[test]
846    fn msvc_debug_off_omits_z7_and_opt_maps_o3_to_o2() {
847        let mut c = msvc_cxx_compile(CompileMode::Object);
848        c.arguments.debug_info = false;
849        c.arguments.define_ndebug = false;
850        c.arguments.opt_level = OptLevel::O3;
851        let lowered = lower(Dialect::Msvc, &BuildAction::Compile(c));
852        assert!(!lowered.command.iter().any(|a| a == "/Z7"));
853        assert!(!lowered.command.iter().any(|a| a == "/DNDEBUG"));
854        assert!(lowered.command.iter().any(|a| a == "/O2"));
855    }
856
857    #[test]
858    fn msvc_archive_lowers_to_lib_out_command() {
859        let action = BuildAction::Archive(ArchiveAction {
860            archiver: Utf8PathBuf::from("lib.exe"),
861            output: Utf8PathBuf::from("C:/build/foo.lib"),
862            inputs: vec![
863                Utf8PathBuf::from("C:/build/a.obj"),
864                Utf8PathBuf::from("C:/build/b.obj"),
865            ],
866            description: "AR C:/build/foo.lib".to_owned(),
867        });
868        let lowered = lower(Dialect::Msvc, &action);
869        assert_eq!(
870            lowered.command,
871            strs(&[
872                "lib.exe",
873                "/nologo",
874                "/OUT:C:/build/foo.lib",
875                "C:/build/a.obj",
876                "C:/build/b.obj",
877            ])
878        );
879    }
880
881    #[test]
882    fn msvc_link_lowers_to_cl_fe_with_link_options() {
883        let action = BuildAction::Link(LinkAction {
884            linker: Utf8PathBuf::from("cl.exe"),
885            output: Utf8PathBuf::from("C:/build/app.exe"),
886            inputs: vec![
887                Utf8PathBuf::from("C:/build/main.obj"),
888                Utf8PathBuf::from("C:/build/foo.lib"),
889            ],
890            implicit_inputs: vec![],
891            arguments: strs(&["/SUBSYSTEM:CONSOLE"]),
892            link_libs: vec![],
893            description: "LINK C:/build/app.exe".to_owned(),
894        });
895        let lowered = lower(Dialect::Msvc, &action);
896        assert_eq!(
897            lowered.command,
898            strs(&[
899                "cl.exe",
900                "/nologo",
901                "C:/build/main.obj",
902                "C:/build/foo.lib",
903                "/FeC:/build/app.exe",
904                "/link",
905                "/SUBSYSTEM:CONSOLE",
906            ])
907        );
908    }
909
910    #[test]
911    fn msvc_link_without_ldflags_omits_link_separator() {
912        let action = BuildAction::Link(LinkAction {
913            linker: Utf8PathBuf::from("cl.exe"),
914            output: Utf8PathBuf::from("C:/build/app.exe"),
915            inputs: vec![Utf8PathBuf::from("C:/build/main.obj")],
916            implicit_inputs: vec![],
917            arguments: vec![],
918            link_libs: vec![],
919            description: "LINK C:/build/app.exe".to_owned(),
920        });
921        let lowered = lower(Dialect::Msvc, &action);
922        assert_eq!(
923            lowered.command,
924            strs(&[
925                "cl.exe",
926                "/nologo",
927                "C:/build/main.obj",
928                "/FeC:/build/app.exe",
929            ])
930        );
931    }
932
933    #[test]
934    fn gnu_link_lowers_link_libs_after_archives() {
935        // System libraries are spelled `-l<name>` and placed after the
936        // archive inputs and ldflags so GNU `ld` resolves them
937        // left-to-right against the archives that reference them.
938        let action = BuildAction::Link(LinkAction {
939            linker: Utf8PathBuf::from("/usr/bin/cc"),
940            output: Utf8PathBuf::from("/abs/build/app"),
941            inputs: vec![
942                Utf8PathBuf::from("/abs/build/main.o"),
943                Utf8PathBuf::from("/abs/build/libsqlite3.a"),
944            ],
945            implicit_inputs: vec![],
946            arguments: vec![],
947            link_libs: strs(&["pthread", "m"]),
948            description: "LINK /abs/build/app".to_owned(),
949        });
950        let lowered = lower(Dialect::GnuLike, &action);
951        assert_eq!(
952            lowered.command,
953            strs(&[
954                "/usr/bin/cc",
955                "/abs/build/main.o",
956                "/abs/build/libsqlite3.a",
957                "-lpthread",
958                "-lm",
959                "-o",
960                "/abs/build/app",
961            ])
962        );
963    }
964
965    #[test]
966    fn msvc_link_lowers_link_libs_as_dot_lib_inputs() {
967        // MSVC `link` has no `-l<name>`; system libraries are spelled
968        // `<name>.lib` and passed as positional inputs (after the
969        // archives, before `/Fe`), not GNU-style flags.
970        let action = BuildAction::Link(LinkAction {
971            linker: Utf8PathBuf::from("cl.exe"),
972            output: Utf8PathBuf::from("C:/build/app.exe"),
973            inputs: vec![Utf8PathBuf::from("C:/build/main.obj")],
974            implicit_inputs: vec![],
975            arguments: vec![],
976            link_libs: strs(&["user32"]),
977            description: "LINK C:/build/app.exe".to_owned(),
978        });
979        let lowered = lower(Dialect::Msvc, &action);
980        assert_eq!(
981            lowered.command,
982            strs(&[
983                "cl.exe",
984                "/nologo",
985                "C:/build/main.obj",
986                "user32.lib",
987                "/FeC:/build/app.exe",
988            ])
989        );
990    }
991}