diff --git a/.agents/docs/2026-06-29-manifest-environment-and-platform-design.md b/.agents/docs/2026-06-29-manifest-environment-and-platform-design.md index 9ca2ae7..52c09c8 100644 --- a/.agents/docs/2026-06-29-manifest-environment-and-platform-design.md +++ b/.agents/docs/2026-06-29-manifest-environment-and-platform-design.md @@ -1,7 +1,8 @@ # mcpp.toml: Build Environment, Platform-Conditional Config, and `build.mcpp` (Design) Date: 2026-06-29 -Status: **Design — for discussion.** Synthesizes a multi-tool survey (Cargo, Zig, +Status: **Phasing — L1 (flags) shipped in mcpp 0.0.74.** Remaining phases tracked +in §"Phasing" below. Synthesizes a multi-tool survey (Cargo, Zig, vcpkg, Bazel, xmake, Conan) and the build-systems literature (*Build Systems à la Carte*, PubGrub, hermeticity/SLSA) against mcpp's current internals. Scope: `src/manifest.cppm`, `src/config.cppm`, `src/xlings.cppm`, `src/toolchain/*`, @@ -220,17 +221,25 @@ missing declared outputs as failure. --- -## Phasing (recommended order) - -1. **L-1 environment** — highest ROI, self-contained, no upstream dep beyond mcpp; the - xlings target model already exists. Surface `[environment]` + extend the project - `.xlings.json` writer. Fold `[toolchain]` into `workspace`. Wire `[build-dependencies]`. -2. **mcpp-index workspace** (companion doc) — the first real consumer; exercises L1's - need (Windows-only `compat.openblas`). -3. **L1 conditional graph** — `[target.'cfg()']` deps+flags, target-evaluated; then - `lazy` + content-hash identity. -4. **L3 `build.mcpp`** — native build program with the two disciplines; backport the - declared-I/O contract to recipe `install()`. +## Phasing (status) + +- **✅ Phase 1 — L1 conditional build flags (mcpp 0.0.74).** `[target.'cfg(...)'.build]` + `cflags`/`cxxflags`/`ldflags`, parsed deferred into `Manifest::conditionalConfigs` + (`manifest.cppm`), evaluated against the resolved target by a recursive `cfg()` + predicate evaluator (`prepare.cppm` `cfgpred::`) and merged into `buildConfig` right + after `--target` resolution. Grammar: `all/any/not` over `os`/`arch`/`family`/`env` + + bare `windows`/`unix`/`linux`/`macos`; native build → host coords, `--target` → + target coords. Test: `tests/e2e/85_target_cfg_build_flags.sh`. +- **Phase 1b — L1 conditional dependencies + `lazy` fetch.** Same `[target.'cfg(...)']` + namespace, `.dependencies`/`.dev-dependencies`/`.build-dependencies`; merge into + `m->dependencies` in the same window (before dep resolution at `prepare.cppm:~731`). + Add `lazy = true` (fetch only when a gated path requests it) + content-hash identity. +- **Phase 2 — L-1 environment.** Surface `[environment]` → extend the project + `.xlings.json` writer (`config.cppm:699-705`) to emit `deps`/`workspace`/`envs`/`subos`; + fold `[toolchain]` into `workspace`; wire `[build-dependencies]`. +- **Phase 3 — mcpp-index workspace** (companion doc) — first real consumer of Phase 1/1b. +- **Phase 4 — L3 `build.mcpp`** — native build program (structured output + declared-I/O); + backport the declared-I/O contract to recipe `install()`. ## Appendix — cross-tool summary - **Declarative-table vs imperative-script**: TOML is static data → declarative tables diff --git a/mcpp.toml b/mcpp.toml index 135a986..6afbdcc 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.73" +version = "0.0.74" 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 cc7d1b0..83ad1de 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -43,6 +43,109 @@ import mcpp.project; namespace mcpp::build { +// ── L1 platform-conditional config: cfg() predicate evaluation ────────────── +// Context = the RESOLVED target's coordinates. A `[target.'cfg(...)'.build]` +// predicate is evaluated against this (target triple for a cross build, host +// for a native build), so conditional flags follow what the binary will run on +// — not the build host. See the manifest design doc. +namespace cfgpred { + +struct Ctx { std::string os, arch, family, env; }; + +// Derive the cfg context from the resolved --target triple, falling back to the +// host for a native build. OS/arch/env detection mirrors abi_profile's +// substring approach (toolchain/abi.cppm) so the vocabulary is consistent. +inline Ctx context_for(std::string_view targetTriple) { + Ctx c; + if (targetTriple.empty()) { + c.os = std::string(mcpp::platform::name); // host: linux/macos/windows + c.arch = std::string(mcpp::platform::host_arch); // host: x86_64/aarch64/... + } else { + auto has = [&](std::string_view n){ return targetTriple.find(n) != std::string_view::npos; }; + c.os = has("windows") || has("mingw") ? "windows" + : has("darwin") || has("apple") || has("macos") ? "macos" + : has("linux") ? "linux" : ""; + auto dash = targetTriple.find('-'); + c.arch = std::string(dash == std::string_view::npos ? targetTriple + : targetTriple.substr(0, dash)); + } + c.family = (c.os == "linux" || c.os == "macos") ? "unix" + : (c.os == "windows") ? "windows" : ""; + // env (libc/abi): musl/gnu on linux, msvc on windows; substring or host default. + if (!targetTriple.empty()) { + auto has = [&](std::string_view n){ return targetTriple.find(n) != std::string_view::npos; }; + c.env = has("musl") ? "musl" : has("msvc") ? "msvc" + : (has("gnu") || c.os == "linux") ? "gnu" : ""; + } else { + c.env = c.os == "linux" ? "gnu" : c.os == "windows" ? "msvc" : ""; + } + return c; +} + +// Recursive-descent evaluator over the inside of `cfg(...)`: +// expr := all(list) | any(list) | not(expr) | key="value" | bareword +// key ∈ {os, arch, family, env} bareword ∈ {windows, unix, linux, macos} +struct Parser { + std::string_view s; std::size_t i = 0; const Ctx& c; + void ws() { while (i < s.size() && std::isspace((unsigned char)s[i])) ++i; } + bool eat(char ch) { ws(); if (i < s.size() && s[i] == ch) { ++i; return true; } return false; } + std::string ident() { + ws(); std::size_t b = i; + while (i < s.size() && (std::isalnum((unsigned char)s[i]) || s[i] == '_')) ++i; + return std::string(s.substr(b, i - b)); + } + std::string str() { + ws(); if (i >= s.size() || s[i] != '"') return {}; + ++i; std::size_t b = i; while (i < s.size() && s[i] != '"') ++i; + auto v = std::string(s.substr(b, i - b)); if (i < s.size()) ++i; return v; + } + bool match_alias(const std::string& a) { + if (a == "windows") return c.os == "windows"; + if (a == "linux") return c.os == "linux"; + if (a == "macos") return c.os == "macos"; + if (a == "unix") return c.family == "unix"; + return false; // unknown bareword → no match + } + bool match_kv(const std::string& k, const std::string& v) { + if (k == "os") return c.os == v; + if (k == "arch") return c.arch == v; + if (k == "family") return c.family == v; + if (k == "env") return c.env == v; + return false; + } + bool expr() { + std::string id = ident(); + if (id == "all" || id == "any") { + eat('('); + bool acc = (id == "all"); + ws(); + if (!(i < s.size() && s[i] == ')')) { + do { bool r = expr(); acc = (id == "all") ? (acc && r) : (acc || r); } + while (eat(',')); + } + eat(')'); + return acc; + } + if (id == "not") { eat('('); bool r = expr(); eat(')'); return !r; } + ws(); + if (i < s.size() && s[i] == '=') { ++i; return match_kv(id, str()); } + return match_alias(id); + } +}; + +// Evaluate a `[target.]` key. Returns the cfg() result, or — for a +// non-cfg key (a bare triple) — an exact match against the resolved triple. +inline bool matches(const std::string& predicate, const Ctx& c, std::string_view triple) { + std::string_view k = predicate; + if (k.starts_with("cfg(") && k.ends_with(")")) { + Parser p{ k.substr(4, k.size() - 5), 0, c }; + return p.expr(); + } + return !triple.empty() && predicate == triple; // bare-triple exact match +} + +} // namespace cfgpred + export std::filesystem::path target_dir(const mcpp::toolchain::Toolchain& tc, const mcpp::toolchain::Fingerprint& fp, const std::filesystem::path& root) @@ -537,6 +640,24 @@ prepare_build(bool print_fingerprint, } if (overrides.force_static) m->buildConfig.linkage = "static"; + // ── L1: merge platform-conditional [target.'cfg(...)'.build] flags ────── + // Evaluated now (target resolved) against the resolved target — the + // --target triple for a cross build, else the host. Matching predicates' + // flags append to buildConfig, mirroring the [profile] merge above. + if (!m->conditionalConfigs.empty()) { + auto cc_ctx = cfgpred::context_for(overrides.target_triple); + for (auto const& cc : m->conditionalConfigs) { + if (!cfgpred::matches(cc.predicate, cc_ctx, overrides.target_triple)) + continue; + m->buildConfig.cflags.insert(m->buildConfig.cflags.end(), + cc.cflags.begin(), cc.cflags.end()); + m->buildConfig.cxxflags.insert(m->buildConfig.cxxflags.end(), + cc.cxxflags.begin(), cc.cxxflags.end()); + m->buildConfig.ldflags.insert(m->buildConfig.ldflags.end(), + cc.ldflags.begin(), cc.ldflags.end()); + } + } + if (tcSpec.has_value() && *tcSpec != "system") { auto spec = mcpp::toolchain::parse_toolchain_spec(*tcSpec); if (!spec || spec->version.empty()) { diff --git a/src/manifest.cppm b/src/manifest.cppm index 56bf340..e343ad5 100644 --- a/src/manifest.cppm +++ b/src/manifest.cppm @@ -171,6 +171,20 @@ struct TargetEntry { std::string linkage; // "static" | "dynamic" | "" (= auto by libc) }; +// `[target.'cfg(...)'.build]` — platform-conditional build flags (L1). The +// predicate is the raw `[target.]` key (e.g. `cfg(windows)`, +// `cfg(all(linux, not(arch="aarch64")))`, or a bare triple). It is stored +// DEFERRED here because manifest parsing is target-agnostic; prepare_build +// evaluates it against the RESOLVED target (host triple for a native build, +// the --target triple for a cross build) and merges matching flags into +// buildConfig. See .agents/docs/2026-06-29-manifest-environment-and-platform-design.md. +struct ConditionalConfig { + std::string predicate; // the [target.] key + std::vector cflags; + std::vector cxxflags; + std::vector ldflags; +}; + // `[lib]` — library "root" interface convention. // // Convention-over-configuration: a library package's primary module @@ -255,6 +269,7 @@ struct Manifest { Toolchain toolchain; // optional; empty == fallback BuildConfig buildConfig; RuntimeConfig runtimeConfig; + std::vector conditionalConfigs; // [target.'cfg(...)'.build], deferred std::map profiles; // [profile.] // [features] — feature name → implied features ("default" = default set). std::map> featuresMap; @@ -1167,6 +1182,26 @@ std::expected parse_string(std::string_view content, } } m.targetOverrides[canon_triple(triple)] = std::move(e); + + // [target..build] — platform-conditional flags (L1). + // `triple` here is the predicate key (cfg(...) or a bare triple); + // stored deferred, evaluated against the resolved target in + // prepare_build. Reuses the [profile] array-reading idiom. + if (auto bit = body.find("build"); bit != body.end() && bit->second.is_table()) { + auto& bt = bit->second.as_table(); + ConditionalConfig cc; + cc.predicate = triple; + auto read_list = [&](const char* key, std::vector& out) { + if (auto f = bt.find(key); f != bt.end() && f->second.is_array()) + for (auto& v : f->second.as_array()) + if (v.is_string()) out.push_back(v.as_string()); + }; + read_list("cflags", cc.cflags); + read_list("cxxflags", cc.cxxflags); + read_list("ldflags", cc.ldflags); + if (!cc.cflags.empty() || !cc.cxxflags.empty() || !cc.ldflags.empty()) + m.conditionalConfigs.push_back(std::move(cc)); + } } } diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index d38401d..2cda798 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 = "0.0.73"; +inline constexpr std::string_view MCPP_VERSION = "0.0.74"; struct FingerprintInputs { Toolchain toolchain; diff --git a/tests/e2e/85_target_cfg_build_flags.sh b/tests/e2e/85_target_cfg_build_flags.sh new file mode 100755 index 0000000..0a749f1 --- /dev/null +++ b/tests/e2e/85_target_cfg_build_flags.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# 85_target_cfg_build_flags.sh — L1 platform-conditional config: a normal mcpp.toml +# scopes [build] flags to a target predicate via `[target.'cfg(...)'.build]`. The +# predicate is evaluated against the RESOLVED target (here the host, a native +# build) — NOT textually — so exactly the host's os/arch predicates apply. This +# test is HOST-AWARE: it asserts the correct subset applies on whichever of +# linux/macos/windows + x86_64/aarch64 the runner is, so it validates the +# evaluator on all three CI platforms. See +# .agents/docs/2026-06-29-manifest-environment-and-platform-design.md (L1). +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT +cd "$TMP" + +mkdir -p app/src +cat > app/mcpp.toml <<'EOF' +[package] +name = "app" +version = "0.1.0" + +[target.'cfg(linux)'.build] +cxxflags = ["-DCOND_LINUX=1"] +[target.'cfg(macos)'.build] +cxxflags = ["-DCOND_MACOS=1"] +[target.'cfg(windows)'.build] +cxxflags = ["-DCOND_WIN=1"] +[target.'cfg(unix)'.build] +cxxflags = ["-DCOND_UNIX=1"] +[target.'cfg(arch = "x86_64")'.build] +cxxflags = ["-DCOND_X64=1"] +[target.'cfg(arch = "aarch64")'.build] +cxxflags = ["-DCOND_ARM64=1"] +# Boolean combinator: any(linux, macos) == unix-family. +[target.'cfg(any(linux, macos))'.build] +cxxflags = ["-DCOND_UNIXLIKE=1"] +EOF +cat > app/src/main.cpp <<'EOF' +// Exactly one OS predicate must apply (the host's), regardless of platform. +#if (defined(COND_LINUX) + defined(COND_MACOS) + defined(COND_WIN)) != 1 +#error "exactly one of cfg(linux)/cfg(macos)/cfg(windows) must apply on any host" +#endif +// cfg(unix) applies iff not windows. +#if defined(COND_WIN) && defined(COND_UNIX) +#error "cfg(unix) wrongly applied on a windows host" +#endif +#if !defined(COND_WIN) && !defined(COND_UNIX) +#error "cfg(unix) should apply on a non-windows host" +#endif +// any(linux, macos) must agree with unix on the CI platforms. +#if defined(COND_UNIXLIKE) != defined(COND_UNIX) +#error "cfg(any(linux,macos)) disagreed with cfg(unix)" +#endif +// Exactly one arch predicate must apply on the CI runners (x86_64 or aarch64). +#if (defined(COND_X64) + defined(COND_ARM64)) != 1 +#error "exactly one of cfg(arch=x86_64)/cfg(arch=aarch64) must apply" +#endif +// Mutual exclusion: the non-host OS predicate must NOT leak in. +int main() { return 0; } +EOF + +cd app +# The #error guards ARE the assertion: a clean build proves the right conditional +# cxxflags reached the TU and the wrong ones did not. +"$MCPP" build > b.log 2>&1 || { cat b.log; echo "FAIL: conditional cfg flags mis-applied for this host"; exit 1; } + +echo "OK"