Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions .agents/docs/2026-06-29-manifest-environment-and-platform-design.md
Original file line number Diff line number Diff line change
@@ -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/*`,
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
121 changes: 121 additions & 0 deletions src/build/prepare.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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.<predicate>]` 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)
Expand Down Expand Up @@ -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()) {
Expand Down
35 changes: 35 additions & 0 deletions src/manifest.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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.<predicate>]` 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.<predicate>] key
std::vector<std::string> cflags;
std::vector<std::string> cxxflags;
std::vector<std::string> ldflags;
};

// `[lib]` — library "root" interface convention.
//
// Convention-over-configuration: a library package's primary module
Expand Down Expand Up @@ -255,6 +269,7 @@ struct Manifest {
Toolchain toolchain; // optional; empty == fallback
BuildConfig buildConfig;
RuntimeConfig runtimeConfig;
std::vector<ConditionalConfig> conditionalConfigs; // [target.'cfg(...)'.build], deferred
std::map<std::string, Profile> profiles; // [profile.<name>]
// [features] — feature name → implied features ("default" = default set).
std::map<std::string, std::vector<std::string>> featuresMap;
Expand Down Expand Up @@ -1167,6 +1182,26 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
}
}
m.targetOverrides[canon_triple(triple)] = std::move(e);

// [target.<predicate>.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<std::string>& 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));
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/toolchain/fingerprint.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
67 changes: 67 additions & 0 deletions tests/e2e/85_target_cfg_build_flags.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading