Skip to content

Commit e65271e

Browse files
committed
feat(manifest): platform-conditional [target.'cfg(...)'.build] flags; v0.0.74
L1 of the manifest platform/environment design: a normal mcpp.toml can scope [build] cflags/cxxflags/ldflags to a target predicate via [target.'cfg(...)'.build], evaluated against the RESOLVED target (host triple for a native build, the --target triple for a cross build) — so conditional flags follow what the binary will run on, not the build host. Closes the gap that forces consumers to generate per-OS [build] flags out-of-band (e.g. mcpp-index's write_build_ldflags bash). Mechanism (à la carte: the dependency graph stays applicative; this is a declarative conditional over flags): - manifest.cppm: [target.<predicate>.build] parsed DEFERRED into Manifest::conditionalConfigs (parsing is target-agnostic). - prepare.cppm: a recursive cfg() evaluator (cfgpred::) over all/any/not + os/arch/family/env + bare windows/unix/linux/macos; the context is derived from the resolved target triple (or host for native). Matching predicates' flags merge into buildConfig right after --target resolution, mirroring the [profile] merge. - Cargo-style table syntax, vcpkg-trimmed token set; precedence and grammar per .agents/docs/2026-06-29-manifest-environment-and-platform-design.md. Conditional [dependencies] + lazy fetch are the documented fast-follow (Phase 1b). Test: tests/e2e/85_target_cfg_build_flags.sh (cfg(linux)/cfg(unix) apply on a Linux build; cfg(windows) does not; cfg(all(linux, not(arch="aarch64"))) applies on x86_64 Linux). Regression: unit 27/0, e2e 70/73, self-host build at 0.0.74.
1 parent 95335f0 commit e65271e

6 files changed

Lines changed: 243 additions & 14 deletions

File tree

.agents/docs/2026-06-29-manifest-environment-and-platform-design.md

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# mcpp.toml: Build Environment, Platform-Conditional Config, and `build.mcpp` (Design)
22

33
Date: 2026-06-29
4-
Status: **Design — for discussion.** Synthesizes a multi-tool survey (Cargo, Zig,
4+
Status: **Phasing — L1 (flags) shipped in mcpp 0.0.74.** Remaining phases tracked
5+
in §"Phasing" below. Synthesizes a multi-tool survey (Cargo, Zig,
56
vcpkg, Bazel, xmake, Conan) and the build-systems literature (*Build Systems à la
67
Carte*, PubGrub, hermeticity/SLSA) against mcpp's current internals. Scope:
78
`src/manifest.cppm`, `src/config.cppm`, `src/xlings.cppm`, `src/toolchain/*`,
@@ -220,17 +221,25 @@ missing declared outputs as failure.
220221

221222
---
222223

223-
## Phasing (recommended order)
224-
225-
1. **L-1 environment** — highest ROI, self-contained, no upstream dep beyond mcpp; the
226-
xlings target model already exists. Surface `[environment]` + extend the project
227-
`.xlings.json` writer. Fold `[toolchain]` into `workspace`. Wire `[build-dependencies]`.
228-
2. **mcpp-index workspace** (companion doc) — the first real consumer; exercises L1's
229-
need (Windows-only `compat.openblas`).
230-
3. **L1 conditional graph**`[target.'cfg()']` deps+flags, target-evaluated; then
231-
`lazy` + content-hash identity.
232-
4. **L3 `build.mcpp`** — native build program with the two disciplines; backport the
233-
declared-I/O contract to recipe `install()`.
224+
## Phasing (status)
225+
226+
- **✅ Phase 1 — L1 conditional build flags (mcpp 0.0.74).** `[target.'cfg(...)'.build]`
227+
`cflags`/`cxxflags`/`ldflags`, parsed deferred into `Manifest::conditionalConfigs`
228+
(`manifest.cppm`), evaluated against the resolved target by a recursive `cfg()`
229+
predicate evaluator (`prepare.cppm` `cfgpred::`) and merged into `buildConfig` right
230+
after `--target` resolution. Grammar: `all/any/not` over `os`/`arch`/`family`/`env`
231+
+ bare `windows`/`unix`/`linux`/`macos`; native build → host coords, `--target`
232+
target coords. Test: `tests/e2e/85_target_cfg_build_flags.sh`.
233+
- **Phase 1b — L1 conditional dependencies + `lazy` fetch.** Same `[target.'cfg(...)']`
234+
namespace, `.dependencies`/`.dev-dependencies`/`.build-dependencies`; merge into
235+
`m->dependencies` in the same window (before dep resolution at `prepare.cppm:~731`).
236+
Add `lazy = true` (fetch only when a gated path requests it) + content-hash identity.
237+
- **Phase 2 — L-1 environment.** Surface `[environment]` → extend the project
238+
`.xlings.json` writer (`config.cppm:699-705`) to emit `deps`/`workspace`/`envs`/`subos`;
239+
fold `[toolchain]` into `workspace`; wire `[build-dependencies]`.
240+
- **Phase 3 — mcpp-index workspace** (companion doc) — first real consumer of Phase 1/1b.
241+
- **Phase 4 — L3 `build.mcpp`** — native build program (structured output + declared-I/O);
242+
backport the declared-I/O contract to recipe `install()`.
234243

235244
## Appendix — cross-tool summary
236245
- **Declarative-table vs imperative-script**: TOML is static data → declarative tables

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "0.0.73"
3+
version = "0.0.74"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/build/prepare.cppm

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,109 @@ import mcpp.project;
4343

4444
namespace mcpp::build {
4545

46+
// ── L1 platform-conditional config: cfg() predicate evaluation ──────────────
47+
// Context = the RESOLVED target's coordinates. A `[target.'cfg(...)'.build]`
48+
// predicate is evaluated against this (target triple for a cross build, host
49+
// for a native build), so conditional flags follow what the binary will run on
50+
// — not the build host. See the manifest design doc.
51+
namespace cfgpred {
52+
53+
struct Ctx { std::string os, arch, family, env; };
54+
55+
// Derive the cfg context from the resolved --target triple, falling back to the
56+
// host for a native build. OS/arch/env detection mirrors abi_profile's
57+
// substring approach (toolchain/abi.cppm) so the vocabulary is consistent.
58+
inline Ctx context_for(std::string_view targetTriple) {
59+
Ctx c;
60+
if (targetTriple.empty()) {
61+
c.os = std::string(mcpp::platform::name); // host: linux/macos/windows
62+
c.arch = std::string(mcpp::platform::host_arch); // host: x86_64/aarch64/...
63+
} else {
64+
auto has = [&](std::string_view n){ return targetTriple.find(n) != std::string_view::npos; };
65+
c.os = has("windows") || has("mingw") ? "windows"
66+
: has("darwin") || has("apple") || has("macos") ? "macos"
67+
: has("linux") ? "linux" : "";
68+
auto dash = targetTriple.find('-');
69+
c.arch = std::string(dash == std::string_view::npos ? targetTriple
70+
: targetTriple.substr(0, dash));
71+
}
72+
c.family = (c.os == "linux" || c.os == "macos") ? "unix"
73+
: (c.os == "windows") ? "windows" : "";
74+
// env (libc/abi): musl/gnu on linux, msvc on windows; substring or host default.
75+
if (!targetTriple.empty()) {
76+
auto has = [&](std::string_view n){ return targetTriple.find(n) != std::string_view::npos; };
77+
c.env = has("musl") ? "musl" : has("msvc") ? "msvc"
78+
: (has("gnu") || c.os == "linux") ? "gnu" : "";
79+
} else {
80+
c.env = c.os == "linux" ? "gnu" : c.os == "windows" ? "msvc" : "";
81+
}
82+
return c;
83+
}
84+
85+
// Recursive-descent evaluator over the inside of `cfg(...)`:
86+
// expr := all(list) | any(list) | not(expr) | key="value" | bareword
87+
// key ∈ {os, arch, family, env} bareword ∈ {windows, unix, linux, macos}
88+
struct Parser {
89+
std::string_view s; std::size_t i = 0; const Ctx& c;
90+
void ws() { while (i < s.size() && std::isspace((unsigned char)s[i])) ++i; }
91+
bool eat(char ch) { ws(); if (i < s.size() && s[i] == ch) { ++i; return true; } return false; }
92+
std::string ident() {
93+
ws(); std::size_t b = i;
94+
while (i < s.size() && (std::isalnum((unsigned char)s[i]) || s[i] == '_')) ++i;
95+
return std::string(s.substr(b, i - b));
96+
}
97+
std::string str() {
98+
ws(); if (i >= s.size() || s[i] != '"') return {};
99+
++i; std::size_t b = i; while (i < s.size() && s[i] != '"') ++i;
100+
auto v = std::string(s.substr(b, i - b)); if (i < s.size()) ++i; return v;
101+
}
102+
bool match_alias(const std::string& a) {
103+
if (a == "windows") return c.os == "windows";
104+
if (a == "linux") return c.os == "linux";
105+
if (a == "macos") return c.os == "macos";
106+
if (a == "unix") return c.family == "unix";
107+
return false; // unknown bareword → no match
108+
}
109+
bool match_kv(const std::string& k, const std::string& v) {
110+
if (k == "os") return c.os == v;
111+
if (k == "arch") return c.arch == v;
112+
if (k == "family") return c.family == v;
113+
if (k == "env") return c.env == v;
114+
return false;
115+
}
116+
bool expr() {
117+
std::string id = ident();
118+
if (id == "all" || id == "any") {
119+
eat('(');
120+
bool acc = (id == "all");
121+
ws();
122+
if (!(i < s.size() && s[i] == ')')) {
123+
do { bool r = expr(); acc = (id == "all") ? (acc && r) : (acc || r); }
124+
while (eat(','));
125+
}
126+
eat(')');
127+
return acc;
128+
}
129+
if (id == "not") { eat('('); bool r = expr(); eat(')'); return !r; }
130+
ws();
131+
if (i < s.size() && s[i] == '=') { ++i; return match_kv(id, str()); }
132+
return match_alias(id);
133+
}
134+
};
135+
136+
// Evaluate a `[target.<predicate>]` key. Returns the cfg() result, or — for a
137+
// non-cfg key (a bare triple) — an exact match against the resolved triple.
138+
inline bool matches(const std::string& predicate, const Ctx& c, std::string_view triple) {
139+
std::string_view k = predicate;
140+
if (k.starts_with("cfg(") && k.ends_with(")")) {
141+
Parser p{ k.substr(4, k.size() - 5), 0, c };
142+
return p.expr();
143+
}
144+
return !triple.empty() && predicate == triple; // bare-triple exact match
145+
}
146+
147+
} // namespace cfgpred
148+
46149
export std::filesystem::path target_dir(const mcpp::toolchain::Toolchain& tc,
47150
const mcpp::toolchain::Fingerprint& fp,
48151
const std::filesystem::path& root)
@@ -537,6 +640,24 @@ prepare_build(bool print_fingerprint,
537640
}
538641
if (overrides.force_static) m->buildConfig.linkage = "static";
539642

643+
// ── L1: merge platform-conditional [target.'cfg(...)'.build] flags ──────
644+
// Evaluated now (target resolved) against the resolved target — the
645+
// --target triple for a cross build, else the host. Matching predicates'
646+
// flags append to buildConfig, mirroring the [profile] merge above.
647+
if (!m->conditionalConfigs.empty()) {
648+
auto cc_ctx = cfgpred::context_for(overrides.target_triple);
649+
for (auto const& cc : m->conditionalConfigs) {
650+
if (!cfgpred::matches(cc.predicate, cc_ctx, overrides.target_triple))
651+
continue;
652+
m->buildConfig.cflags.insert(m->buildConfig.cflags.end(),
653+
cc.cflags.begin(), cc.cflags.end());
654+
m->buildConfig.cxxflags.insert(m->buildConfig.cxxflags.end(),
655+
cc.cxxflags.begin(), cc.cxxflags.end());
656+
m->buildConfig.ldflags.insert(m->buildConfig.ldflags.end(),
657+
cc.ldflags.begin(), cc.ldflags.end());
658+
}
659+
}
660+
540661
if (tcSpec.has_value() && *tcSpec != "system") {
541662
auto spec = mcpp::toolchain::parse_toolchain_spec(*tcSpec);
542663
if (!spec || spec->version.empty()) {

src/manifest.cppm

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,20 @@ struct TargetEntry {
171171
std::string linkage; // "static" | "dynamic" | "" (= auto by libc)
172172
};
173173

174+
// `[target.'cfg(...)'.build]` — platform-conditional build flags (L1). The
175+
// predicate is the raw `[target.<predicate>]` key (e.g. `cfg(windows)`,
176+
// `cfg(all(linux, not(arch="aarch64")))`, or a bare triple). It is stored
177+
// DEFERRED here because manifest parsing is target-agnostic; prepare_build
178+
// evaluates it against the RESOLVED target (host triple for a native build,
179+
// the --target triple for a cross build) and merges matching flags into
180+
// buildConfig. See .agents/docs/2026-06-29-manifest-environment-and-platform-design.md.
181+
struct ConditionalConfig {
182+
std::string predicate; // the [target.<predicate>] key
183+
std::vector<std::string> cflags;
184+
std::vector<std::string> cxxflags;
185+
std::vector<std::string> ldflags;
186+
};
187+
174188
// `[lib]` — library "root" interface convention.
175189
//
176190
// Convention-over-configuration: a library package's primary module
@@ -255,6 +269,7 @@ struct Manifest {
255269
Toolchain toolchain; // optional; empty == fallback
256270
BuildConfig buildConfig;
257271
RuntimeConfig runtimeConfig;
272+
std::vector<ConditionalConfig> conditionalConfigs; // [target.'cfg(...)'.build], deferred
258273
std::map<std::string, Profile> profiles; // [profile.<name>]
259274
// [features] — feature name → implied features ("default" = default set).
260275
std::map<std::string, std::vector<std::string>> featuresMap;
@@ -1167,6 +1182,26 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
11671182
}
11681183
}
11691184
m.targetOverrides[canon_triple(triple)] = std::move(e);
1185+
1186+
// [target.<predicate>.build] — platform-conditional flags (L1).
1187+
// `triple` here is the predicate key (cfg(...) or a bare triple);
1188+
// stored deferred, evaluated against the resolved target in
1189+
// prepare_build. Reuses the [profile] array-reading idiom.
1190+
if (auto bit = body.find("build"); bit != body.end() && bit->second.is_table()) {
1191+
auto& bt = bit->second.as_table();
1192+
ConditionalConfig cc;
1193+
cc.predicate = triple;
1194+
auto read_list = [&](const char* key, std::vector<std::string>& out) {
1195+
if (auto f = bt.find(key); f != bt.end() && f->second.is_array())
1196+
for (auto& v : f->second.as_array())
1197+
if (v.is_string()) out.push_back(v.as_string());
1198+
};
1199+
read_list("cflags", cc.cflags);
1200+
read_list("cxxflags", cc.cxxflags);
1201+
read_list("ldflags", cc.ldflags);
1202+
if (!cc.cflags.empty() || !cc.cxxflags.empty() || !cc.ldflags.empty())
1203+
m.conditionalConfigs.push_back(std::move(cc));
1204+
}
11701205
}
11711206
}
11721207

src/toolchain/fingerprint.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import mcpp.toolchain.detect;
1818

1919
export namespace mcpp::toolchain {
2020

21-
inline constexpr std::string_view MCPP_VERSION = "0.0.73";
21+
inline constexpr std::string_view MCPP_VERSION = "0.0.74";
2222

2323
struct FingerprintInputs {
2424
Toolchain toolchain;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env bash
2+
# 85_target_cfg_build_flags.sh — L1 platform-conditional config: a normal mcpp.toml
3+
# can scope [build] flags to a target predicate via `[target.'cfg(...)'.build]`.
4+
# The predicate is evaluated against the RESOLVED TARGET (here: the host build's
5+
# own triple), NOT textually — so `cfg(linux)`/`cfg(unix)` flags apply on a Linux
6+
# runner and `cfg(windows)` flags do NOT. See
7+
# .agents/docs/2026-06-29-manifest-environment-and-platform-design.md (L1).
8+
#
9+
# requires: linux
10+
set -e
11+
12+
TMP=$(mktemp -d)
13+
trap "rm -rf $TMP" EXIT
14+
cd "$TMP"
15+
16+
mkdir -p app/src
17+
cat > app/mcpp.toml <<'EOF'
18+
[package]
19+
name = "app"
20+
version = "0.1.0"
21+
22+
# Matching predicate (Linux host) → these cxxflags apply.
23+
[target.'cfg(linux)'.build]
24+
cxxflags = ["-DCOND_LINUX=1"]
25+
26+
# Also matches on Linux (unix family alias).
27+
[target.'cfg(unix)'.build]
28+
cxxflags = ["-DCOND_UNIX=1"]
29+
30+
# Non-matching predicate → must NOT apply on a Linux build.
31+
[target.'cfg(windows)'.build]
32+
cxxflags = ["-DCOND_WIN=1"]
33+
34+
# Boolean combinator: linux AND NOT aarch64 (x86_64 runner) → applies.
35+
[target.'cfg(all(linux, not(arch = "aarch64")))'.build]
36+
cxxflags = ["-DCOND_X64_LINUX=1"]
37+
EOF
38+
cat > app/src/main.cpp <<'EOF'
39+
// The conditional cxxflags must reach this TU. Missing/!expected → #error.
40+
#ifndef COND_LINUX
41+
#error "cfg(linux) cxxflag did not apply on a Linux build"
42+
#endif
43+
#ifndef COND_UNIX
44+
#error "cfg(unix) cxxflag did not apply on a Linux build"
45+
#endif
46+
#ifdef COND_WIN
47+
#error "cfg(windows) cxxflag wrongly applied on a Linux build"
48+
#endif
49+
#ifndef COND_X64_LINUX
50+
#error "cfg(all(linux, not(arch=aarch64))) cxxflag did not apply on x86_64 Linux"
51+
#endif
52+
int main() { return 0; }
53+
EOF
54+
55+
cd app
56+
"$MCPP" build > b.log 2>&1 || { cat b.log; echo "FAIL: build errored (conditional flag mis-applied?)"; exit 1; }
57+
58+
# The matching flag must be on the consumer TU; the non-matching one must not.
59+
grep -q 'COND_LINUX=1' compile_commands.json || { echo "FAIL: cfg(linux) flag absent from compile db"; exit 1; }
60+
grep -q 'COND_X64_LINUX=1' compile_commands.json || { echo "FAIL: cfg(all/not) flag absent"; exit 1; }
61+
if grep -q 'COND_WIN=1' compile_commands.json; then
62+
echo "FAIL: cfg(windows) flag leaked into a Linux build's compile db"; exit 1; fi
63+
64+
echo "OK"

0 commit comments

Comments
 (0)