diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index 740f8157..d5233cd7 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -141,6 +141,7 @@ c_standard = "c11" # Standard for C source files (default c11) cflags = ["-DFOO=1"] # Extra C compile flags cxxflags = ["-DBAR=2"] # Extra C++ compile flags (do not put -std=... here) ldflags = ["-lfoo"] # Extra link flags +defines = ["BIZ=1", "QUX"] # Preprocessor macros for every TU (desugars to -D; reaches module scans) static_stdlib = true # Statically link libstdc++ (default true) target = "x86_64-linux-musl" # Default build target when no --target is passed # (≙ cargo build.target; e.g. "ship fully-static") @@ -182,6 +183,34 @@ then only guaranteed to run on the build machine's version and above). A lower floor (11–13) requires a self-built libc++ archive (already verified to work, a data-level switch, available on request). +`defines` takes **bare** macro names (no `-D`) and desugars each entry to `-D` on +both the C and C++ compile channels. It reaches every TU in the package — module +interface units included — so it also reaches the P1689 module scan, which is what +makes a macro-guarded `import` resolvable. Assembly units pick it up too. It is a +build input like any other, so `[target.'cfg(...)'.build]` can carry it: + +```toml +[build] +defines = ["APP_NAME=\"demo\""] + +[target.'cfg(windows)'.build] +defines = ["USE_WIN32", "WINVER=0x0A00"] +``` + +Picking the right axis: + +| You want the macro on… | Use | +|---|---| +| every TU of this package | `[build].defines` (here) | +| one binary's own entry source only | `[targets.].defines` | +| a specific set of files | `[build].flags` with a `glob` + `defines` | +| every TU **and** every consumer's TUs | `[features.].defines` (an interface contribution) | + +`[build].defines` is private to the package: it does not propagate to consumers. + +Unsupported keys under `[build]` are reported as a warning (an error under +`--strict`) rather than silently ignored. + Do not configure the C++ standard via `build.cxxflags = ["-std=..."]`. Instead use: ```toml diff --git a/docs/zh/05-mcpp-toml.md b/docs/zh/05-mcpp-toml.md index 04c5c318..6063a2b0 100644 --- a/docs/zh/05-mcpp-toml.md +++ b/docs/zh/05-mcpp-toml.md @@ -136,6 +136,7 @@ c_standard = "c11" # C 源文件的标准(默认 c11) cflags = ["-DFOO=1"] # 额外 C 编译参数 cxxflags = ["-DBAR=2"] # 额外 C++ 编译参数(不要放 -std=...) ldflags = ["-lfoo"] # 额外链接参数 +defines = ["BIZ=1", "QUX"] # 作用于每个 TU 的预处理宏(脱糖为 -D;会进入模块扫描) static_stdlib = true # 静态链接 libstdc++(默认 true) macos_deployment_target = "14.0" # macOS 产物的最低支持系统版本(仅 macOS 生效) ``` @@ -166,6 +167,32 @@ cargo/rustc、cc 等同样尊重该变量)> 本字段(项目默认,类似 SwiftP 系统 libc++(产物只保证在构建机同版本及以上运行)。更低 floor(11–13) 需自建 libc++ 归档(已验证可行,数据级切换,按需提供)。 +`defines` 接受**裸**宏名(不带 `-D`),把每个条目脱糖为 `-D`,同时作用于 C 和 +C++ 编译通道。它覆盖包内每个 TU(含模块接口单元),因此也会进入 P1689 模块扫描 +—— 这正是被宏保护的 `import` 能被解析的前提。汇编单元同样能拿到。它是普通的构建 +输入,所以 `[target.'cfg(...)'.build]` 也能承载它: + +```toml +[build] +defines = ["APP_NAME=\"demo\""] + +[target.'cfg(windows)'.build] +defines = ["USE_WIN32", "WINVER=0x0A00"] +``` + +选择合适的轴: + +| 想让宏作用于… | 用 | +|---|---| +| 本包的每个 TU | `[build].defines`(本节) | +| 仅某个二进制自己的入口源 | `[targets.].defines` | +| 指定的一批文件 | `[build].flags` 配 `glob` + `defines` | +| 本包每个 TU **以及**消费者的 TU | `[features.].defines`(接口贡献) | + +`[build].defines` 是包私有的:不会传播给消费者。 + +`[build]` 下不支持的键会作为警告报出(`--strict` 下为错误),而不是被静默忽略。 + C++ 标准不要通过 `build.cxxflags = ["-std=..."]` 配置。请使用: ```toml diff --git a/mcpp.toml b/mcpp.toml index 0890e94f..d9d6d459 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.7.27.1" +version = "2026.7.28.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index e2a8bd06..6f2612c9 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -442,6 +442,28 @@ void merge_conditional_build_inputs(mcpp::manifest::Manifest& m, } } +// Desugar `[build].defines` into `-D` on both C and C++ flag channels. +// +// ORDER (both halves are load-bearing): this must run AFTER +// merge_conditional_build_inputs — `defines` is a BuildInputs member, so a +// matching `[target.'cfg(...)'.build] defines` has been appended by then and +// folds in the same pass, landing after the unconditional entries so GNU +// last-wins gives the conditional rule precedence — and BEFORE the manifest is +// snapshotted into packages[] / fingerprinted, because that snapshot (not the +// manifest) is what the P1689 scan, the compile edges and compute_fingerprint +// actually read. +// +// Idempotent: clearing the vector after folding makes repeated calls harmless. +// Both `cflags` and `cxxflags` get the macro; assembly units pick it up for +// free via the -D/-U/-I subset the ninja backend filters out of packageCflags. +void fold_build_defines_into_flags(mcpp::manifest::BuildConfig& bc) { + for (auto const& d : bc.defines) { + bc.cflags.push_back("-D" + d); + bc.cxxflags.push_back("-D" + d); + } + bc.defines.clear(); +} + // Feature-activation closure — THE single implementation (build.mcpp env // contract, Stage 2a feature-deps, and the main feature pass all call this): // seed = [features].default ∪ requested, expanded transitively over implies; @@ -943,6 +965,10 @@ prepare_build(bool print_fingerprint, m->buildDependencies.insert(cc.buildDependencies.begin(), cc.buildDependencies.end()); } } + // `[build].defines` must reach the scanner (P1689) and the compile edge, + // and must participate in the fingerprint. Fold before dependency + // resolution / fingerprinting. + fold_build_defines_into_flags(m->buildConfig); // msvc@system: a *system* toolchain — located on the machine, never // resolved through xim packages. mcpp does not install MSVC. @@ -2073,6 +2099,7 @@ prepare_build(bool print_fingerprint, cfgpred::context_for(overrides.target_triple), overrides.target_triple); } + fold_build_defines_into_flags(manifest->buildConfig); return std::pair{effRoot, std::move(*manifest)}; }; @@ -2964,6 +2991,7 @@ prepare_build(bool print_fingerprint, cfgpred::context_for(overrides.target_triple), overrides.target_triple); } + fold_build_defines_into_flags(dep_manifest->buildConfig); } else { auto loaded = loadVersionDep(name, key.ns, key.shortName, spec.version); if (!loaded) return std::unexpected(loaded.error()); diff --git a/src/manifest/toml.cppm b/src/manifest/toml.cppm index 77c5a661..fb9a6119 100644 --- a/src/manifest/toml.cppm +++ b/src/manifest/toml.cppm @@ -841,6 +841,7 @@ std::expected parse_string(std::string_view content, if (auto v = doc->get_bool("build.allow_host_libs")) m.buildConfig.allowHostLibs = *v; if (auto v = doc->get_string_array("build.cflags")) m.buildConfig.cflags = *v; if (auto v = doc->get_string_array("build.cxxflags")) m.buildConfig.cxxflags = *v; + if (auto v = doc->get_string_array("build.defines")) m.buildConfig.defines = *v; // Module-graph-global dialect flags (issue #210) — see types.cppm // dialect_flags(); this key is the explicit escape hatch for flags the // known-list doesn't recognize yet. @@ -888,6 +889,38 @@ std::expected parse_string(std::string_view content, if (val.is_string()) m.xlings.envs[k] = val.as_string(); if (auto v = doc->get_string("build.macos_deployment_target")) m.buildConfig.macosDeploymentTarget = *v; + + // Surface unsupported [build] keys instead of silently dropping them. + // #296 is #131's footgun one section over: `[build] defines` on an mcpp + // that did not know the key simply vanished, and the build then failed + // much later with a module-graph divergence naming neither the key nor + // the manifest. An unknown key in a section this central is always a typo + // or a version mismatch, never an intentional no-op — so say so. Same + // policy as [targets.] above: a warning, an error under --strict. + // + // MUST stay in sync with the `doc->get_*("build.")` reads above. + static constexpr std::string_view kKnownBuildKeys[] = { + "allow_host_libs", "c_standard", "cflags", "cxxflags", + "default-profile", "defines", "dialect_cxxflags", "flags", + "include_dirs", "include_dirs_after", "ldflags", + "macos_deployment_target", "profile", "sources", "static_stdlib", + "target", + }; + if (auto* bt = doc->get_table("build")) { + for (auto& [key, _] : *bt) { + bool known = false; + for (auto k : kKnownBuildKeys) if (key == k) { known = true; break; } + if (!known) { + m.schemaWarnings.push_back(std::format( + "[build] has unsupported key '{}' (ignored). Supported keys: " + "sources, cflags, cxxflags, ldflags, defines, flags, " + "include_dirs, include_dirs_after, dialect_cxxflags, " + "c_standard, target, static_stdlib, allow_host_libs, " + "profile, macos_deployment_target.", key)); + } + } + } + for (auto const& flag : m.buildConfig.cxxflags) { if (starts_with_std_flag(flag)) { return std::unexpected(error(origin, @@ -991,6 +1024,12 @@ std::expected parse_string(std::string_view content, read_list("cxxflags", cc.inputs.cxxflags); read_list("ldflags", cc.inputs.ldflags); read_list("sources", cc.inputs.sources); + // #296: package-level macros are a build input like any other, + // so the cfg axis carries them too — a platform-only macro + // (`[target.'cfg(windows)'.build] defines = ["USE_WIN32"]`) + // must reach the scan and the compile exactly like an + // unconditional one. + read_list("defines", cc.inputs.defines); // #258: per-glob flags and include dirs, through the SAME entry // grammar `[build].flags` uses — a conditional section is just // a set of build inputs, so it reads the same way. @@ -1003,6 +1042,31 @@ std::expected parse_string(std::string_view content, cc.inputs.globFlags)) return std::unexpected(error(origin, *err)); } + // The conditional axis carries BuildInputs and nothing else, so + // its vocabulary is exactly that struct's members — a key + // outside it (`static_stdlib`, `target`, a profile knob) is not + // conditionable and would otherwise vanish without a word, the + // #296 failure mode. MUST stay in sync with the reads above and + // with types.cppm's BuildInputs. + static constexpr std::string_view kKnownConditionalBuildKeys[] = { + "cflags", "cxxflags", "defines", "flags", + "include_dirs", "include_dirs_after", "ldflags", "sources", + }; + for (auto& [key, _] : bt) { + bool known = false; + for (auto k : kKnownConditionalBuildKeys) + if (key == k) { known = true; break; } + if (!known) { + m.schemaWarnings.push_back(std::format( + "[target.{}.build] has unsupported key '{}' (ignored). " + "A conditional section may only contribute build INPUTS: " + "sources, cflags, cxxflags, ldflags, defines, flags, " + "include_dirs, include_dirs_after. Selection knobs " + "(target, linkage, static_stdlib) and profile settings " + "are resolved before the predicate is evaluated and " + "cannot be conditioned.", triple, key)); + } + } } // [target..{dependencies,dev-dependencies,build-dependencies}] // parsed via the shared table-based loader (same selectors/namespaces @@ -1019,6 +1083,7 @@ std::expected parse_string(std::string_view content, if (auto r = read_deps("build-dependencies", cc.buildDependencies); !r) return std::unexpected(r.error()); if (!cc.inputs.cflags.empty() || !cc.inputs.cxxflags.empty() || !cc.inputs.ldflags.empty() || !cc.inputs.sources.empty() + || !cc.inputs.defines.empty() || !cc.inputs.globFlags.empty() || !cc.inputs.includeDirs.empty() || !cc.inputs.includeDirsAfter.empty() || !cc.dependencies.empty() || !cc.devDependencies.empty() diff --git a/src/manifest/types.cppm b/src/manifest/types.cppm index 2c65fe17..8f931708 100644 --- a/src/manifest/types.cppm +++ b/src/manifest/types.cppm @@ -171,6 +171,17 @@ struct BuildInputs { std::vector cflags; std::vector cxxflags; std::vector ldflags; + // #296: package-level preprocessor macros. Unlike per-target `defines` + // (which only reach the binary's own entry TU), these reach EVERY TU in + // the package — module interface units included — so they participate in + // the P1689 module scan, which is what makes a macro-guarded `import` + // resolvable. Desugared to `-D` on both the C and C++ channels + // (fold_build_defines_into_flags in prepare.cppm) after the conditional + // merge and before the manifest is snapshotted into packages[] / + // fingerprinted. A member HERE rather than on BuildConfig so the cfg axis + // can carry it: `[target.'cfg(windows)'.build] defines = [...]` must work, + // and membership of this type is what guarantees it (see above). + std::vector defines; std::vector globFlags; // flags = [...] (ordered) std::vector includeDirs; // relative to package root // #249: emitted as -idirafter (searched after the toolchain's system dirs) @@ -185,6 +196,7 @@ inline void append(BuildInputs& dst, const BuildInputs& src) { dst.cflags.insert(dst.cflags.end(), src.cflags.begin(), src.cflags.end()); dst.cxxflags.insert(dst.cxxflags.end(), src.cxxflags.begin(), src.cxxflags.end()); dst.ldflags.insert(dst.ldflags.end(), src.ldflags.begin(), src.ldflags.end()); + dst.defines.insert(dst.defines.end(), src.defines.begin(), src.defines.end()); dst.globFlags.insert(dst.globFlags.end(), src.globFlags.begin(), src.globFlags.end()); dst.includeDirs.insert(dst.includeDirs.end(), diff --git a/src/manifest/xpkg.cppm b/src/manifest/xpkg.cppm index b6451638..b5645ae6 100644 --- a/src/manifest/xpkg.cppm +++ b/src/manifest/xpkg.cppm @@ -202,7 +202,7 @@ namespace mcpp::manifest { // Kept as one array so the did-you-mean suggester speaks the same vocabulary // the parser accepts. inline constexpr std::string_view kKnownXpkgKeys[] = { - "cflags", "c_standard", "cxxflags", "deps", "features", "flags", + "cflags", "c_standard", "cxxflags", "defines", "deps", "features", "flags", "generated_files", "import_std", "include_dirs", "include_dirs_after", "language", "ldflags", "linux", "macosx", "modules", "provides", "runtime", "scan_overrides", @@ -215,8 +215,10 @@ inline constexpr std::pair kXpkgKeyAliases[] { "dependencies", "deps" }, { "dependency", "deps" }, { "requires", "deps" }, // cargo muscle memory - { "define", "flags" }, - { "defines", "flags" }, + // #296: `defines` is a real key now (package-level bare macros). Only the + // singular misspelling needs redirecting, and it points at `defines` — the + // old redirect to `flags` predates the key existing. + { "define", "defines" }, { "feature", "features" }, { "source", "sources" }, { "target", "targets" }, @@ -1261,6 +1263,7 @@ synthesize_from_xpkg_lua(std::string_view luaContent, : sub == "cxxflags" ? &cc.inputs.cxxflags : sub == "ldflags" ? &cc.inputs.ldflags : sub == "sources" ? &cc.inputs.sources + : sub == "defines" ? &cc.inputs.defines : nullptr; // Unknown sub-keys stay a HARD ERROR here. The shared // BuildInputs parser must not be read as licence to relax @@ -1273,8 +1276,8 @@ synthesize_from_xpkg_lua(std::string_view luaContent, if (!dst && !pathDst) { return std::unexpected(ManifestError{ std::format("unknown target_cfg key '{}' (expected " - "cflags/cxxflags/ldflags/sources/flags/" - "include_dirs/include_dirs_after)", sub), + "cflags/cxxflags/ldflags/sources/defines/" + "flags/include_dirs/include_dirs_after)", sub), m.sourcePath, 0, 0}); } if (!cur.consume('=') || !cur.consume('{')) { @@ -1298,6 +1301,7 @@ synthesize_from_xpkg_lua(std::string_view luaContent, cur.consume('}'); if (!cc.inputs.cflags.empty() || !cc.inputs.cxxflags.empty() || !cc.inputs.ldflags.empty() || !cc.inputs.sources.empty() + || !cc.inputs.defines.empty() || !cc.inputs.globFlags.empty() || !cc.inputs.includeDirs.empty() || !cc.inputs.includeDirsAfter.empty()) @@ -1517,19 +1521,28 @@ synthesize_from_xpkg_lua(std::string_view luaContent, } cur.consume('}'); } - else if (key == "cflags" || key == "cxxflags" || key == "ldflags") { + else if (key == "cflags" || key == "cxxflags" || key == "ldflags" + || key == "defines") { // `{ "-Dfoo", "-Wall", ... }` — appended to the per-rule baseline // by ninja_backend. cflags goes to the C rule (.c files), cxxflags // to C++ rule (.cpp/.cc/.cxx/.cppm), ldflags to link commands. + // + // #296: `defines` carries BARE macro names (`{ "USE_FOO", "N=1" }`, + // no `-D`) and desugars onto BOTH compile channels at prepare time, + // which is what lets it reach the P1689 scan. Same key, same + // meaning as `[build].defines` in an mcpp.toml — the two manifest + // surfaces are two spellings of one schema, so a key that exists in + // one must not be a did-you-mean error in the other. if (!cur.consume('{')) { return std::unexpected(ManifestError{ std::format("expected '{{' after `{} =`", key), m.sourcePath, 0, 0}); } cur.skip_ws_and_comments(); - auto& target = (key == "cflags") - ? m.buildConfig.cflags - : (key == "cxxflags" ? m.buildConfig.cxxflags : m.buildConfig.ldflags); + auto& target = (key == "cflags") ? m.buildConfig.cflags + : (key == "cxxflags") ? m.buildConfig.cxxflags + : (key == "defines") ? m.buildConfig.defines + : m.buildConfig.ldflags; while (!cur.eof() && cur.peek() != '}') { auto s = cur.read_string(); if (key == "cxxflags" && starts_with_std_flag(s)) { diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index cabf91c9..9d8f6933 100644 --- a/src/toolchain/fingerprint.cppm +++ b/src/toolchain/fingerprint.cppm @@ -18,7 +18,7 @@ import mcpp.toolchain.detect; export namespace mcpp::toolchain { -inline constexpr std::string_view MCPP_VERSION = "2026.7.27.1"; +inline constexpr std::string_view MCPP_VERSION = "2026.7.28.1"; struct FingerprintInputs { Toolchain toolchain; diff --git a/tests/e2e/167_build_defines_module_scan.sh b/tests/e2e/167_build_defines_module_scan.sh new file mode 100644 index 00000000..d1a9bcd6 --- /dev/null +++ b/tests/e2e/167_build_defines_module_scan.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +# requires: +# 167_build_defines_module_scan.sh — mcpp#296: `[build].defines` is the +# package-level macro channel. It must reach EVERY TU (module interface units +# included) on BOTH the compile edge and the P1689 module scan, and the cfg +# axis must be able to carry it like any other build input. +# +# The original report: an `import` guarded by a `[build].defines` macro was +# invisible to the scanner while `scan_overrides` asserted it existed, so the +# build died with "module-graph divergence" naming neither the key nor the +# manifest — because the key was read by nothing and dropped in silence. +# +# Deliberately toolchain-neutral: the module-graph half uses a LOCAL module +# rather than `import std`, so this runs on every platform — which matters, +# since #296 was reported on Windows. Macro delivery is asserted with #error +# guards, so the compiler itself is the assertion (cf. 85_target_cfg_build_flags). +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT +cd "$TMP" + +# ── 1. the #296 shape: a macro-guarded import must reach the P1689 scan ── +# scan_overrides declares that `mid` imports `lib`. That only holds if +# -DUSE_LIB reached the SCAN of mid.cppm; otherwise the scanner sees no import +# and the planner's assumption diverges from the compiler's. +mkdir -p scan/src +cat > scan/src/lib.cppm <<'EOF' +export module lib; +export int lib_value() { return 42; } +EOF +cat > scan/src/mid.cppm <<'EOF' +export module mid; +#ifdef USE_LIB +import lib; +#else +#error "[build].defines did not reach the module interface unit mid.cppm" +#endif +export int mid_value() { return lib_value(); } +EOF +cat > scan/src/main.cpp <<'EOF' +import mid; +// The same macro must reach an ordinary TU, and must carry a value and a +// value CONTAINING A SPACE (the -D quoting path, where the space is part of +// one argv token rather than a token boundary — mcpp#234). +#ifndef USE_LIB +#error "[build].defines did not reach main.cpp" +#endif +#if LEVEL != 3 +#error "[build].defines dropped or mangled a NAME=value entry" +#endif +BIGINT wide = 1; +int main() { return (mid_value() == 42 && wide == 1) ? 0 : 1; } +EOF +cat > scan/mcpp.toml <<'EOF' +[package] +name = "scan" +version = "0.1.0" + +[build] +defines = ["USE_LIB", "LEVEL=3", "BIGINT=long long"] + +[scan_overrides."src/lib.cppm"] +provides = ["lib"] + +[scan_overrides."src/mid.cppm"] +provides = ["mid"] +imports = ["lib"] + +[scan_overrides."src/main.cpp"] +imports = ["mid"] +EOF + +cd scan +"$MCPP" build > build.log 2>&1 || { cat build.log + echo "FAIL: [build].defines did not reach the scan and/or the compile"; exit 1; } +# Run on its own line, not inside a pipeline: `x=$(cmd | tail)` takes the exit +# status of `tail`, so a crashing binary would be reported as a string mismatch +# (or not at all). +"$MCPP" run > run.log 2>&1 || { cat run.log; echo "FAIL: built binary did not run"; exit 1; } +cd "$TMP" + +# ── 2. the cfg axis carries `defines` too (#296 regression guard) ── +# `defines` is a BuildInputs member, so `[target.'cfg(...)'.build]` must merge +# it. HOST-AWARE, like 85_target_cfg_build_flags.sh: exactly the host's +# predicates apply, on whichever platform the runner is. +mkdir -p cond/src +cat > cond/src/main.cpp <<'EOF' +#if (defined(COND_LINUX) + defined(COND_MACOS) + defined(COND_WIN)) != 1 +#error "exactly one conditional [build].defines entry must apply on any host" +#endif +#ifndef BASE_DEFINE +#error "the unconditional [build].defines entry was lost when a cfg section merged" +#endif +int main() { return 0; } +EOF +cat > cond/mcpp.toml <<'EOF' +[package] +name = "cond" +version = "0.1.0" + +[build] +defines = ["BASE_DEFINE"] + +[target.'cfg(linux)'.build] +defines = ["COND_LINUX"] +[target.'cfg(macos)'.build] +defines = ["COND_MACOS"] +[target.'cfg(windows)'.build] +defines = ["COND_WIN"] +EOF +cd cond +"$MCPP" build > build.log 2>&1 || { cat build.log + echo "FAIL: conditional [build].defines did not reach the TU"; exit 1; } +cd "$TMP" + +# ── 3. a DEPENDENCY's own [build].defines reaches the dependency's TUs ── +# The root package must not be the only one folded: prepare_build folds at the +# path/git-dep site too, mirroring the #229 conditional-merge funnel. +mkdir -p app/src dep/src +cat > dep/src/dep.cppm <<'EOF' +export module dep; +#ifndef DEP_OWN_DEFINE +#error "the dependency's own [build].defines did not reach its module unit" +#endif +export int dep_value() { return 7; } +EOF +cat > dep/mcpp.toml <<'EOF' +[package] +name = "dep" +version = "0.1.0" + +[build] +defines = ["DEP_OWN_DEFINE"] + +[targets.dep] +kind = "lib" +EOF +cat > app/src/main.cpp <<'EOF' +import dep; +// A dependency's defines are PRIVATE build inputs — they must not leak into +// the consumer (propagating macros is what feature `defines` are for). +#ifdef DEP_OWN_DEFINE +#error "a dependency's [build].defines leaked into the consumer" +#endif +int main() { return dep_value() == 7 ? 0 : 1; } +EOF +cat > app/mcpp.toml <<'EOF' +[package] +name = "app" +version = "0.1.0" + +[dependencies] +dep = { path = "../dep" } +EOF +cd app +"$MCPP" build > build.log 2>&1 || { cat build.log + echo "FAIL: a dependency's [build].defines did not reach its own TUs"; exit 1; } +cd "$TMP" + +# ── 4. an unknown [build] key is reported, not silently dropped ── +# The root cause of #296: `defines` was not a key at all, and [build] swallowed +# it without a word. Same policy as [targets.] — a warning, an error +# under --strict. +mkdir -p unknown/src +cat > unknown/src/main.cpp <<'EOF' +int main() { return 0; } +EOF +cat > unknown/mcpp.toml <<'EOF' +[package] +name = "unknown" +version = "0.1.0" + +[build] +defnes = ["TYPO"] +EOF +cd unknown +"$MCPP" build > build.log 2>&1 || { cat build.log + echo "FAIL: an unknown [build] key must warn, not fail the build"; exit 1; } +grep -q "defnes" build.log || { + cat build.log; echo "FAIL: unknown [build] key 'defnes' was dropped in silence"; exit 1; } +if "$MCPP" build --strict > strict.log 2>&1; then + cat strict.log + echo "FAIL: --strict must reject an unknown [build] key"; exit 1 +fi +grep -q "defnes" strict.log || { + cat strict.log; echo "FAIL: --strict rejection did not name the offending key"; exit 1; } + +echo "OK" diff --git a/tests/e2e/93_xpkg_parse.sh b/tests/e2e/93_xpkg_parse.sh index 5aed9302..8012e596 100755 --- a/tests/e2e/93_xpkg_parse.sh +++ b/tests/e2e/93_xpkg_parse.sh @@ -22,6 +22,12 @@ package = { mcpp = { schema = "0.1", sources = { [[mcpp_generated/gen.cc]] }, + -- mcpp#296: `defines` is a real descriptor key (package-level bare + -- macros), not a did-you-mean redirect to `flags`. The two manifest + -- surfaces are two spellings of one schema, so a key accepted in an + -- mcpp.toml must not be an unknown-key error here. The strict parse + -- below plus the `unknown_keys == []` assertion are the guard. + defines = { [[XPKG_DEFINE]], [[N=1]] }, generated_files = { ["mcpp_generated/gen.cc"] = [==[ export module gen; diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index fcc40362..69bdd65d 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -341,6 +341,181 @@ kind = "lib" EXPECT_EQ(m->buildConfig.cStandard, "c11"); } +// #296: [build].defines is sugar for `-D` on both C and C++ channels; it +// must parse into buildConfig.defines so prepare_build can fold it into flags +// before the P1689 scan and fingerprint. Order is preserved because the fold +// appends in declaration order and "last flag wins" is the override semantics. +TEST(Manifest, BuildDefinesParsesIntoSeparateVector) { + constexpr auto src = R"( +[package] +name = "x" +version = "0.1.0" +[build] +defines = ["TEST_USE_MODULES", "VALUE=42"] +)"; + auto m = mcpp::manifest::parse_string(src); + ASSERT_TRUE(m.has_value()) << m.error().format(); + ASSERT_EQ(m->buildConfig.defines.size(), 2u); + EXPECT_EQ(m->buildConfig.defines[0], "TEST_USE_MODULES"); + EXPECT_EQ(m->buildConfig.defines[1], "VALUE=42"); + // A well-formed [build] must not warn — the unknown-key guard below is + // only allowed to fire on keys the parser genuinely does not read. + EXPECT_TRUE(m->schemaWarnings.empty()) + << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); +} + +// #296: `defines` is a BuildInputs member, NOT a BuildConfig-only field — so +// the cfg axis carries it like any other build input. This is the regression +// that matters: a platform-only macro must be expressible, because a key that +// parses unconditionally but vanishes under `[target.'cfg(...)'.build]` is the +// exact silent-drop failure #296 was filed for, just one section over. +TEST(Manifest, ConditionalBuildDefinesAreCarriedByTheCfgAxis) { + constexpr auto src = R"( +[package] +name = "x" +version = "0.1.0" +[build] +defines = ["BASE"] +[target.'cfg(windows)'.build] +defines = ["USE_WIN32", "WINVER=0x0A00"] +)"; + auto m = mcpp::manifest::parse_string(src); + ASSERT_TRUE(m.has_value()) << m.error().format(); + ASSERT_EQ(m->conditionalConfigs.size(), 1u); + EXPECT_EQ(m->conditionalConfigs[0].predicate, "cfg(windows)"); + ASSERT_EQ(m->conditionalConfigs[0].inputs.defines.size(), 2u); + EXPECT_EQ(m->conditionalConfigs[0].inputs.defines[0], "USE_WIN32"); + EXPECT_EQ(m->conditionalConfigs[0].inputs.defines[1], "WINVER=0x0A00"); + EXPECT_TRUE(m->schemaWarnings.empty()) + << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); +} + +// A conditional section carrying ONLY `defines` must still be recorded — the +// emptiness gate that decides whether to push a ConditionalConfig has to know +// about every BuildInputs member, or the section is dropped before it is ever +// evaluated. +TEST(Manifest, ConditionalSectionWithOnlyDefinesIsRecorded) { + constexpr auto src = R"( +[package] +name = "x" +version = "0.1.0" +[target.'cfg(linux)'.build] +defines = ["ONLY_DEFINE"] +)"; + auto m = mcpp::manifest::parse_string(src); + ASSERT_TRUE(m.has_value()) << m.error().format(); + ASSERT_EQ(m->conditionalConfigs.size(), 1u); + ASSERT_EQ(m->conditionalConfigs[0].inputs.defines.size(), 1u); + EXPECT_EQ(m->conditionalConfigs[0].inputs.defines[0], "ONLY_DEFINE"); +} + +// The single additive merge must carry `defines` too — merge_conditional_ +// build_inputs routes the cfg axis through append(), so a member missing here +// is a member that silently never reaches the build. +TEST(Manifest, AppendBuildInputsMergesDefines) { + mcpp::manifest::BuildInputs dst, src; + dst.defines = {"BASE"}; + src.defines = {"COND"}; + mcpp::manifest::append(dst, src); + ASSERT_EQ(dst.defines.size(), 2u); + EXPECT_EQ(dst.defines[0], "BASE"); + // Conditional entries land AFTER the base ones so GNU last-wins gives the + // conditional rule precedence (an off-OS `-U` after the base `-D`). + EXPECT_EQ(dst.defines[1], "COND"); +} + +// #296 root cause: an unknown [build] key used to vanish without a word, and +// the build then failed much later with a module-graph divergence naming +// neither the key nor the manifest. Same policy as [targets.]: a schema +// warning (an error under --strict). +TEST(Manifest, UnknownBuildKeyIsReportedNotSilentlyDropped) { + constexpr auto src = R"( +[package] +name = "x" +version = "0.1.0" +[build] +cxxflags = ["-Wall"] +defnes = ["TYPO"] +)"; + auto m = mcpp::manifest::parse_string(src); + ASSERT_TRUE(m.has_value()) << m.error().format(); + ASSERT_EQ(m->schemaWarnings.size(), 1u); + EXPECT_NE(m->schemaWarnings[0].find("defnes"), std::string::npos) + << m->schemaWarnings[0]; + EXPECT_NE(m->schemaWarnings[0].find("[build]"), std::string::npos) + << m->schemaWarnings[0]; + // The key is still ignored — this is a warning, not a parse failure. + EXPECT_EQ(m->buildConfig.cxxflags.size(), 1u); +} + +// The conditional axis may only contribute build INPUTS, so a selection knob +// under it is inexpressible by construction. Say so rather than dropping it. +TEST(Manifest, UnknownConditionalBuildKeyIsReported) { + constexpr auto src = R"( +[package] +name = "x" +version = "0.1.0" +[target.'cfg(windows)'.build] +cxxflags = ["-DOK"] +static_stdlib = false +)"; + auto m = mcpp::manifest::parse_string(src); + ASSERT_TRUE(m.has_value()) << m.error().format(); + ASSERT_EQ(m->schemaWarnings.size(), 1u); + EXPECT_NE(m->schemaWarnings[0].find("static_stdlib"), std::string::npos) + << m->schemaWarnings[0]; + EXPECT_NE(m->schemaWarnings[0].find("cfg(windows)"), std::string::npos) + << m->schemaWarnings[0]; +} + +// Every key the [build] parser actually reads must be absent from the +// unknown-key warning — a guard that fires on a supported key is worse than +// no guard, because --strict turns it into a build failure. +TEST(Manifest, EverySupportedBuildKeyPassesTheUnknownKeyGuard) { + constexpr auto src = R"( +[package] +name = "x" +version = "0.1.0" +[build] +sources = ["src/**/*.cpp"] +cflags = ["-O2"] +cxxflags = ["-Wall"] +ldflags = ["-lm"] +defines = ["A"] +flags = [{ glob = "src/*.c", defines = ["B"] }] +include_dirs = ["include"] +include_dirs_after = ["vendor"] +dialect_cxxflags = ["-fcontracts"] +c_standard = "c11" +target = "x86_64-linux-musl" +static_stdlib = true +allow_host_libs = false +profile = "dev" +macos_deployment_target = "14.0" +)"; + auto m = mcpp::manifest::parse_string(src); + ASSERT_TRUE(m.has_value()) << m.error().format(); + EXPECT_TRUE(m->schemaWarnings.empty()) + << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); +} + +// `default-profile` is the canonical spelling of the `profile` alias; both +// must clear the guard. +TEST(Manifest, BuildDefaultProfileSpellingPassesTheUnknownKeyGuard) { + constexpr auto src = R"( +[package] +name = "x" +version = "0.1.0" +[build] +default-profile = "dev" +)"; + auto m = mcpp::manifest::parse_string(src); + ASSERT_TRUE(m.has_value()) << m.error().format(); + EXPECT_EQ(m->buildConfig.defaultProfile, "dev"); + EXPECT_TRUE(m->schemaWarnings.empty()) + << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); +} + // #249: `[build] include_dirs_after` parses into buildConfig.includeDirsAfter // — the -idirafter channel (searched AFTER the toolchain's system dirs), so // an extracted-tarball root containing a file named like a standard header