Skip to content

Commit a8f9149

Browse files
committed
feat(build.mcpp): dialect-aware compile and directives, plus the toolchain env
MSVC support in the build.mcpp path was zero, not partial: `grep -i msvc` over build_program.cppm hit only comments. Three layers had to be fixed together, because each one only becomes visible after the previous is gone — first 'C:\Program' is not recognized, then D9002 on -O0, then LNK1181, then 'cannot open include file: cstdio'. - CommandDialect gains the link/language spellings it lacked: libFlag (a FORMAT, since GNU prefixes -lz and MSVC suffixes z.lib — no single prefix expresses both), libSearchPrefix, forceCxxLang, staticRuntime, outputExePrefix. - The host compile is spelled through the dialect instead of hardcoded GNU. - mcpp:link-lib / link-search / cfg are translated at the parse boundary, so the wire protocol stays declarative — a build program names WHICH library it needs, never how the local driver spells one. Storing the translated form in the cache is safe: the cache key already hashes the compiler. - host_base_flags returns nothing for MSVC — cl.exe finds headers and import libs through INCLUDE/LIB, not argv — and capture_exec now receives tc.envOverrides, which only ninja_backend consumed before. Named modules under cl.exe (.ifc + /reference) remain unimplemented; both import kinds share ONE gate and one diagnostic rather than failing obscurely.
1 parent 7fb47d1 commit a8f9149

3 files changed

Lines changed: 208 additions & 16 deletions

File tree

src/build/build_program.cppm

Lines changed: 105 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import mcpp.manifest;
1717
import mcpp.platform;
1818
import mcpp.platform.process;
1919
import mcpp.toolchain.cppfly; // std_flag (dialect- and c++fly-aware -std= spelling)
20+
import mcpp.toolchain.dialect; // CommandDialect — gnu vs cl.exe spellings
2021
import mcpp.toolchain.fingerprint; // hash_file / hash_string (FNV-1a, 16 hex)
2122
import mcpp.toolchain.linkmodel; // shared C-library / clang-cfg-bypass model
2223
import mcpp.toolchain.model; // Toolchain, PayloadPaths, is_clang/is_musl_target/is_mingw_target
@@ -78,8 +79,10 @@ namespace fs = std::filesystem;
7879
struct 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+
104123
std::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).
169203
std::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)

src/toolchain/dialect.cppm

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,22 @@ struct CommandDialect {
3333
std::string_view debugFlags; // "-g" | "/Zi /FS"
3434
std::string_view alwaysFlags; // "" | "/nologo /EHsc /utf-8"
3535

36+
// Link and language-selection spellings.
37+
//
38+
// `libFlag` is a FORMAT, not a prefix: GNU names a library by prefixing
39+
// (`-lz`) while MSVC names it by suffixing (`z.lib`), and no single
40+
// prefix string can express both. Use lib_flag_for().
41+
std::string_view libFlag; // "-l{}" | "{}.lib"
42+
std::string_view libSearchPrefix; // "-L" | "/LIBPATH:"
43+
// The `.mcpp` extension is unknown to every compiler driver, so the
44+
// language has to be forced or the driver hands the file to the linker.
45+
std::string_view forceCxxLang; // "-x c++" | "/TP"
46+
// Static CRT / runtime. On MSVC this is a compile-time CRT model, not a
47+
// link mode — there is no /MT equivalent of `-static` for the whole image.
48+
std::string_view staticRuntime; // "-static"| "/MT"
49+
// Output an executable (linking driver step).
50+
std::string_view outputExePrefix; // "-o " | "/Fe:"
51+
3652
// Artifact naming.
3753
std::string_view objExt; // ".o" | ".obj"
3854

@@ -56,6 +72,16 @@ struct CommandDialect {
5672
// Dialect lookup. GCC / Clang / MinGW → gnu; MSVC → msvc.
5773
const CommandDialect& dialect_for(const Toolchain& tc);
5874

75+
// The two dialect rows, reachable without a Toolchain. Exposed so the MSVC
76+
// row — which no build reaches until the cl.exe backend lands — can still be
77+
// unit-tested, and so callers that already know the shape they want (the
78+
// build.mcpp host compile) need not synthesize a Toolchain to ask.
79+
const CommandDialect& gnu_dialect();
80+
const CommandDialect& msvc_dialect();
81+
82+
// Name a library the way this dialect does: `-lz` vs `z.lib`.
83+
std::string lib_flag_for(const CommandDialect& d, std::string_view name);
84+
5985
// The full -std=/-/std: flag for a normalized standard (canonical like
6086
// "c++26"/"gnu++23", numeric level). MSVC: /std:c++20 exists; everything
6187
// newer maps to /std:c++latest (required for import std); gnu dialects have
@@ -79,6 +105,11 @@ constexpr CommandDialect kGnuDialect{
79105
.optPrefix = "-O",
80106
.debugFlags = "-g",
81107
.alwaysFlags = "",
108+
.libFlag = "-l{}",
109+
.libSearchPrefix = "-L",
110+
.forceCxxLang = "-x c++",
111+
.staticRuntime = "-static",
112+
.outputExePrefix = "-o ",
82113
.objExt = ".o",
83114
.ninjaDepsMode = "",
84115
.rspfileLink = false,
@@ -99,6 +130,11 @@ constexpr CommandDialect kMsvcDialect{
99130
.optPrefix = "/O",
100131
.debugFlags = "/Zi /FS",
101132
.alwaysFlags = "/nologo /EHsc /utf-8",
133+
.libFlag = "{}.lib",
134+
.libSearchPrefix = "/LIBPATH:",
135+
.forceCxxLang = "/TP",
136+
.staticRuntime = "/MT",
137+
.outputExePrefix = "/Fe:",
102138
.objExt = ".obj",
103139
.ninjaDepsMode = "msvc",
104140
.rspfileLink = true,
@@ -113,6 +149,18 @@ const CommandDialect& dialect_for(const Toolchain& tc) {
113149
return kGnuDialect;
114150
}
115151

152+
const CommandDialect& gnu_dialect() { return kGnuDialect; }
153+
const CommandDialect& msvc_dialect() { return kMsvcDialect; }
154+
155+
std::string lib_flag_for(const CommandDialect& d, std::string_view name) {
156+
// Two shapes, one table entry: `{}` marks where the name goes, which is
157+
// a prefix position for GNU and a suffix position for MSVC.
158+
std::string out(d.libFlag);
159+
if (auto p = out.find("{}"); p != std::string::npos)
160+
out.replace(p, 2, name);
161+
return out;
162+
}
163+
116164
std::string std_flag_for(const CommandDialect& d,
117165
std::string_view canonical, int level) {
118166
if (d.id == "msvc") {

tests/unit/test_dialect.cpp

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#include <gtest/gtest.h>
2+
3+
import std;
4+
import mcpp.toolchain.dialect;
5+
import mcpp.toolchain.model;
6+
7+
// The MSVC row is unreachable in a real build until the cl.exe backend lands,
8+
// so these tests are the only thing keeping it honest.
9+
10+
TEST(Dialect, LibFlagHasBothShapes) {
11+
// GNU names a library by prefixing, MSVC by suffixing. A single
12+
// string_view prefix cannot express `z.lib`, which is why libFlag is a
13+
// format rather than a prefix.
14+
EXPECT_EQ(mcpp::toolchain::lib_flag_for(mcpp::toolchain::gnu_dialect(), "z"),
15+
"-lz");
16+
EXPECT_EQ(mcpp::toolchain::lib_flag_for(mcpp::toolchain::msvc_dialect(), "z"),
17+
"z.lib");
18+
}
19+
20+
TEST(Dialect, LibFlagHandlesDottedAndHyphenatedNames) {
21+
EXPECT_EQ(mcpp::toolchain::lib_flag_for(mcpp::toolchain::gnu_dialect(),
22+
"avcodec-60"), "-lavcodec-60");
23+
EXPECT_EQ(mcpp::toolchain::lib_flag_for(mcpp::toolchain::msvc_dialect(),
24+
"avcodec-60"), "avcodec-60.lib");
25+
}
26+
27+
// Every dialect field must be populated in both rows. An empty one silently
28+
// emits nothing, which for staticRuntime or forceCxxLang means the compile
29+
// changes meaning rather than failing.
30+
TEST(Dialect, LinkAndLanguageFieldsPopulatedInBothRows) {
31+
for (auto const* d : { &mcpp::toolchain::gnu_dialect(),
32+
&mcpp::toolchain::msvc_dialect() }) {
33+
EXPECT_FALSE(d->libFlag.empty()) << d->id;
34+
EXPECT_FALSE(d->libSearchPrefix.empty()) << d->id;
35+
EXPECT_FALSE(d->forceCxxLang.empty()) << d->id;
36+
EXPECT_FALSE(d->staticRuntime.empty()) << d->id;
37+
EXPECT_FALSE(d->outputExePrefix.empty()) << d->id;
38+
// A `{}` placeholder is what makes lib_flag_for work at all.
39+
EXPECT_NE(d->libFlag.find("{}"), std::string_view::npos) << d->id;
40+
}
41+
}
42+
43+
// dialect_for must keep routing clang-targeting-MSVC to the gnu spellings:
44+
// that driver takes GNU flags even though its ABI and STL are Microsoft's.
45+
TEST(Dialect, OnlyNativeClExeGetsTheMsvcRow) {
46+
mcpp::toolchain::Toolchain clangMsvc;
47+
clangMsvc.compiler = mcpp::toolchain::CompilerId::Clang;
48+
clangMsvc.targetTriple = "x86_64-pc-windows-msvc";
49+
EXPECT_EQ(mcpp::toolchain::dialect_for(clangMsvc).id, "gnu");
50+
51+
mcpp::toolchain::Toolchain cl;
52+
cl.compiler = mcpp::toolchain::CompilerId::MSVC;
53+
cl.targetTriple = "x86_64-pc-windows-msvc";
54+
EXPECT_EQ(mcpp::toolchain::dialect_for(cl).id, "msvc");
55+
}

0 commit comments

Comments
 (0)