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-cache 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-cache 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.as_str().to_owned());
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) -> String {
138    format!("-std={standard}")
139}
140
141/// GNU/Clang compile argv. The layout is fixed so it reproduces the
142/// historic command lines byte-for-byte: driver, standard, profile
143/// (`-O<n>` / `-g` / `-DNDEBUG`), the `-MD -MF <depfile>` (plus
144/// `-MT <stamp>` in syntax-only mode) dependency block, defines,
145/// includes, system includes, escape-hatch flags, and the
146/// mode-specific tail.
147fn compile_argv_gnu(compile: &CompileAction) -> Vec<String> {
148    let args = &compile.arguments;
149    let mut out: Vec<String> = Vec::new();
150    out.push(compile.compiler.as_str().to_owned());
151    out.push(gnu_std_flag(compile.standard));
152    out.push(args.opt_level.as_flag().to_owned());
153    if args.debug_info {
154        out.push("-g".to_owned());
155    }
156    if args.define_ndebug {
157        out.push("-DNDEBUG".to_owned());
158    }
159    if let Some(depfile) = &compile.depfile {
160        // `-MD`, not `-MMD`: `-MMD` omits headers found through
161        // system include dirs, so an edit under an `-isystem` path
162        // (a foundation port, an extracted registry package, a
163        // pkg-config dir) would stop invalidating rebuilds.
164        out.push("-MD".to_owned());
165        out.push("-MF".to_owned());
166        out.push(depfile.as_str().to_owned());
167        // In syntax-only mode the depfile records the stamp (not an
168        // object) as its target, so header edits still invalidate the
169        // check via Ninja's `deps = gcc` machinery.
170        if let CompileMode::SyntaxOnly { stamp } = &compile.mode {
171            out.push("-MT".to_owned());
172            out.push(stamp.as_str().to_owned());
173        }
174    }
175    for define in &args.defines {
176        out.push(format!("-D{define}"));
177    }
178    for include in &args.include_dirs {
179        out.push("-I".to_owned());
180        out.push(include.as_str().to_owned());
181    }
182    // System include dirs come after the user dirs: `-isystem`
183    // paths are searched after every `-I` path, so spelling them
184    // last keeps argv order aligned with the actual search order.
185    for include in &args.system_include_dirs {
186        out.push("-isystem".to_owned());
187        out.push(include.as_str().to_owned());
188    }
189    out.extend(args.extra_flags.iter().cloned());
190    match &compile.mode {
191        CompileMode::Object => {
192            out.push("-c".to_owned());
193            out.push(compile.source.as_str().to_owned());
194            out.push("-o".to_owned());
195            out.push(compile.object.as_str().to_owned());
196        }
197        CompileMode::SyntaxOnly { .. } => {
198            out.push(compile.source.as_str().to_owned());
199            out.push("-fsyntax-only".to_owned());
200        }
201    }
202    out
203}
204
205fn lower_archive_gnu(archive: &ArchiveAction) -> Vec<String> {
206    // GNU `ar` archives with the `crs` mode flags (create, replace,
207    // write index): `ar crs <lib> <obj>...`.
208    let mut command = vec![
209        archive.archiver.as_str().to_owned(),
210        "crs".to_owned(),
211        archive.output.as_str().to_owned(),
212    ];
213    for input in &archive.inputs {
214        command.push(input.as_str().to_owned());
215    }
216    command
217}
218
219fn lower_link_gnu(link: &LinkAction) -> Vec<String> {
220    // `<driver> <inputs...> <ldflags...> -l<lib>... -o <exe>`.
221    // System libraries follow the archives so a static library's
222    // dependencies resolve left-to-right under GNU `ld`.
223    let mut command = vec![link.linker.as_str().to_owned()];
224    for input in &link.inputs {
225        command.push(input.as_str().to_owned());
226    }
227    command.extend(link.arguments.iter().cloned());
228    for lib in &link.link_libs {
229        command.push(format!("-l{lib}"));
230    }
231    command.push("-o".to_owned());
232    command.push(link.output.as_str().to_owned());
233    command
234}
235
236// ---------------------------------------------------------------
237// MSVC (`cl.exe` / `lib.exe`) dialect.
238// ---------------------------------------------------------------
239
240fn msvc_std_flag(standard: LanguageStandard) -> &'static str {
241    standard.msvc_spelling().unwrap_or_else(|| {
242        unreachable!(
243            "the planner validates MSVC-dialect standards before lowering; `{standard}` has no stable /std: flag"
244        )
245    })
246}
247
248/// The `cl.exe` flag that forces the source's language, prepended to the
249/// file name (`/Tp<file>` for C++, `/Tc<file>` for C).
250///
251/// `cl` infers language from extension and only defaults `.cpp` / `.cxx`
252/// to C++; Cabin also classifies `.cc`, `.c++`, and `.C` as C++ (and
253/// `.c` as C). Driving the language explicitly makes the translation
254/// unit follow Cabin's own source classification rather than `cl`'s
255/// extension table, so every supported extension compiles as the
256/// language Cabin intends.
257fn msvc_source_flag(language: SourceLanguage) -> &'static str {
258    match language {
259        SourceLanguage::C => "/Tc",
260        SourceLanguage::Cxx => "/Tp",
261    }
262}
263
264fn msvc_opt_flag(opt: OptLevel) -> &'static str {
265    match opt {
266        OptLevel::O0 => "/Od",
267        OptLevel::O1 | OptLevel::S | OptLevel::Z => "/O1",
268        // cl.exe has no `/O3`; `/O2` is its maximum speed setting.
269        OptLevel::O2 | OptLevel::O3 => "/O2",
270    }
271}
272
273/// MSVC `cl.exe` compile argv. Mirrors the GNU layout with MSVC
274/// spellings: `/std:` standard, `/EHsc` for C++ exceptions, `/O`
275/// optimization, `/Z7` debug info (embedded in the object so parallel
276/// compiles never contend on a shared PDB), `/showIncludes` for
277/// dependency discovery (no Makefile depfile), `/D` defines, `/I`
278/// includes, escape-hatch flags, and the mode-specific tail
279/// (`/c /Tp<src> /Fo<obj>` or `/Tp<src> /Zs`, with `/Tc` for C).
280fn compile_argv_msvc(compile: &CompileAction) -> Vec<String> {
281    let args = &compile.arguments;
282    let mut out: Vec<String> = vec![compile.compiler.as_str().to_owned(), "/nologo".to_owned()];
283    out.push(msvc_std_flag(compile.standard).to_owned());
284    if compile.standard.language() == SourceLanguage::Cxx {
285        out.push("/EHsc".to_owned());
286    }
287    out.push(msvc_opt_flag(args.opt_level).to_owned());
288    if args.debug_info {
289        out.push("/Z7".to_owned());
290    }
291    if args.define_ndebug {
292        out.push("/DNDEBUG".to_owned());
293    }
294    // `/showIncludes` drives Ninja's `deps = msvc`. Emitted whenever
295    // the planner asked for dependency tracking, matching the GNU
296    // dialect's `-MD -MF` condition.
297    if compile.depfile.is_some() {
298        out.push("/showIncludes".to_owned());
299    }
300    for define in &args.defines {
301        out.push(format!("/D{define}"));
302    }
303    for include in &args.include_dirs {
304        out.push("/I".to_owned());
305        out.push(include.as_str().to_owned());
306    }
307    // `/external:I` marks the directory as external; `/external:W0`
308    // (emitted once, ahead of the block) silences warnings inside
309    // those headers, matching the GNU dialect's `-isystem`
310    // semantics. The planner only populates the system bucket when
311    // the detected `cl` / `clang-cl` understands `/external:`.
312    if !args.system_include_dirs.is_empty() {
313        out.push("/external:W0".to_owned());
314    }
315    for include in &args.system_include_dirs {
316        out.push("/external:I".to_owned());
317        out.push(include.as_str().to_owned());
318    }
319    out.extend(args.extra_flags.iter().cloned());
320    let source = format!(
321        "{}{}",
322        msvc_source_flag(compile.standard.language()),
323        compile.source.as_str()
324    );
325    match &compile.mode {
326        CompileMode::Object => {
327            out.push("/c".to_owned());
328            out.push(source);
329            out.push(format!("/Fo{}", compile.object));
330        }
331        CompileMode::SyntaxOnly { .. } => {
332            out.push(source);
333            out.push("/Zs".to_owned());
334        }
335    }
336    out
337}
338
339fn lower_archive_msvc(archive: &ArchiveAction) -> Vec<String> {
340    // `lib /nologo /OUT:<lib> <obj>...`.
341    let mut command = vec![
342        archive.archiver.as_str().to_owned(),
343        "/nologo".to_owned(),
344        format!("/OUT:{}", archive.output),
345    ];
346    for input in &archive.inputs {
347        command.push(input.as_str().to_owned());
348    }
349    command
350}
351
352fn lower_link_msvc(link: &LinkAction) -> Vec<String> {
353    // `<driver> /nologo <inputs...> <lib>.lib... /Fe<exe> [/link <ldflags...>]`.
354    // cl.exe consumes object and `.lib` inputs positionally and
355    // forwards `/link` options to the linker, so system libraries are
356    // spelled `<name>.lib` and passed as positional inputs after the
357    // archives rather than as GNU `-l<name>` flags.
358    let mut command = vec![link.linker.as_str().to_owned(), "/nologo".to_owned()];
359    for input in &link.inputs {
360        command.push(input.as_str().to_owned());
361    }
362    for lib in &link.link_libs {
363        command.push(format!("{lib}.lib"));
364    }
365    command.push(format!("/Fe{}", link.output));
366    if !link.arguments.is_empty() {
367        command.push("/link".to_owned());
368        command.extend(link.arguments.iter().cloned());
369    }
370    command
371}
372
373// ---------------------------------------------------------------
374// Archive / link dispatch.
375// ---------------------------------------------------------------
376
377fn lower_archive(dialect: Dialect, archive: &ArchiveAction) -> LoweredAction {
378    let command = match dialect {
379        Dialect::GnuLike => lower_archive_gnu(archive),
380        Dialect::Msvc => lower_archive_msvc(archive),
381    };
382    LoweredAction {
383        kind: LoweredActionKind::ArchiveStaticLibrary,
384        inputs: archive.inputs.clone(),
385        implicit_inputs: Vec::new(),
386        outputs: vec![archive.output.clone()],
387        depfile: None,
388        command,
389        description: archive.description.clone(),
390    }
391}
392
393fn lower_link(dialect: Dialect, link: &LinkAction) -> LoweredAction {
394    let command = match dialect {
395        Dialect::GnuLike => lower_link_gnu(link),
396        Dialect::Msvc => lower_link_msvc(link),
397    };
398    LoweredAction {
399        kind: LoweredActionKind::LinkExecutable,
400        inputs: link.inputs.clone(),
401        implicit_inputs: link.implicit_inputs.clone(),
402        outputs: vec![link.output.clone()],
403        depfile: None,
404        command,
405        description: link.description.clone(),
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412    use crate::action::CompileArguments;
413
414    fn strs(v: &[&str]) -> Vec<String> {
415        v.iter().map(|s| (*s).to_owned()).collect()
416    }
417
418    /// A C++ compile exercising every argv segment: optimization +
419    /// debug + NDEBUG, depfile, a define, an include dir, a system
420    /// include dir, and an escape-hatch flag.
421    fn cxx_compile(mode: CompileMode) -> CompileAction {
422        CompileAction {
423            standard: LanguageStandard::Cxx(CxxStandard::Cxx17),
424            source: Utf8PathBuf::from("/abs/src/main.cc"),
425            object: Utf8PathBuf::from("/abs/build/main.o"),
426            mode,
427            implicit_inputs: vec![Utf8PathBuf::from("/abs/build/generated.h")],
428            depfile: Some(Utf8PathBuf::from("/abs/build/main.o.d")),
429            compiler: Utf8PathBuf::from("/usr/bin/g++"),
430            compiler_wrapper: None,
431            arguments: CompileArguments {
432                opt_level: OptLevel::O0,
433                debug_info: false,
434                define_ndebug: false,
435                include_dirs: vec![Utf8PathBuf::from("/abs/include")],
436                system_include_dirs: vec![Utf8PathBuf::from("/abs/dep/include")],
437                defines: strs(&["FOO=1"]),
438                extra_flags: strs(&["-Wall"]),
439            },
440            description: "CXX /abs/build/main.o".to_owned(),
441        }
442    }
443
444    #[test]
445    fn gnu_object_mode_lowers_to_historic_compile_argv() {
446        let lowered = lower(
447            Dialect::GnuLike,
448            &BuildAction::Compile(cxx_compile(CompileMode::Object)),
449        );
450        assert_eq!(lowered.kind, LoweredActionKind::CompileCpp);
451        assert_eq!(
452            lowered.outputs,
453            vec![Utf8PathBuf::from("/abs/build/main.o")]
454        );
455        assert_eq!(lowered.inputs, vec![Utf8PathBuf::from("/abs/src/main.cc")]);
456        assert_eq!(
457            lowered.depfile,
458            Some(Utf8PathBuf::from("/abs/build/main.o.d"))
459        );
460        assert_eq!(
461            lowered.command,
462            strs(&[
463                "/usr/bin/g++",
464                "-std=c++17",
465                "-O0",
466                "-MD",
467                "-MF",
468                "/abs/build/main.o.d",
469                "-DFOO=1",
470                "-I",
471                "/abs/include",
472                "-isystem",
473                "/abs/dep/include",
474                "-Wall",
475                "-c",
476                "/abs/src/main.cc",
477                "-o",
478                "/abs/build/main.o",
479            ])
480        );
481    }
482
483    #[test]
484    fn gnu_dialect_spells_declared_standards() {
485        let mut compile = cxx_compile(CompileMode::Object);
486        compile.standard = LanguageStandard::Cxx(CxxStandard::Cxx20);
487        let lowered = lower(Dialect::GnuLike, &BuildAction::Compile(compile));
488        assert_eq!(lowered.command[1], "-std=c++20");
489
490        let mut compile = cxx_compile(CompileMode::Object);
491        compile.standard = LanguageStandard::C(CStandard::C99);
492        let lowered = lower(Dialect::GnuLike, &BuildAction::Compile(compile));
493        assert_eq!(lowered.kind, LoweredActionKind::CompileC);
494        assert_eq!(lowered.command[1], "-std=c99");
495    }
496
497    #[test]
498    fn msvc_dialect_spells_declared_standards() {
499        let mut compile = cxx_compile(CompileMode::Object);
500        compile.standard = LanguageStandard::Cxx(CxxStandard::Cxx20);
501        let lowered = lower(Dialect::Msvc, &BuildAction::Compile(compile));
502        assert_eq!(lowered.command[2], "/std:c++20");
503
504        let mut compile = cxx_compile(CompileMode::Object);
505        compile.standard = LanguageStandard::C(CStandard::C17);
506        let lowered = lower(Dialect::Msvc, &BuildAction::Compile(compile));
507        assert_eq!(lowered.kind, LoweredActionKind::CompileC);
508        assert_eq!(lowered.command[2], "/std:c17");
509        // The C compile keeps /EHsc off and forces /Tc.
510        assert!(!lowered.command.iter().any(|a| a == "/EHsc"));
511        assert!(lowered.command.iter().any(|a| a.starts_with("/Tc")));
512    }
513
514    #[test]
515    fn gnu_debug_and_ndebug_flags_follow_std_and_opt() {
516        let mut c = cxx_compile(CompileMode::Object);
517        c.arguments.opt_level = OptLevel::O3;
518        c.arguments.debug_info = true;
519        c.arguments.define_ndebug = true;
520        let lowered = lower(Dialect::GnuLike, &BuildAction::Compile(c));
521        assert_eq!(
522            &lowered.command[1..5],
523            &strs(&["-std=c++17", "-O3", "-g", "-DNDEBUG"])[..]
524        );
525    }
526
527    #[test]
528    fn gnu_syntax_only_mode_lowers_to_historic_check_argv() {
529        let stamp = Utf8PathBuf::from("/abs/build/main.o.check");
530        let lowered = lower(
531            Dialect::GnuLike,
532            &BuildAction::Compile(cxx_compile(CompileMode::SyntaxOnly {
533                stamp: stamp.clone(),
534            })),
535        );
536        assert_eq!(lowered.kind, LoweredActionKind::SyntaxCheckCpp);
537        assert_eq!(lowered.outputs, vec![stamp]);
538        assert_eq!(
539            lowered.command,
540            strs(&[
541                "/usr/bin/g++",
542                "-std=c++17",
543                "-O0",
544                "-MD",
545                "-MF",
546                "/abs/build/main.o.d",
547                "-MT",
548                "/abs/build/main.o.check",
549                "-DFOO=1",
550                "-I",
551                "/abs/include",
552                "-isystem",
553                "/abs/dep/include",
554                "-Wall",
555                "/abs/src/main.cc",
556                "-fsyntax-only",
557            ])
558        );
559    }
560
561    #[test]
562    fn c_compile_lowers_to_c_kinds_and_c_standard() {
563        let mut c = cxx_compile(CompileMode::Object);
564        c.standard = LanguageStandard::C(CStandard::C11);
565        let lowered = lower(Dialect::GnuLike, &BuildAction::Compile(c.clone()));
566        assert_eq!(lowered.kind, LoweredActionKind::CompileC);
567        assert_eq!(lowered.command[1], "-std=c11");
568        c.mode = CompileMode::SyntaxOnly {
569            stamp: Utf8PathBuf::from("/abs/build/main.o.check"),
570        };
571        assert_eq!(
572            lower(Dialect::GnuLike, &BuildAction::Compile(c)).kind,
573            LoweredActionKind::SyntaxCheckC
574        );
575    }
576
577    #[test]
578    fn compiler_wrapper_prefixes_only_the_run_command() {
579        let mut c = cxx_compile(CompileMode::Object);
580        c.compiler_wrapper = Some(Utf8PathBuf::from("/usr/local/bin/ccache"));
581        let lowered = lower(Dialect::GnuLike, &BuildAction::Compile(c.clone()));
582        assert_eq!(lowered.command[0], "/usr/local/bin/ccache");
583        assert_eq!(lowered.command[1], "/usr/bin/g++");
584        // The shared argv builder (used for compile_commands) never
585        // sees the wrapper.
586        let unwrapped = compile_argv(Dialect::GnuLike, &c);
587        assert_eq!(unwrapped[0], "/usr/bin/g++");
588        assert!(!unwrapped.iter().any(|a| a == "/usr/local/bin/ccache"));
589    }
590
591    #[test]
592    fn gnu_archive_lowers_to_ar_crs_command() {
593        let action = BuildAction::Archive(ArchiveAction {
594            archiver: Utf8PathBuf::from("/usr/bin/ar"),
595            output: Utf8PathBuf::from("/abs/build/libfoo.a"),
596            inputs: vec![
597                Utf8PathBuf::from("/abs/build/a.o"),
598                Utf8PathBuf::from("/abs/build/b.o"),
599            ],
600            description: "AR /abs/build/libfoo.a".to_owned(),
601        });
602        let lowered = lower(Dialect::GnuLike, &action);
603        assert_eq!(lowered.kind, LoweredActionKind::ArchiveStaticLibrary);
604        assert_eq!(
605            lowered.command,
606            strs(&[
607                "/usr/bin/ar",
608                "crs",
609                "/abs/build/libfoo.a",
610                "/abs/build/a.o",
611                "/abs/build/b.o",
612            ])
613        );
614    }
615
616    #[test]
617    fn gnu_link_lowers_to_driver_inputs_ldflags_output() {
618        let action = BuildAction::Link(LinkAction {
619            linker: Utf8PathBuf::from("/usr/bin/g++"),
620            output: Utf8PathBuf::from("/abs/build/app"),
621            inputs: vec![
622                Utf8PathBuf::from("/abs/build/main.o"),
623                Utf8PathBuf::from("/abs/build/libfoo.a"),
624            ],
625            implicit_inputs: vec![],
626            arguments: strs(&["-Wl,--as-needed"]),
627            link_libs: vec![],
628            description: "LINK /abs/build/app".to_owned(),
629        });
630        let lowered = lower(Dialect::GnuLike, &action);
631        assert_eq!(lowered.kind, LoweredActionKind::LinkExecutable);
632        assert_eq!(
633            lowered.command,
634            strs(&[
635                "/usr/bin/g++",
636                "/abs/build/main.o",
637                "/abs/build/libfoo.a",
638                "-Wl,--as-needed",
639                "-o",
640                "/abs/build/app",
641            ])
642        );
643    }
644
645    // -----------------------------------------------------------
646    // MSVC dialect.
647    // -----------------------------------------------------------
648
649    /// The MSVC analogue of [`cxx_compile`], with MSVC-spelled
650    /// escape-hatch flags and `cl` paths.
651    fn msvc_cxx_compile(mode: CompileMode) -> CompileAction {
652        CompileAction {
653            standard: LanguageStandard::Cxx(CxxStandard::Cxx17),
654            source: Utf8PathBuf::from("C:/src/main.cc"),
655            object: Utf8PathBuf::from("C:/build/main.obj"),
656            mode,
657            implicit_inputs: vec![],
658            depfile: Some(Utf8PathBuf::from("C:/build/main.obj.d")),
659            compiler: Utf8PathBuf::from("cl.exe"),
660            compiler_wrapper: None,
661            arguments: CompileArguments {
662                opt_level: OptLevel::O2,
663                debug_info: true,
664                define_ndebug: true,
665                include_dirs: vec![Utf8PathBuf::from("C:/include")],
666                system_include_dirs: vec![Utf8PathBuf::from("C:/dep/include")],
667                defines: strs(&["FOO=1"]),
668                extra_flags: strs(&["/W4"]),
669            },
670            description: "CXX C:/build/main.obj".to_owned(),
671        }
672    }
673
674    #[test]
675    fn msvc_object_mode_lowers_to_cl_argv_without_depfile() {
676        let lowered = lower(
677            Dialect::Msvc,
678            &BuildAction::Compile(msvc_cxx_compile(CompileMode::Object)),
679        );
680        assert_eq!(lowered.kind, LoweredActionKind::CompileCpp);
681        // MSVC tracks headers via `/showIncludes`, so no Makefile
682        // depfile is carried on the lowered action.
683        assert_eq!(lowered.depfile, None);
684        assert_eq!(
685            lowered.command,
686            strs(&[
687                "cl.exe",
688                "/nologo",
689                "/std:c++17",
690                "/EHsc",
691                "/O2",
692                "/Z7",
693                "/DNDEBUG",
694                "/showIncludes",
695                "/DFOO=1",
696                "/I",
697                "C:/include",
698                "/external:W0",
699                "/external:I",
700                "C:/dep/include",
701                "/W4",
702                "/c",
703                "/TpC:/src/main.cc",
704                "/FoC:/build/main.obj",
705            ])
706        );
707    }
708
709    #[test]
710    fn system_include_markers_are_omitted_when_no_system_dirs_exist() {
711        // `/external:W0` must not appear on a command with no external
712        // include block, and the GNU spelling must not emit a stray
713        // `-isystem`.
714        let mut gnu = cxx_compile(CompileMode::Object);
715        gnu.arguments.system_include_dirs = Vec::new();
716        let gnu_argv = compile_argv(Dialect::GnuLike, &gnu);
717        assert!(!gnu_argv.iter().any(|a| a == "-isystem"));
718
719        let mut msvc = msvc_cxx_compile(CompileMode::Object);
720        msvc.arguments.system_include_dirs = Vec::new();
721        let msvc_argv = compile_argv(Dialect::Msvc, &msvc);
722        assert!(!msvc_argv.iter().any(|a| a.starts_with("/external:")));
723    }
724
725    #[test]
726    fn msvc_syntax_only_uses_zs_and_no_output() {
727        let stamp = Utf8PathBuf::from("C:/build/main.obj.check");
728        let lowered = lower(
729            Dialect::Msvc,
730            &BuildAction::Compile(msvc_cxx_compile(CompileMode::SyntaxOnly {
731                stamp: stamp.clone(),
732            })),
733        );
734        assert_eq!(lowered.kind, LoweredActionKind::SyntaxCheckCpp);
735        assert_eq!(lowered.outputs, vec![stamp]);
736        let tail = &lowered.command[lowered.command.len() - 2..];
737        assert_eq!(tail, &strs(&["/TpC:/src/main.cc", "/Zs"])[..]);
738        assert!(!lowered.command.iter().any(|a| a.starts_with("/Fo")));
739    }
740
741    #[test]
742    fn msvc_c_compile_uses_c_standard_and_no_ehsc() {
743        let mut c = msvc_cxx_compile(CompileMode::Object);
744        c.standard = LanguageStandard::C(CStandard::C11);
745        let lowered = lower(Dialect::Msvc, &BuildAction::Compile(c));
746        assert_eq!(lowered.kind, LoweredActionKind::CompileC);
747        assert!(lowered.command.iter().any(|a| a == "/std:c11"));
748        assert!(!lowered.command.iter().any(|a| a == "/EHsc"));
749    }
750
751    #[test]
752    fn msvc_forces_source_language_per_classification() {
753        // `cl` only defaults `.cpp`/`.cxx` to C++; Cabin drives the
754        // language explicitly so any supported extension compiles as the
755        // language Cabin classified it. The source token carries `/Tp`
756        // for C++ and `/Tc` for C, with no bare source argument.
757        let cxx = lower(
758            Dialect::Msvc,
759            &BuildAction::Compile(msvc_cxx_compile(CompileMode::Object)),
760        );
761        assert!(cxx.command.iter().any(|a| a == "/TpC:/src/main.cc"));
762        assert!(!cxx.command.iter().any(|a| a == "C:/src/main.cc"));
763
764        let mut c_action = msvc_cxx_compile(CompileMode::Object);
765        c_action.standard = LanguageStandard::C(CStandard::C11);
766        let c = lower(Dialect::Msvc, &BuildAction::Compile(c_action));
767        assert!(c.command.iter().any(|a| a == "/TcC:/src/main.cc"));
768        assert!(!c.command.iter().any(|a| a == "/TpC:/src/main.cc"));
769    }
770
771    #[test]
772    fn msvc_debug_off_omits_z7_and_opt_maps_o3_to_o2() {
773        let mut c = msvc_cxx_compile(CompileMode::Object);
774        c.arguments.debug_info = false;
775        c.arguments.define_ndebug = false;
776        c.arguments.opt_level = OptLevel::O3;
777        let lowered = lower(Dialect::Msvc, &BuildAction::Compile(c));
778        assert!(!lowered.command.iter().any(|a| a == "/Z7"));
779        assert!(!lowered.command.iter().any(|a| a == "/DNDEBUG"));
780        assert!(lowered.command.iter().any(|a| a == "/O2"));
781    }
782
783    #[test]
784    fn msvc_archive_lowers_to_lib_out_command() {
785        let action = BuildAction::Archive(ArchiveAction {
786            archiver: Utf8PathBuf::from("lib.exe"),
787            output: Utf8PathBuf::from("C:/build/foo.lib"),
788            inputs: vec![
789                Utf8PathBuf::from("C:/build/a.obj"),
790                Utf8PathBuf::from("C:/build/b.obj"),
791            ],
792            description: "AR C:/build/foo.lib".to_owned(),
793        });
794        let lowered = lower(Dialect::Msvc, &action);
795        assert_eq!(
796            lowered.command,
797            strs(&[
798                "lib.exe",
799                "/nologo",
800                "/OUT:C:/build/foo.lib",
801                "C:/build/a.obj",
802                "C:/build/b.obj",
803            ])
804        );
805    }
806
807    #[test]
808    fn msvc_link_lowers_to_cl_fe_with_link_options() {
809        let action = BuildAction::Link(LinkAction {
810            linker: Utf8PathBuf::from("cl.exe"),
811            output: Utf8PathBuf::from("C:/build/app.exe"),
812            inputs: vec![
813                Utf8PathBuf::from("C:/build/main.obj"),
814                Utf8PathBuf::from("C:/build/foo.lib"),
815            ],
816            implicit_inputs: vec![],
817            arguments: strs(&["/SUBSYSTEM:CONSOLE"]),
818            link_libs: vec![],
819            description: "LINK C:/build/app.exe".to_owned(),
820        });
821        let lowered = lower(Dialect::Msvc, &action);
822        assert_eq!(
823            lowered.command,
824            strs(&[
825                "cl.exe",
826                "/nologo",
827                "C:/build/main.obj",
828                "C:/build/foo.lib",
829                "/FeC:/build/app.exe",
830                "/link",
831                "/SUBSYSTEM:CONSOLE",
832            ])
833        );
834    }
835
836    #[test]
837    fn msvc_link_without_ldflags_omits_link_separator() {
838        let action = BuildAction::Link(LinkAction {
839            linker: Utf8PathBuf::from("cl.exe"),
840            output: Utf8PathBuf::from("C:/build/app.exe"),
841            inputs: vec![Utf8PathBuf::from("C:/build/main.obj")],
842            implicit_inputs: vec![],
843            arguments: vec![],
844            link_libs: vec![],
845            description: "LINK C:/build/app.exe".to_owned(),
846        });
847        let lowered = lower(Dialect::Msvc, &action);
848        assert_eq!(
849            lowered.command,
850            strs(&[
851                "cl.exe",
852                "/nologo",
853                "C:/build/main.obj",
854                "/FeC:/build/app.exe",
855            ])
856        );
857    }
858
859    #[test]
860    fn gnu_link_lowers_link_libs_after_archives() {
861        // System libraries are spelled `-l<name>` and placed after the
862        // archive inputs and ldflags so GNU `ld` resolves them
863        // left-to-right against the archives that reference them.
864        let action = BuildAction::Link(LinkAction {
865            linker: Utf8PathBuf::from("/usr/bin/cc"),
866            output: Utf8PathBuf::from("/abs/build/app"),
867            inputs: vec![
868                Utf8PathBuf::from("/abs/build/main.o"),
869                Utf8PathBuf::from("/abs/build/libsqlite3.a"),
870            ],
871            implicit_inputs: vec![],
872            arguments: vec![],
873            link_libs: strs(&["pthread", "m"]),
874            description: "LINK /abs/build/app".to_owned(),
875        });
876        let lowered = lower(Dialect::GnuLike, &action);
877        assert_eq!(
878            lowered.command,
879            strs(&[
880                "/usr/bin/cc",
881                "/abs/build/main.o",
882                "/abs/build/libsqlite3.a",
883                "-lpthread",
884                "-lm",
885                "-o",
886                "/abs/build/app",
887            ])
888        );
889    }
890
891    #[test]
892    fn msvc_link_lowers_link_libs_as_dot_lib_inputs() {
893        // MSVC `link` has no `-l<name>`; system libraries are spelled
894        // `<name>.lib` and passed as positional inputs (after the
895        // archives, before `/Fe`), not GNU-style flags.
896        let action = BuildAction::Link(LinkAction {
897            linker: Utf8PathBuf::from("cl.exe"),
898            output: Utf8PathBuf::from("C:/build/app.exe"),
899            inputs: vec![Utf8PathBuf::from("C:/build/main.obj")],
900            implicit_inputs: vec![],
901            arguments: vec![],
902            link_libs: strs(&["user32"]),
903            description: "LINK C:/build/app.exe".to_owned(),
904        });
905        let lowered = lower(Dialect::Msvc, &action);
906        assert_eq!(
907            lowered.command,
908            strs(&[
909                "cl.exe",
910                "/nologo",
911                "C:/build/main.obj",
912                "user32.lib",
913                "/FeC:/build/app.exe",
914            ])
915        );
916    }
917}