@@ -17,6 +17,7 @@ import mcpp.manifest;
1717import mcpp.platform;
1818import mcpp.platform.process;
1919import mcpp.toolchain.cppfly; // std_flag (dialect- and c++fly-aware -std= spelling)
20+ import mcpp.toolchain.dialect; // CommandDialect — gnu vs cl.exe spellings
2021import mcpp.toolchain.fingerprint; // hash_file / hash_string (FNV-1a, 16 hex)
2122import mcpp.toolchain.linkmodel; // shared C-library / clang-cfg-bypass model
2223import mcpp.toolchain.model; // Toolchain, PayloadPaths, is_clang/is_musl_target/is_mingw_target
@@ -78,8 +79,10 @@ namespace fs = std::filesystem;
7879struct Directives {
7980 std::vector<std::string> cxxflags; // -> buildConfig.cxxflags
8081 std::vector<std::string> cflags; // -> buildConfig.cflags
81- std::vector<std::string> ldflags; // -> buildConfig.ldflags (already -l/-L)
82- std::vector<std::string> defines; // cfg= -> -D, into BOTH c/cxx flags
82+ // -> buildConfig.ldflags, already spelled for the host dialect
83+ // (-l/-L for GNU, name.lib//LIBPATH: for cl.exe) — see parse_line.
84+ std::vector<std::string> ldflags;
85+ std::vector<std::string> defines; // cfg= -> define prefix, into BOTH c/cxx flags
8386 std::vector<std::string> generated; // relative source paths
8487 // source= — select a PRE-EXISTING file (tarball payload / vendored tree)
8588 // into the compile set. Downstream identical to generated=; the semantic
@@ -101,6 +104,22 @@ struct Directives {
101104 std::vector<std::string> rerunEnv; // declared env-var inputs
102105};
103106
107+ // Split a whitespace-separated flag string into argv tokens. The dialect
108+ // table stores some entries as multi-token strings ("-x c++",
109+ // "/nologo /EHsc /utf-8") because their other consumer is a ninja command
110+ // line, where a single string is what's wanted; an argv vector is not.
111+ std::vector<std::string> split_ws (std::string_view s) {
112+ std::vector<std::string> out;
113+ std::size_t i = 0 ;
114+ while (i < s.size ()) {
115+ while (i < s.size () && (s[i] == ' ' || s[i] == ' \t ' )) ++i;
116+ std::size_t b = i;
117+ while (i < s.size () && s[i] != ' ' && s[i] != ' \t ' ) ++i;
118+ if (i > b) out.emplace_back (s.substr (b, i - b));
119+ }
120+ return out;
121+ }
122+
104123std::string trim (std::string_view s) {
105124 std::size_t b = 0 , e = s.size ();
106125 while (b < e && (s[b] == ' ' || s[b] == ' \t ' || s[b] == ' \r ' )) ++b;
@@ -119,7 +138,17 @@ std::string abs_against_root(const fs::path& root, std::string_view p) {
119138
120139// Parse one stdout line. Returns true if it was a recognized (or unknown-but-
121140// `mcpp:`) directive; false for ordinary program chatter.
122- bool parse_line (const fs::path& root, std::string_view raw, Directives& d) {
141+ // `dial` decides how `link-lib` / `link-search` are spelled. The `mcpp:`
142+ // protocol itself is declarative — a build program says WHICH library it
143+ // needs, never how the local compiler driver names one — so the translation
144+ // belongs here at the boundary, not in the program.
145+ //
146+ // Storing the translated form in Directives (and therefore in the build.mcpp
147+ // cache) is safe because the cache key already hashes the compiler: switching
148+ // toolchains invalidates the entry before any spelling from the old dialect
149+ // could be replayed under the new one.
150+ bool parse_line (const fs::path& root, const mcpp::toolchain::CommandDialect& dial,
151+ std::string_view raw, Directives& d) {
123152 std::string line = trim (raw);
124153 constexpr std::string_view kPfx = " mcpp:" ;
125154 if (!line.starts_with (kPfx )) return false ;
@@ -130,9 +159,13 @@ bool parse_line(const fs::path& root, std::string_view raw, Directives& d) {
130159
131160 if (key == " cxxflag" ) d.cxxflags .push_back (val);
132161 else if (key == " cflag" ) d.cflags .push_back (val);
133- else if (key == " link-lib" ) d.ldflags .push_back (" -l" + val);
134- else if (key == " link-search" ) d.ldflags .push_back (" -L" + abs_against_root (root, val));
135- else if (key == " cfg" ) d.defines .push_back (" -D" + val);
162+ else if (key == " link-lib" ) d.ldflags .push_back (
163+ mcpp::toolchain::lib_flag_for (dial, val));
164+ else if (key == " link-search" ) d.ldflags .push_back (
165+ std::string (dial.libSearchPrefix )
166+ + abs_against_root (root, val));
167+ else if (key == " cfg" ) d.defines .push_back (
168+ std::string (dial.definePrefix ) + val);
136169 else if (key == " generated" ) d.generated .push_back (val);
137170 else if (key == " source" ) d.sources .push_back (val);
138171 else if (key == " include-dir" ) d.includeDirs .push_back (abs_against_root (root, val));
@@ -144,12 +177,13 @@ bool parse_line(const fs::path& root, std::string_view raw, Directives& d) {
144177 return true ;
145178}
146179
147- void parse_output (const fs::path& root, std::string_view out, Directives& d) {
180+ void parse_output (const fs::path& root, const mcpp::toolchain::CommandDialect& dial,
181+ std::string_view out, Directives& d) {
148182 std::size_t pos = 0 ;
149183 while (pos <= out.size ()) {
150184 std::size_t nl = out.find (' \n ' , pos);
151185 std::string_view ln = out.substr (pos, nl == std::string_view::npos ? std::string_view::npos : nl - pos);
152- parse_line (root, ln, d);
186+ parse_line (root, dial, ln, d);
153187 if (nl == std::string_view::npos) break ;
154188 pos = nl + 1 ;
155189 }
@@ -168,6 +202,14 @@ std::string env_value(const std::string& name) {
168202// only ones needed. Passed as separate argv tokens (no shell).
169203std::vector<std::string> host_base_flags (const mcpp::toolchain::Toolchain& tc) {
170204 std::vector<std::string> f;
205+
206+ // MSVC carries none of this on the command line: cl.exe and link.exe find
207+ // headers and import libraries through INCLUDE / LIB, which detection
208+ // synthesized into tc.envOverrides. Emitting the GNU shapes below would
209+ // produce a string of unknown options and then LNK1181. The environment
210+ // is passed to capture_exec instead — that is the whole MSVC "base".
211+ if (tc.compiler == mcpp::toolchain::CompilerId::MSVC ) return f;
212+
171213 const auto lm = mcpp::toolchain::resolve_link_model (tc);
172214
173215 // Clang with a bundled cfg on LINUX: bypass it (--no-default-config) and
@@ -688,6 +730,14 @@ std::expected<void, std::string> run_build_program(
688730 cppStandard.level );
689731 auto base = host_base_flags (tc);
690732
733+ // The host compile has always been spelled in GNU driver syntax with no
734+ // dialect branch at all — `grep -i msvc` over this file used to hit only
735+ // comments. Under cl.exe every one of `-O0` / `-x c++` / `-static` / `-o`
736+ // is wrong, so the whole build.mcpp path was unusable on a native MSVC
737+ // toolchain regardless of what else was fixed.
738+ const auto & dial = mcpp::toolchain::dialect_for (tc);
739+ const bool msvcHost = dial.id == std::string_view (" msvc" );
740+
691741 // Only wire the bundled `mcpp` module when build.mcpp actually imports it —
692742 // so the common `#include`-based program compiles exactly as before (no
693743 // -fmodules, cwd = project root). When it does `import mcpp;`, compile the
@@ -699,6 +749,18 @@ std::expected<void, std::string> run_build_program(
699749 bool usesStdCompat = imports_module (srcText, " std.compat" );
700750 bool usesStd = usesStdCompat || imports_module (srcText, " std" );
701751
752+ // Named modules under cl.exe go through .ifc + /reference, a different
753+ // pipeline from GCC's gcm.cache and Clang's -fmodule-file. That work is
754+ // not done, so say so plainly — one gate for both module kinds, because
755+ // they fail for exactly the same reason and two conditions would drift.
756+ if (msvcHost && (usesModule || usesStd)) {
757+ return std::unexpected (std::string (
758+ " build.mcpp: `import mcpp;` / `import std;` are not yet supported "
759+ " under MSVC.\n "
760+ " Use #include in build.mcpp, or build with a GCC/Clang "
761+ " toolchain." ));
762+ }
763+
702764 std::vector<std::string> moduleFlags;
703765 if (usesModule) {
704766 auto mf = build_mcpp_module (bdir, hostCompiler, base, std_flag,
@@ -793,15 +855,28 @@ std::expected<void, std::string> run_build_program(
793855
794856 // `-x c++` is required: the `.mcpp` extension is unknown to the compiler, so
795857 // without it the driver hands build.mcpp to the linker as a linker script.
796- std::vector<std::string> compileArgv = { hostCompiler.string (), std_flag, " -O0" };
858+ std::vector<std::string> compileArgv = { hostCompiler.string () };
859+ if (msvcHost) {
860+ // /nologo /EHsc /utf-8 — cl.exe needs these to behave like the other
861+ // two drivers do by default (quiet, exceptions on, UTF-8 sources).
862+ for (auto & f : split_ws (dial.alwaysFlags )) compileArgv.push_back (f);
863+ }
864+ compileArgv.push_back (std_flag);
865+ // No optimization: this program runs once per build and its compile time
866+ // is on the critical path. MSVC spells "off" /Od, not /O0.
867+ compileArgv.push_back (msvcHost ? std::string (" /Od" )
868+ : std::string (dial.optPrefix ) + " 0" );
797869 for (auto & bf : base) compileArgv.push_back (bf);
798870 for (auto & mf : moduleFlags) compileArgv.push_back (mf);
799871 for (auto & sf : stdFlags) compileArgv.push_back (sf);
800- compileArgv.push_back (" -x" ); compileArgv.push_back (" c++" );
872+ // The `.mcpp` extension is unknown to every driver, so without this the
873+ // file is handed to the linker as a linker script.
874+ for (auto & f : split_ws (dial.forceCxxLang )) compileArgv.push_back (f);
801875 compileArgv.push_back (src.string ());
802876 if (usesModule || !stdObjects.empty ()) {
803- // Link the module objects (reset the input language first so the .o
804- // isn't treated as C++ source).
877+ // Link the module objects (GNU: reset the input language first so the
878+ // .o isn't treated as C++ source; cl.exe infers by extension and is
879+ // unreachable here anyway, gated above).
805880 compileArgv.push_back (" -x" ); compileArgv.push_back (" none" );
806881 if (usesModule) compileArgv.push_back ((bdir / " mcpp.o" ).string ());
807882 for (auto & so : stdObjects) compileArgv.push_back (so);
@@ -810,8 +885,13 @@ std::expected<void, std::string> run_build_program(
810885 // Deliberately NOT in `base`: that also feeds the bundled module's
811886 // compile/precompile commands, where a link flag has no business (and for
812887 // Clang would perturb the default PIC/PIE codegen of mcpp.o).
813- if (staticHostHelper) compileArgv.push_back (" -static" );
814- compileArgv.push_back (" -o" ); compileArgv.push_back (bin.string ());
888+ if (staticHostHelper) compileArgv.push_back (std::string (dial.staticRuntime ));
889+ if (msvcHost) {
890+ // /Fe: takes its value attached, not as a separate argv token.
891+ compileArgv.push_back (std::string (dial.outputExePrefix ) + bin.string ());
892+ } else {
893+ compileArgv.push_back (" -o" ); compileArgv.push_back (bin.string ());
894+ }
815895 mcpp::ui::info (" build.mcpp" , " compiling" );
816896 // GCC resolves imported BMIs via gcm.cache/ relative to the compile cwd, so
817897 // any compile that imports a module — `mcpp`, `std`, or both — has to run
@@ -820,7 +900,16 @@ std::expected<void, std::string> run_build_program(
820900 // only mcpp. Otherwise the project root is fine.
821901 const bool needsBmiCwd = usesModule || stdStagedInBdir;
822902 std::string compileCwd = needsBmiCwd ? bdir.string () : root.string ();
823- auto cres = mcpp::platform::process::capture_exec (compileArgv, {}, compileCwd);
903+ // The toolchain's own environment (MSVC's INCLUDE / LIB / VSLANG, which
904+ // detection synthesized from the located VC tools + Windows SDK). Only
905+ // ninja_backend consumed these before, so a build.mcpp compile under
906+ // cl.exe could not find <cstdio> no matter how correct its argv was —
907+ // the third and last layer of #331's first finding.
908+ std::vector<std::pair<std::string, std::string>> compileEnv;
909+ for (auto const & ev : tc.envOverrides )
910+ compileEnv.emplace_back (ev.key , ev.value );
911+ auto cres = mcpp::platform::process::capture_exec (compileArgv, compileEnv,
912+ compileCwd);
824913 if (cres.exit_code != 0 ) {
825914 return std::unexpected (std::format (
826915 " build.mcpp failed to compile (exit {}):\n {}" , cres.exit_code , cres.output ));
@@ -838,7 +927,7 @@ std::expected<void, std::string> run_build_program(
838927 }
839928
840929 Directives d;
841- parse_output (root, rres.output , d);
930+ parse_output (root, dial, rres.output , d);
842931
843932 // Dependency mode (genBase set): relative `generated=` paths resolve
844933 // against OUT_DIR-style genBase, not the (possibly read-only, shared)
0 commit comments