@@ -297,6 +297,14 @@ std::string emit_ninja_string(const BuildPlan& plan) {
297297 } else {
298298 append (" ar = ar\n " );
299299 }
300+ // Separate linker (link.exe) for the msvc dialect.
301+ const bool separateLinker =
302+ dial.linkStyle == mcpp::toolchain::CommandDialect::LinkStyle::SeparateLinker;
303+ if (separateLinker) {
304+ append (std::format (" ld = {}\n " ,
305+ flags.ldBinary .empty () ? std::string (" link.exe" )
306+ : escape_ninja_path (flags.ldBinary )));
307+ }
300308 if (dyndep) {
301309 append (std::format (" mcpp = {}\n " , escape_ninja_path (mcpp_exe_path ())));
302310 if (!plan.scanDepsPath .empty ()) {
@@ -339,14 +347,25 @@ std::string emit_ninja_string(const BuildPlan& plan) {
339347 // msvc); the rule *structure* is shared across compilers.
340348 std::string module_output_flag = traits.needsExplicitModuleOutput
341349 ? std::string (traits.moduleOutputPrefix ) + " $bmi_out" : " " ;
350+ // msvc: /showIncludes feeds ninja's deps=msvc header tracking; the
351+ // stable-English prefix is guaranteed by VSLANG=1033 in envOverrides.
352+ const bool msvcDeps = dial.ninjaDepsMode == std::string_view (" msvc" );
342353 const std::string compile_tail = std::format (
343- " {} $in {}$out" , dial.compileOnly , dial.outputObjPrefix );
354+ " {}{} $in {}$out" ,
355+ msvcDeps ? " /showIncludes " : " " , dial.compileOnly , dial.outputObjPrefix );
356+ auto append_deps = [&] {
357+ if (msvcDeps) append (" deps = msvc\n " );
358+ };
359+ // cl.exe needs /TP (our module interfaces are .cppm, unknown to cl) and
360+ // /interface to treat the TU as a module interface unit.
361+ const std::string module_src_flags = msvcDeps ? " /interface /TP" : " " ;
344362 append (" rule cxx_module\n " );
345363 if constexpr (mcpp::platform::is_windows) {
346364 // Windows: skip BMI restat optimization (requires POSIX shell).
347365 append (std::format (" command = "
348- " $cxx $local_includes $cxxflags $unit_cxxflags{} {}\n " ,
349- module_output_flag, compile_tail));
366+ " $cxx $local_includes $cxxflags $unit_cxxflags{}{} {}\n " ,
367+ module_output_flag, module_src_flags, compile_tail));
368+ append_deps ();
350369 } else {
351370 append (std::format (" command = "
352371 " if [ -n \" $bmi_out\" ] && [ -f \" $bmi_out\" ]; then "
@@ -370,6 +389,7 @@ std::string emit_ninja_string(const BuildPlan& plan) {
370389 " command = $cxx $local_includes $cxxflags $unit_cxxflags {}\n " ,
371390 compile_tail));
372391 append (" description = OBJ $out\n " );
392+ append_deps ();
373393 if (dyndep)
374394 append (" restat = 1\n " );
375395 append (" \n " );
@@ -380,25 +400,47 @@ std::string emit_ninja_string(const BuildPlan& plan) {
380400 " command = $cc $local_includes $cflags $unit_cflags {}\n " ,
381401 compile_tail));
382402 append (" description = CC $out\n " );
403+ append_deps ();
383404 if (dyndep)
384405 append (" restat = 1\n " );
385406 append (" \n " );
386407 }
387408
388- // Link rule: driver-style today (g++/clang++ act as the linker). The
389- // dialect's LinkStyle::SeparateLinker (link.exe /OUT: + rspfile) is the
390- // MSVC backend's insertion point — unreachable until that lands.
391- append (" rule cxx_link\n " );
392- append (" command = $cxx $in -o $out $ldflags $unit_ldflags\n " );
393- append (" description = LINK $out\n\n " );
409+ // Link/archive/shared: driver-style (g++/clang++ are the linker) vs the
410+ // msvc dialect's separate link.exe/lib.exe. The msvc commands go through
411+ // response files — object lists exceed cmd.exe's 8191-char limit fast.
412+ if (separateLinker) {
413+ append (" rule cxx_link\n " );
414+ append (" command = $ld /nologo /OUT:$out @$out.rsp $ldflags $unit_ldflags\n " );
415+ append (" rspfile = $out.rsp\n " );
416+ append (" rspfile_content = $in\n " );
417+ append (" description = LINK $out\n\n " );
418+
419+ append (" rule cxx_archive\n " );
420+ append (" command = $ar /nologo /OUT:$out @$out.rsp\n " );
421+ append (" rspfile = $out.rsp\n " );
422+ append (" rspfile_content = $in\n " );
423+ append (" description = AR $out\n\n " );
424+
425+ append (" rule cxx_shared\n " );
426+ append (" command = $ld /nologo /DLL /OUT:$out /IMPLIB:$out.lib "
427+ " @$out.rsp $ldflags $unit_ldflags\n " );
428+ append (" rspfile = $out.rsp\n " );
429+ append (" rspfile_content = $in\n " );
430+ append (" description = SHARED $out\n\n " );
431+ } else {
432+ append (" rule cxx_link\n " );
433+ append (" command = $cxx $in -o $out $ldflags $unit_ldflags\n " );
434+ append (" description = LINK $out\n\n " );
394435
395- append (" rule cxx_archive\n " );
396- append (std::format (" command = {}\n " , dial.archiveCmd ));
397- append (" description = AR $out\n\n " );
436+ append (" rule cxx_archive\n " );
437+ append (std::format (" command = {}\n " , dial.archiveCmd ));
438+ append (" description = AR $out\n\n " );
398439
399- append (" rule cxx_shared\n " );
400- append (" command = $cxx -shared $in -o $out $ldflags $soname_flag $unit_ldflags\n " );
401- append (" description = SHARED $out\n\n " );
440+ append (" rule cxx_shared\n " );
441+ append (" command = $cxx -shared $in -o $out $ldflags $soname_flag $unit_ldflags\n " );
442+ append (" description = SHARED $out\n\n " );
443+ }
402444
403445 append (" rule runtime_alias\n " );
404446 if constexpr (mcpp::platform::is_windows) {
@@ -413,7 +455,12 @@ std::string emit_ninja_string(const BuildPlan& plan) {
413455 // GCC: built-in -fdeps-format=p1689r5 flags during preprocessing.
414456 // Clang: external clang-scan-deps tool with -format=p1689.
415457 append (" rule cxx_scan\n " );
416- if (plan.scanDepsPath .empty ()) {
458+ if (msvcDeps) {
459+ // MSVC: compiler-integrated P1689 via /scanDependencies (scan
460+ // only — no codegen); /TP because our module units are .cppm.
461+ append (" command = $cxx $local_includes $cxxflags $unit_cxxflags "
462+ " /scanDependencies $out /TP /c $in /Fo:$compile_target\n " );
463+ } else if (plan.scanDepsPath .empty ()) {
417464 // GCC path: compiler-integrated P1689 scanning.
418465 append (" command = $cxx $local_includes $cxxflags -fmodules "
419466 " $unit_cxxflags "
@@ -445,7 +492,8 @@ std::string emit_ninja_string(const BuildPlan& plan) {
445492
446493 // Stage prebuilt std artifacts into the compiler-specific BMI cache.
447494 auto std_bmi_dst = mcpp::toolchain::staged_std_bmi_path (plan.toolchain , {});
448- auto std_o_dst = std::filesystem::path (" obj" ) / " std.o" ;
495+ auto std_o_dst = std::filesystem::path (" obj" )
496+ / std::format (" std{}" , dial.objExt );
449497
450498 bool has_std_artifacts = !plan.stdBmiPath .empty () && !plan.stdObjectPath .empty ();
451499 if (has_std_artifacts) {
@@ -456,8 +504,10 @@ std::string emit_ninja_string(const BuildPlan& plan) {
456504 }
457505
458506 bool has_std_compat = !plan.stdCompatBmiPath .empty () && !plan.stdCompatObjectPath .empty ();
459- auto compat_bmi_dst = std::filesystem::path (" pcm.cache" ) / " std.compat.pcm" ;
460- auto compat_o_dst = std::filesystem::path (" obj" ) / " std.compat.o" ;
507+ auto compat_bmi_dst = std::filesystem::path (traits.bmiDir )
508+ / std::format (" std.compat{}" , traits.bmiExt );
509+ auto compat_o_dst = std::filesystem::path (" obj" )
510+ / std::format (" std.compat{}" , dial.objExt );
461511 if (has_std_compat) {
462512 // std.compat.pcm depends on std.pcm — ensure std.pcm is staged first
463513 // so clang can resolve the transitive dependency when loading std.compat.pcm.
@@ -803,7 +853,22 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
803853 // Record ninja binary for P0 fast-path cache.
804854 BuildResult r;
805855 r.ninjaProgram = ninjaProgram;
806- if (auto runtimeEnv = runtime_env_for_dirs (plan.toolchain .compilerRuntimeDirs )) {
856+ if (!plan.toolchain .envOverrides .empty ()) {
857+ // Toolchain-declared env (MSVC INCLUDE/LIB/PATH/VSLANG). Encode all
858+ // pairs (plus any runtime-dirs pair) into the fast-path cache's
859+ // single env slot: "@env" key + \x1f-separated k=v records — the
860+ // fast path must re-create this exact environment for ninja.
861+ r.runtimeEnvKey = " @env" ;
862+ std::string joined;
863+ auto add = [&](const std::string& k, const std::string& v) {
864+ if (!joined.empty ()) joined += ' \x1f ' ;
865+ joined += k; joined += ' =' ; joined += v;
866+ };
867+ if (auto runtimeEnv = runtime_env_for_dirs (plan.toolchain .compilerRuntimeDirs ))
868+ add (runtimeEnv->first , runtimeEnv->second );
869+ for (auto & ev : plan.toolchain .envOverrides ) add (ev.key , ev.value );
870+ r.runtimeEnvValue = std::move (joined);
871+ } else if (auto runtimeEnv = runtime_env_for_dirs (plan.toolchain .compilerRuntimeDirs )) {
807872 r.runtimeEnvKey = runtimeEnv->first ;
808873 r.runtimeEnvValue = runtimeEnv->second ;
809874 } else {
@@ -824,12 +889,11 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
824889 if (opts.parallelJobs )
825890 nargv.push_back (std::format (" -j{}" , opts.parallelJobs ));
826891
892+ // Real env pairs for THIS run (the "@env" cache encoding above is only
893+ // for the fast path's later re-creation of the same environment).
827894 std::vector<std::pair<std::string, std::string>> nenv;
828- if (r.runtimeEnvKey != " -" && !r.runtimeEnvValue .empty ())
829- nenv.emplace_back (r.runtimeEnvKey , r.runtimeEnvValue );
830- // Toolchain-declared env (empty for GCC/Clang; MSVC's INCLUDE/LIB/PATH).
831- // NOTE: not persisted in the fast-path cache yet — revisit when the MSVC
832- // backend lands (its fast path must re-derive these from detection).
895+ if (auto runtimeEnv = runtime_env_for_dirs (plan.toolchain .compilerRuntimeDirs ))
896+ nenv.emplace_back (runtimeEnv->first , runtimeEnv->second );
833897 for (auto & ev : plan.toolchain .envOverrides )
834898 nenv.emplace_back (ev.key , ev.value );
835899
0 commit comments