|
9 | 9 |
|
10 | 10 | export module mcpp.build.prepare; |
11 | 11 |
|
| 12 | +// The cfg() predicate evaluator and the fingerprint canonicalisers moved out — |
| 13 | +// see mcpp.build.prepare_inputs. Re-exported so every existing caller of |
| 14 | +// `target_dir` / `canonical_compile_flags` keeps working: a split whose only |
| 15 | +// visible effect is that other files stop compiling is not an improvement. |
| 16 | +export import mcpp.build.prepare_inputs; |
| 17 | + |
12 | 18 | import std; |
13 | 19 | import mcpp.diag; |
14 | 20 | import mcpp.home; |
@@ -98,347 +104,6 @@ inline void warn_unknown_xpkg_keys(const mcpp::manifest::Manifest& dm, |
98 | 104 | } |
99 | 105 | } |
100 | 106 |
|
101 | | -// ── L1 platform-conditional config: cfg() predicate evaluation ────────────── |
102 | | -// Context = the RESOLVED target's coordinates. A `[target.'cfg(...)'.build]` |
103 | | -// predicate is evaluated against this (target triple for a cross build, host |
104 | | -// for a native build), so conditional flags follow what the binary will run on |
105 | | -// — not the build host. See the manifest design doc. |
106 | | -namespace cfgpred { |
107 | | - |
108 | | -struct Ctx { std::string os, arch, family, env; }; |
109 | | - |
110 | | -// Derive the cfg context from the resolved --target triple, falling back to |
111 | | -// the host for a native build. Parsing goes through triple.cppm — the single |
112 | | -// triple parser — so the cfg vocabulary IS the canonical triple vocabulary |
113 | | -// (os: linux|macos|windows, arch: GNU spellings, env: gnu|musl|msvc), and |
114 | | -// alias spellings ("x86_64-w64-mingw32") evaluate identically to canonical. |
115 | | -inline Ctx context_for(std::string_view targetTriple) { |
116 | | - namespace triple = mcpp::toolchain::triple; |
117 | | - Ctx c; |
118 | | - auto t = targetTriple.empty() |
119 | | - ? std::optional<triple::Triple>(triple::host_triple()) |
120 | | - : triple::parse(targetTriple); |
121 | | - if (t) { |
122 | | - c.os = t->os; |
123 | | - c.arch = t->arch; |
124 | | - c.env = t->env; |
125 | | - c.family = t->family(); |
126 | | - } else { |
127 | | - // Escape-hatch triple outside the language: only the leading arch |
128 | | - // segment is derivable; other dimensions stay empty (never match). |
129 | | - auto dash = targetTriple.find('-'); |
130 | | - c.arch = std::string(dash == std::string_view::npos ? targetTriple |
131 | | - : targetTriple.substr(0, dash)); |
132 | | - } |
133 | | - return c; |
134 | | -} |
135 | | - |
136 | | -// Recursive-descent evaluator over the inside of `cfg(...)`: |
137 | | -// expr := all(list) | any(list) | not(expr) | key="value" | bareword |
138 | | -// key ∈ {os, arch, family, env} bareword ∈ {windows, unix, linux, macos} |
139 | | -struct Parser { |
140 | | - std::string_view s; std::size_t i = 0; const Ctx& c; |
141 | | - void ws() { while (i < s.size() && std::isspace((unsigned char)s[i])) ++i; } |
142 | | - bool eat(char ch) { ws(); if (i < s.size() && s[i] == ch) { ++i; return true; } return false; } |
143 | | - std::string ident() { |
144 | | - ws(); std::size_t b = i; |
145 | | - while (i < s.size() && (std::isalnum((unsigned char)s[i]) || s[i] == '_')) ++i; |
146 | | - return std::string(s.substr(b, i - b)); |
147 | | - } |
148 | | - std::string str() { |
149 | | - ws(); if (i >= s.size() || s[i] != '"') return {}; |
150 | | - ++i; std::size_t b = i; while (i < s.size() && s[i] != '"') ++i; |
151 | | - auto v = std::string(s.substr(b, i - b)); if (i < s.size()) ++i; return v; |
152 | | - } |
153 | | - bool match_alias(const std::string& a) { |
154 | | - if (a == "windows") return c.os == "windows"; |
155 | | - if (a == "linux") return c.os == "linux"; |
156 | | - if (a == "macos") return c.os == "macos"; |
157 | | - if (a == "unix") return c.family == "unix"; |
158 | | - return false; // unknown bareword → no match |
159 | | - } |
160 | | - bool match_kv(const std::string& k, const std::string& v) { |
161 | | - if (k == "os") return c.os == v; |
162 | | - if (k == "arch") return c.arch == v; |
163 | | - if (k == "family") return c.family == v; |
164 | | - if (k == "env") return c.env == v; |
165 | | - return false; |
166 | | - } |
167 | | - bool expr() { |
168 | | - std::string id = ident(); |
169 | | - if (id == "all" || id == "any") { |
170 | | - eat('('); |
171 | | - bool acc = (id == "all"); |
172 | | - ws(); |
173 | | - if (!(i < s.size() && s[i] == ')')) { |
174 | | - do { bool r = expr(); acc = (id == "all") ? (acc && r) : (acc || r); } |
175 | | - while (eat(',')); |
176 | | - } |
177 | | - eat(')'); |
178 | | - return acc; |
179 | | - } |
180 | | - if (id == "not") { eat('('); bool r = expr(); eat(')'); return !r; } |
181 | | - ws(); |
182 | | - if (i < s.size() && s[i] == '=') { ++i; return match_kv(id, str()); } |
183 | | - return match_alias(id); |
184 | | - } |
185 | | -}; |
186 | | - |
187 | | -// Evaluate a `[target.<predicate>]` key. Returns the cfg() result, or — for a |
188 | | -// non-cfg key (a bare triple) — an exact match against the resolved triple. |
189 | | -inline bool matches(const std::string& predicate, const Ctx& c, std::string_view triple) { |
190 | | - std::string_view k = predicate; |
191 | | - if (k.starts_with("cfg(") && k.ends_with(")")) { |
192 | | - Parser p{ k.substr(4, k.size() - 5), 0, c }; |
193 | | - return p.expr(); |
194 | | - } |
195 | | - // Bare OS/family alias sugar: `[target.linux]` ≡ `[target.'cfg(linux)']`. |
196 | | - // These aliases are never valid triples (no dash), so there is no ambiguity |
197 | | - // with the exact-triple namespace. Evaluated as the cfg bareword. |
198 | | - if (predicate == "windows" || predicate == "linux" || |
199 | | - predicate == "macos" || predicate == "unix") { |
200 | | - Parser p{ predicate, 0, c }; |
201 | | - return p.expr(); |
202 | | - } |
203 | | - // Bare-triple match, spelling-independent: a `[target.x86_64-w64-mingw32]` |
204 | | - // key matches a resolved `x86_64-windows-gnu` build (and vice versa) — |
205 | | - // both normalize through triple::parse. Unparseable keys (the explicit- |
206 | | - // section escape hatch) fall back to exact string comparison. |
207 | | - if (triple.empty()) return false; |
208 | | - if (auto p = mcpp::toolchain::triple::parse(predicate)) { |
209 | | - if (auto rt = mcpp::toolchain::triple::parse(triple)) |
210 | | - return p->str() == rt->str(); |
211 | | - } |
212 | | - return predicate == triple; |
213 | | -} |
214 | | - |
215 | | -} // namespace cfgpred |
216 | | - |
217 | | -export std::filesystem::path target_dir(const mcpp::toolchain::Toolchain& tc, |
218 | | - const mcpp::toolchain::Fingerprint& fp, |
219 | | - const std::filesystem::path& root) |
220 | | -{ |
221 | | - // Canonical triple names the output directory (D1: `target/ |
222 | | - // x86_64-windows-gnu/`, not the GNU spelling the compiler reports via |
223 | | - // -dumpmachine) — alias inputs land in the same directory. Triples |
224 | | - // outside the language keep their raw spelling. |
225 | | - auto triple = tc.targetTriple.empty() ? std::string{"unknown"} : tc.targetTriple; |
226 | | - if (auto t = mcpp::toolchain::triple::parse(triple)) triple = t->str(); |
227 | | - return root / "target" / triple / fp.hex; |
228 | | -} |
229 | | - |
230 | | - |
231 | | -// Compose a stable canonical compile-flags string for fingerprinting. |
232 | | -// Exported so the "every build-variant knob is in here" invariant is machine- |
233 | | -// checkable: the profile knobs were absent for a long time precisely because |
234 | | -// nothing could assert on this string. |
235 | | -export std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) { |
236 | | - std::string s; |
237 | | - s += "-std="; s += m.package.standard; |
238 | | - s += " -fmodules"; |
239 | | - // macOS deployment target changes the effective compile triple |
240 | | - // (arm64-apple-macosxNN) — a std.pcm built for one target cannot be |
241 | | - // loaded by a TU compiled for another. Fold the resolved value |
242 | | - // (env override > [build] macos_deployment_target manifest default) |
243 | | - // into the fingerprint so switching targets rebuilds the BMI cache |
244 | | - // instead of dying with a module config mismatch. |
245 | | - // |
246 | | - // The built-in default floor (rustc-style) lives in the single |
247 | | - // resolver (platform::macos::deployment_target), so this rule, the |
248 | | - // flags and the std-module prebuild always agree — the 0.0.50-era |
249 | | - // attempt to inject a default here alone left the test build's |
250 | | - // std.pcm unstaged (import std failed wholesale on macos CI). |
251 | | - if constexpr (mcpp::platform::is_macos) { |
252 | | - auto dtv = mcpp::platform::macos::deployment_target( |
253 | | - m.buildConfig.macosDeploymentTarget); |
254 | | - if (!dtv.empty()) { |
255 | | - s += " macos_deployment_target="; |
256 | | - s += dtv; |
257 | | - } |
258 | | - } |
259 | | - if (!m.buildConfig.cStandard.empty()) { |
260 | | - s += " c_standard="; |
261 | | - s += m.buildConfig.cStandard; |
262 | | - } |
263 | | - for (auto const& flag : m.buildConfig.cflags) { |
264 | | - s += " cflag:"; |
265 | | - s += flag; |
266 | | - } |
267 | | - for (auto const& flag : m.buildConfig.cxxflags) { |
268 | | - s += " cxxflag:"; |
269 | | - s += flag; |
270 | | - } |
271 | | - // Explicit [build] dialect_cxxflags (auto-promoted ones are already in |
272 | | - // cxxflags above) — they change every BMI in the graph. |
273 | | - for (auto const& flag : m.buildConfig.dialectCxxflags) { |
274 | | - s += " dialect:"; |
275 | | - s += flag; |
276 | | - } |
277 | | - for (auto const& flag : m.buildConfig.ldflags) { |
278 | | - s += " ldflag:"; |
279 | | - s += flag; |
280 | | - } |
281 | | - // Per-glob flags (G4): full ordered serialization — glob + every list — |
282 | | - // so editing any entry (or reordering) re-fingerprints the output dir. |
283 | | - for (auto const& gf : m.buildConfig.globFlags) { |
284 | | - s += " globflags:"; s += gf.glob; |
285 | | - for (auto const& f : gf.cflags) { s += " gc:"; s += f; } |
286 | | - for (auto const& f : gf.cxxflags) { s += " gxx:"; s += f; } |
287 | | - for (auto const& f : gf.asmflags) { s += " gas:"; s += f; } |
288 | | - for (auto const& f : gf.defines) { s += " gd:"; s += f; } |
289 | | - } |
290 | | - // [build] module_extensions changes WHICH FILES ARE MODULE INTERFACES, |
291 | | - // i.e. the shape of the graph: which units emit a BMI, which objects link |
292 | | - // unconditionally, which ninja rule each unit gets. That is a build |
293 | | - // variant, so it belongs in the fingerprint — mcpp.toml's mtime alone only |
294 | | - // protects the fast path within one output dir, not the BMI cache. |
295 | | - // |
296 | | - // Contrast [build] build_program_timeout, which is deliberately absent: |
297 | | - // it changes no edge. See BuildConfig::buildProgramTimeoutSecs. |
298 | | - for (auto const& e : m.buildConfig.moduleExtensions) { |
299 | | - s += " modext:"; |
300 | | - s += e; |
301 | | - } |
302 | | - // The resolved [profile] knobs. These are NOT in cflags/cxxflags: the |
303 | | - // profile block (see the profile resolution below) lands them in |
304 | | - // buildConfig.optLevel/debug/lto/strip and flags.cppm turns them into |
305 | | - // -O<n>/-g/-flto at command-construction time. Leaving them out made |
306 | | - // `--dev`, `--release` and `--profile dist` share ONE fingerprint, hence |
307 | | - // one target/<triple>/<fp>/ directory AND one global cache entry — so a |
308 | | - // release build could be served -O0 -g dependency objects. They are |
309 | | - // build-variant by definition; they belong here. |
310 | | - s += " opt="; s += m.buildConfig.optLevel; |
311 | | - s += " debug="; s += m.buildConfig.debug ? "1" : "0"; |
312 | | - s += " lto="; s += m.buildConfig.lto ? "1" : "0"; |
313 | | - s += " strip="; s += m.buildConfig.strip ? "1" : "0"; |
314 | | - return s; |
315 | | -} |
316 | | - |
317 | | -std::string canonical_package_build_metadata( |
318 | | - const std::vector<mcpp::modgraph::PackageRoot>& packages) |
319 | | -{ |
320 | | - std::string s; |
321 | | - for (auto const& pkg : packages) { |
322 | | - s += "\npackage:"; |
323 | | - s += pkg.manifest.package.namespace_; |
324 | | - s += "/"; |
325 | | - s += pkg.manifest.package.name; |
326 | | - s += "@"; |
327 | | - s += pkg.manifest.package.version; |
328 | | - s += " source="; |
329 | | - s += pkg.manifest.package.sourceProvenance; |
330 | | - auto const& runtime = pkg.manifest.runtimeConfig; |
331 | | - for (auto const& requirement : runtime.requirements) { |
332 | | - s += " runtime-need:"; |
333 | | - s += requirement.kind; |
334 | | - s += ':'; |
335 | | - s += requirement.value; |
336 | | - s += ':'; |
337 | | - s += requirement.phase; |
338 | | - s += requirement.required ? ":required" : ":optional"; |
339 | | - } |
340 | | - for (auto const& artifact : runtime.artifacts) { |
341 | | - s += " runtime-artifact:"; |
342 | | - s += artifact.role; |
343 | | - s += ':'; |
344 | | - s += artifact.path.generic_string(); |
345 | | - s += ':'; |
346 | | - s += artifact.provenance; |
347 | | - s += ':'; |
348 | | - s += artifact.abi; |
349 | | - s += ':'; |
350 | | - s += artifact.digest; |
351 | | - s += ':'; |
352 | | - s += artifact.hostFingerprint; |
353 | | - } |
354 | | - for (auto const& value : runtime.linkIntent.libraries) |
355 | | - s += " link-library:" + value; |
356 | | - for (auto const& value : runtime.linkIntent.linkLibraryDirs) |
357 | | - s += " link-dir:" + value.generic_string(); |
358 | | - for (auto const& value : runtime.linkIntent.transitiveNeededDirs) |
359 | | - s += " needed-dir:" + value.generic_string(); |
360 | | - for (auto const& value : runtime.linkIntent.runtimeSearchDirs) |
361 | | - s += " runtime-dir:" + value.generic_string(); |
362 | | - for (auto const& value : runtime.linkIntent.frameworks) |
363 | | - s += " framework:" + value; |
364 | | - for (auto const& value : runtime.linkIntent.deployFiles) |
365 | | - s += " deploy:" + value.generic_string(); |
366 | | - // Legacy fields remain fingerprinted while they are readable. |
367 | | - for (auto const& value : runtime.libraryDirs) |
368 | | - s += " legacy-runtime-dir:" + value.generic_string(); |
369 | | - for (auto const& value : runtime.dlopenLibs) |
370 | | - s += " legacy-soname:" + value; |
371 | | - for (auto const& value : runtime.capabilities) |
372 | | - s += " legacy-capability:" + value; |
373 | | - for (auto const& value : runtime.provides) |
374 | | - s += " legacy-provides:" + value; |
375 | | - for (auto const& [capability, provider] : runtime.providerOverrides) |
376 | | - s += " provider-override:" + capability + '=' + provider; |
377 | | - if (!pkg.manifest.buildConfig.cStandard.empty()) { |
378 | | - s += " c_standard="; |
379 | | - s += pkg.manifest.buildConfig.cStandard; |
380 | | - } |
381 | | - for (auto const& flag : pkg.manifest.buildConfig.cflags) { |
382 | | - s += " cflag:"; |
383 | | - s += flag; |
384 | | - } |
385 | | - for (auto const& flag : pkg.manifest.buildConfig.cxxflags) { |
386 | | - s += " cxxflag:"; |
387 | | - s += flag; |
388 | | - } |
389 | | - for (auto const& flag : pkg.manifest.buildConfig.ldflags) { |
390 | | - s += " ldflag:"; |
391 | | - s += flag; |
392 | | - } |
393 | | - // Per-glob flags — same full ordered serialization as the root-side |
394 | | - // block above. Until #253 dependency globFlags were unfingerprinted |
395 | | - // (held only by "descriptor frozen per version" + "feature toggles |
396 | | - // always change cflags via -DMCPP_FEATURE_*"); feature-folded entries |
397 | | - // make the vector build-variant, so fingerprint it directly. |
398 | | - // featureOrigin is diagnostic-only and deliberately NOT serialized |
399 | | - // (the active feature set is already in cflags above). |
400 | | - for (auto const& gf : pkg.manifest.buildConfig.globFlags) { |
401 | | - s += " globflags:"; s += gf.glob; |
402 | | - for (auto const& f : gf.cflags) { s += " gc:"; s += f; } |
403 | | - for (auto const& f : gf.cxxflags) { s += " gxx:"; s += f; } |
404 | | - for (auto const& f : gf.asmflags) { s += " gas:"; s += f; } |
405 | | - for (auto const& f : gf.defines) { s += " gd:"; s += f; } |
406 | | - } |
407 | | - // Same reason as the root block, and it cannot be skipped on the |
408 | | - // grounds that "a descriptor is frozen per version": path and git |
409 | | - // dependencies are not frozen, and this key changes their products. |
410 | | - for (auto const& e : pkg.manifest.buildConfig.moduleExtensions) { |
411 | | - s += " modext:"; |
412 | | - s += e; |
413 | | - } |
414 | | - if (pkg.usageResolved) { |
415 | | - for (auto const& dir : pkg.privateBuild.includeDirs) { |
416 | | - s += " private_include:"; |
417 | | - s += dir.generic_string(); |
418 | | - } |
419 | | - for (auto const& dir : pkg.publicUsage.includeDirs) { |
420 | | - s += " public_include:"; |
421 | | - s += dir.generic_string(); |
422 | | - } |
423 | | - for (auto const& dir : pkg.privateBuild.includeDirsAfter) { |
424 | | - s += " private_include_after:"; |
425 | | - s += dir.generic_string(); |
426 | | - } |
427 | | - for (auto const& dir : pkg.publicUsage.includeDirsAfter) { |
428 | | - s += " public_include_after:"; |
429 | | - s += dir.generic_string(); |
430 | | - } |
431 | | - } |
432 | | - for (auto const& [path, content] : pkg.manifest.buildConfig.generatedFiles) { |
433 | | - s += " genfile:"; |
434 | | - s += path.generic_string(); |
435 | | - s += "="; |
436 | | - s += content; |
437 | | - } |
438 | | - } |
439 | | - return s; |
440 | | -} |
441 | | - |
442 | 107 | std::expected<void, std::string> |
443 | 108 | materialize_generated_files(const std::filesystem::path& root, |
444 | 109 | const mcpp::manifest::Manifest& manifest) |
|
0 commit comments