Skip to content

Commit c40f0d7

Browse files
committed
feat(toolchain): native MSVC cl.exe build backend (C1-C7)
- C1 env model: find_windows_sdk() + build_env_for_cl() synthesize INCLUDE/LIB/PATH/VSLANG=1033 from the detected VC tools + SDK (no vcvarsall run); enrich_toolchain_from_cl fills tc.envOverrides; missing SDK keeps detection working and fails the BUILD with guidance (replaces the 0.0.88 'not yet supported' gate — the gate is gone). - C2 emission: SeparateLinker rules (link.exe /OUT + rspfile — cmd's 8191 limit), lib.exe archives, /DLL+/IMPLIB shared, deps=msvc via /showIncludes, /interface /TP for .cppm module units, /std: mapping (std_flag_for), /MD|/MT CRT model, /Od, /nologo /EHsc /utf-8 baseline, .obj object extension end to end (plan objExt + std staging names). - C3 std/std.compat staging: single-cl commands (/ifcOutput), ifc.cache layout, registry-dispatched staged paths (clang hardcoding removed), stdmod executes with the toolchain env (capture_with_env). - C4 scanning: /scanDependencies as the third builtin P1689 producer (provider capability + ninja scan rule); dyndep ifc-parameterized as-is. - C8 fast path: '@env' multi-var encoding in the build cache env slot so incremental msvc builds re-create INCLUDE/LIB for ninja. - cd /d in std stage commands (cmd.exe won't change drive without it; D: workspace + C: BMI cache is the real CI layout). Zero-diff gate re-verified for GCC and LLVM vs the 0.0.89 release binary.
1 parent 3470d67 commit c40f0d7

10 files changed

Lines changed: 441 additions & 88 deletions

File tree

src/build/execute.cppm

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,22 @@ export std::optional<int> try_fast_build(const std::filesystem::path& projectRoo
335335
if (verbose) argv.push_back("-v");
336336

337337
std::vector<std::pair<std::string, std::string>> childEnv;
338-
if (runtimeEnvKey != "-" && !runtimeEnvValue.empty())
338+
if (runtimeEnvKey == "@env") {
339+
// Multi-var encoding (MSVC INCLUDE/LIB/PATH/VSLANG + optional runtime
340+
// pair): \x1f-separated k=v records in the single value slot.
341+
std::string_view rest = runtimeEnvValue;
342+
while (!rest.empty()) {
343+
auto sep = rest.find('\x1f');
344+
auto rec = rest.substr(0, sep);
345+
if (auto eq = rec.find('='); eq != std::string_view::npos && eq > 0)
346+
childEnv.emplace_back(std::string(rec.substr(0, eq)),
347+
std::string(rec.substr(eq + 1)));
348+
if (sep == std::string_view::npos) break;
349+
rest.remove_prefix(sep + 1);
350+
}
351+
} else if (runtimeEnvKey != "-" && !runtimeEnvValue.empty()) {
339352
childEnv.emplace_back(runtimeEnvKey, runtimeEnvValue);
353+
}
340354

341355
auto t0 = std::chrono::steady_clock::now();
342356
// capture_exec merges stderr into the captured output (replacing `2>&1`),

src/build/flags.cppm

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ struct CompileFlags {
2727
std::string cxx; // full cxxflags string
2828
std::string cc; // full cflags string
2929
std::string ld; // ldflags string
30-
std::filesystem::path cxxBinary; // g++ / clang++
31-
std::filesystem::path ccBinary; // gcc / clang (derived)
32-
std::filesystem::path arBinary; // ar path (may be empty → use PATH)
30+
std::filesystem::path cxxBinary; // g++ / clang++ / cl.exe
31+
std::filesystem::path ccBinary; // gcc / clang (derived; cl.exe = same)
32+
std::filesystem::path arBinary; // ar / llvm-ar / lib.exe (empty → PATH)
33+
std::filesystem::path ldBinary; // link.exe (SeparateLinker dialects only)
3334
std::string sysroot; // --sysroot=... (for ninja ldflags)
3435
std::string bFlag; // -B<binutils> (for ninja ldflags)
3536
bool staticStdlib = true;
@@ -142,15 +143,17 @@ CompileFlags compute_flags(const BuildPlan& plan) {
142143
f.cxxBinary = plan.toolchain.binaryPath;
143144
f.ccBinary = mcpp::toolchain::derive_c_compiler(plan.toolchain);
144145

145-
// PIC?
146+
const bool isMsvcDialect = (d.id == "msvc");
147+
148+
// PIC? (GNU-only concept; PE code is position independent by design.)
146149
bool need_pic = false;
147150
for (auto& lu : plan.linkUnits) {
148151
if (lu.kind == LinkUnit::SharedLibrary) {
149152
need_pic = true;
150153
break;
151154
}
152155
}
153-
std::string pic_flag = need_pic ? " -fPIC" : "";
156+
std::string pic_flag = (need_pic && !isMsvcDialect) ? " -fPIC" : "";
154157

155158
// Include dirs
156159
std::string include_flags;
@@ -235,9 +238,21 @@ CompileFlags compute_flags(const BuildPlan& plan) {
235238
// unless the profile pins -O0.
236239
auto& prof = plan.manifest.buildConfig;
237240
std::string opt_flag = isMuslTc && prof.optLevel != "0"
238-
? " -Og" : std::format(" {}{}", d.optPrefix, prof.optLevel);
241+
? " -Og"
242+
: (isMsvcDialect && prof.optLevel == "0")
243+
? " /Od" // MSVC's no-opt spelling (there is no /O0)
244+
: std::format(" {}{}", d.optPrefix, prof.optLevel);
239245
if (prof.debug) opt_flag += std::format(" {}", d.debugFlags);
240-
if (prof.lto) opt_flag += " -flto";
246+
if (prof.lto && !isMsvcDialect) opt_flag += " -flto";
247+
248+
// MSVC baseline: /nologo /EHsc /utf-8 (dialect alwaysFlags) + the CRT
249+
// model — /MD default, /MT under static linkage (portable-by-default is
250+
// impossible on MSVC-ABI; /MT at least removes the vcruntime DLL dep).
251+
std::string msvc_base;
252+
if (isMsvcDialect) {
253+
msvc_base = std::format(" {}", d.alwaysFlags);
254+
msvc_base += (plan.manifest.buildConfig.linkage == "static") ? " /MT" : " /MD";
255+
}
241256

242257
// User link flags
243258
std::string user_ldflags;
@@ -264,9 +279,8 @@ CompileFlags compute_flags(const BuildPlan& plan) {
264279
}
265280
std::string std_compat_module_flag;
266281
if (!traits.stdCompatBmiUsePrefix.empty() && !plan.stdCompatBmiPath.empty()) {
267-
// NOTE: staging path is Clang's today; registry-dispatch when the
268-
// MSVC backend lands (std.compat.ixx staging).
269-
auto compatDst = mcpp::toolchain::clang::staged_std_compat_bmi_path(plan.outputDir);
282+
auto compatDst = mcpp::toolchain::staged_std_compat_bmi_path(
283+
plan.toolchain, plan.outputDir);
270284
std_compat_module_flag = std::string(traits.stdCompatBmiUsePrefix)
271285
+ escape_path(compatDst);
272286
}
@@ -290,12 +304,17 @@ CompileFlags compute_flags(const BuildPlan& plan) {
290304
// plan.dialectFlags rides right behind -std= (issue #210): module-graph-
291305
// global dialect flags reach every TU (deps included) via this global
292306
// cxxflags string, exactly like the standard flag itself.
293-
f.cxx = std::format("{}{}{}{}{}{}{}{}{}{}{}", cxx_std_flag, plan.dialectFlags,
294-
module_flag, std_module_flag,
307+
f.cxx = std::format("{}{}{}{}{}{}{}{}{}{}{}{}", cxx_std_flag, plan.dialectFlags,
308+
msvc_base, module_flag, std_module_flag,
295309
std_compat_module_flag, prebuilt_module_flag,
296310
opt_flag, pic_flag, compile_toolchain_flags, b_flag, include_flags);
297-
f.cc = std::format("{}{}{}{}{}{}{}", d.stdPrefix, c_std, opt_flag, pic_flag,
298-
compile_toolchain_flags, b_flag, include_flags);
311+
// MSVC compiles C with cl.exe too; /std: for C uses cN spellings — skip
312+
// the C standard flag there (cl defaults are fine for the C entry TUs).
313+
f.cc = isMsvcDialect
314+
? std::format("{}{}{}{}{}", msvc_base, opt_flag, compile_toolchain_flags,
315+
b_flag, include_flags)
316+
: std::format("{}{}{}{}{}{}{}", d.stdPrefix, c_std, opt_flag, pic_flag,
317+
compile_toolchain_flags, b_flag, include_flags);
299318

300319
// Link flags
301320
f.staticStdlib = plan.manifest.buildConfig.staticStdlib;
@@ -343,6 +362,18 @@ CompileFlags compute_flags(const BuildPlan& plan) {
343362
if (prof.strip) link_extra += " -s";
344363

345364
if constexpr (mcpp::platform::is_windows) {
365+
if (isMsvcDialect) {
366+
// Native cl.exe: link.exe does the link (SeparateLinker). Search
367+
// paths for dependency runtime import libs via /LIBPATH; user
368+
// ldflags pass through verbatim; GNU link_extra (-flto/-s) does
369+
// not apply.
370+
f.ldBinary = mcpp::toolchain::link_tool(plan.toolchain);
371+
std::string libpaths;
372+
for (auto& dir : plan.depRuntimeLibraryDirs)
373+
libpaths += " /LIBPATH:" + escape_path(dir);
374+
f.ld = libpaths + user_ldflags;
375+
return f;
376+
}
346377
// PE link: no rpath/loader/payload model. MSVC-ABI Clang needs
347378
// nothing extra (MSVC STL/SDK via the driver); MinGW adds the static
348379
// libstdc++/libgcc pair (static_stdlib above) and -B so its own

src/build/ninja_backend.cppm

Lines changed: 89 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -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

src/build/plan.cppm

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,13 @@ std::string sanitize_for_path(std::string_view module_name) {
130130
return s;
131131
}
132132

133-
std::string object_filename_for(const std::filesystem::path& src) {
133+
std::string object_filename_for(const std::filesystem::path& src,
134+
std::string_view objExt = ".o") {
134135
auto stem = src.stem().string();
135136
// distinguish .cppm vs .cpp by extension prefix to avoid collisions
136-
return stem + (src.extension() == ".cppm" ? ".m.o" : ".o");
137+
return stem + (src.extension() == ".cppm"
138+
? ".m" + std::string(objExt)
139+
: std::string(objExt));
137140
}
138141

139142
std::string qualified_package_name(const mcpp::manifest::Manifest& manifest) {
@@ -326,6 +329,8 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
326329
plan.dialectFlags += ' ';
327330
plan.dialectFlags += f;
328331
}
332+
// Object extension is dialect-spelled (.o vs .obj).
333+
const std::string_view objExt = mcpp::toolchain::dialect_for(tc).objExt;
329334
plan.projectRoot = projectRoot;
330335
plan.outputDir = outputDir;
331336
plan.stdBmiPath = stdBmiPath;
@@ -400,7 +405,7 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
400405
// derived from `<pkg>/<parent-dir>` so collisions are impossible.
401406
std::map<std::string, int> basenameCount;
402407
for (auto idx : topoOrder) {
403-
basenameCount[object_filename_for(graph.units[idx].path)]++;
408+
basenameCount[object_filename_for(graph.units[idx].path, objExt)]++;
404409
}
405410
auto sanitize = [](const std::string& s) {
406411
std::string out; out.reserve(s.size());
@@ -417,7 +422,7 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
417422
cu.localIncludeDirs = u.localIncludeDirs;
418423
cu.packageCflags = u.packageCflags;
419424
cu.packageCxxflags = u.packageCxxflags;
420-
const auto fname = object_filename_for(u.path);
425+
const auto fname = object_filename_for(u.path, objExt);
421426
if (basenameCount[fname] > 1) {
422427
// Use <sanitized-pkg>/<parent-dir-name> as prefix to handle
423428
// both cross-package (multi-version mangling) and intra-package
@@ -661,7 +666,7 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
661666
// Add main.cpp -> obj/main.o
662667
CompileUnit main_cu;
663668
main_cu.source = *lu.entryMain;
664-
main_cu.object = std::filesystem::path("obj") / object_filename_for(*lu.entryMain);
669+
main_cu.object = std::filesystem::path("obj") / object_filename_for(*lu.entryMain, objExt);
665670
main_cu.packageName = qualified_package_name(manifest);
666671
if (!packages.empty() && packages[0].usageResolved) {
667672
main_cu.localIncludeDirs = packages[0].privateBuild.includeDirs;

0 commit comments

Comments
 (0)