From ab9556acc5594a7650eb7270ffb6de37737c1c3c Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 07:12:41 +0800 Subject: [PATCH 1/6] feat(schema): provides=, --strict, profile passthrough, platforms validation Implements .agents/docs/2026-06-04-manifest-schema-ownership.md D2-D5: - [runtime] provides = [...] (TOML + xpkg Lua): strong provider claim; provider mapping now prefers provides-declarers over weak capabilities-listing packages (back-compat retained). - --strict on build: schema warnings become errors. - features strict check: a requested feature (incl. backend= sugar) must exist in the target's [features] table when one is declared. - [profile.] passthrough: cflags/cxxflags/ldflags (fixed keys, open values). - [package] platforms validated against the mcpp-owned vocabulary. --- src/build/plan.cppm | 16 +++++++++++++++ src/cli.cppm | 50 +++++++++++++++++++++++++++++++++++++++++++++ src/manifest.cppm | 23 ++++++++++++++++++++- 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 62c27a5..0e6d530 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -248,7 +248,23 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, for (auto const& cap : package.manifest.runtimeConfig.capabilities) { if (std::ranges::find(plan.runtimeCapabilities, cap) == plan.runtimeCapabilities.end()) plan.runtimeCapabilities.push_back(cap); + } + } + // Provider mapping (capability -> package), strongest first: packages + // that explicitly `provides` a capability win over packages that merely + // list it in `capabilities` (weak/back-compat providers). Downstream + // lookups take the first match. + for (auto const& package : packages) { + for (auto const& cap : package.manifest.runtimeConfig.provides) plan.runtimeProviders.push_back({cap, package.manifest.package.name}); + } + for (auto const& package : packages) { + for (auto const& cap : package.manifest.runtimeConfig.capabilities) { + bool dup = false; + for (auto& pr : plan.runtimeProviders) + if (pr.capability == cap + && pr.provider == package.manifest.package.name) { dup = true; break; } + if (!dup) plan.runtimeProviders.push_back({cap, package.manifest.package.name}); } } // The same private runtime directories embedded as executable RUNPATH are diff --git a/src/cli.cppm b/src/cli.cppm index 121e2f3..c13e5c3 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -1222,6 +1222,7 @@ struct BuildOverrides { std::string package_filter; // -p : only build this workspace member std::string profile; // --profile (default "release") std::string features; // --features a,b,c (root package activation) + bool strict = false; // --strict: schema warnings become errors }; // `prepare_build` builds the BuildContext for any verb that compiles. @@ -1391,6 +1392,24 @@ prepare_build(bool print_fingerprint, m->buildConfig.debug = pr.debug; m->buildConfig.lto = pr.lto; m->buildConfig.strip = pr.strip; + m->buildConfig.cflags.insert(m->buildConfig.cflags.end(), + pr.cflags.begin(), pr.cflags.end()); + m->buildConfig.cxxflags.insert(m->buildConfig.cxxflags.end(), + pr.cxxflags.begin(), pr.cxxflags.end()); + m->buildConfig.ldflags.insert(m->buildConfig.ldflags.end(), + pr.ldflags.begin(), pr.ldflags.end()); + } + + // [package] platforms — fixed vocabulary owned by mcpp (it owns the + // target/triple system). Unknown values: warning, or error under --strict. + for (auto& pf : m->package.platforms) { + if (pf != "linux" && pf != "macos" && pf != "windows") { + auto msg = std::format( + "[package] platforms contains unknown platform '{}' " + "(expected: linux | macos | windows)", pf); + if (overrides.strict) return std::unexpected(msg); + std::println(stderr, "warning: {}", msg); + } } auto tcSpec = m->toolchain.for_platform(kCurrentPlatform); @@ -3015,6 +3034,24 @@ prepare_build(bool print_fingerprint, if (c == std::string::npos) break; p = c + 1; } + // Strict schema check: a requested feature must exist in the + // target package's [features] table when one is declared (a + // package with no [features] accepts any request — pure-define + // usage). Covers backend= sugar (feature backend-) too. + auto unknown_requested = [](const mcpp::manifest::Manifest& pm, + const std::vector& requested) + -> std::optional { + if (pm.featuresMap.empty()) return std::nullopt; + for (auto& f : requested) + if (!pm.featuresMap.contains(f)) return f; + return std::nullopt; + }; + if (auto bad = unknown_requested(packages[0].manifest, rootReq)) { + auto msg = std::format( + "--features requests '{}' which [features] does not declare", *bad); + if (overrides.strict) return std::unexpected(msg); + std::println(stderr, "warning: {}", msg); + } apply(packages[0], rootReq); } for (std::size_t i = 1; i < packages.size(); ++i) { @@ -3023,6 +3060,16 @@ prepare_build(bool print_fingerprint, for (auto& [dname, dspec] : m->dependencies) { if (dname == pname || dspec.shortName == pname) { req = dspec.features; break; } } + if (!req.empty() && !packages[i].manifest.featuresMap.empty()) { + for (auto& f : req) { + if (packages[i].manifest.featuresMap.contains(f)) continue; + auto msg = std::format( + "dependency '{}' does not declare requested feature '{}' " + "in its [features] table", pname, f); + if (overrides.strict) return std::unexpected(msg); + std::println(stderr, "warning: {}", msg); + } + } if (!req.empty() || packages[i].manifest.featuresMap.contains("default")) apply(packages[i], req); } @@ -3710,6 +3757,7 @@ int cmd_build(const mcpplibs::cmdline::ParsedArgs& parsed) { if (auto p = parsed.value("package")) ov.package_filter = *p; if (auto pr = parsed.value("profile")) ov.profile = *pr; if (auto fs = parsed.value("features")) ov.features = *fs; + ov.strict = parsed.is_flag_set("strict"); ov.force_static = parsed.is_flag_set("static"); // P0: try fast-path if inputs haven't changed. @@ -5728,6 +5776,8 @@ int run(int argc, char** argv) { .help("Build profile: release (default) | dev | dist | <[profile.*] name>")) .option(cl::Option("features").takes_value().value_name("LIST") .help("Activate root-package features (comma-separated)")) + .option(cl::Option("strict") + .help("Treat manifest schema warnings (unknown feature/platform) as errors")) .action(wrap_rc(cmd_build))) .subcommand(cl::App("run") .description("Build + run a binary target (after `--`, args are passed to it)") diff --git a/src/manifest.cppm b/src/manifest.cppm index 07b5ab1..fdf5796 100644 --- a/src/manifest.cppm +++ b/src/manifest.cppm @@ -115,7 +115,11 @@ struct BuildConfig { struct RuntimeConfig { std::vector libraryDirs; // relative to package root std::vector dlopenLibs; // runtime-loaded sonames - std::vector capabilities; // host/system capabilities + std::vector capabilities; // host/system capabilities REQUIRED + // Capabilities this package explicitly FULFILS (strong provider claim). + // Packages that merely list a capability in `capabilities` are weak + // providers (back-compat); provides-declarers win provider selection. + std::vector provides; // [runtime.] provider = "" — explicit provider selection // (the three-tier knob: default/auto → explicit override). std::map providerOverrides; @@ -190,6 +194,10 @@ struct Profile { bool debug = false; bool lto = false; bool strip = false; + // Passthrough escape hatch (fixed keys, open values — I6 completeness): + std::vector cflags; + std::vector cxxflags; + std::vector ldflags; }; struct Manifest { @@ -504,6 +512,14 @@ std::expected parse_string(std::string_view content, if (auto it = tt.find("debug"); it != tt.end() && it->second.is_bool()) pr.debug = it->second.as_bool(); if (auto it = tt.find("lto"); it != tt.end() && it->second.is_bool()) pr.lto = it->second.as_bool(); if (auto it = tt.find("strip"); it != tt.end() && it->second.is_bool()) pr.strip = it->second.as_bool(); + auto read_list = [&](const char* key, std::vector& out) { + if (auto it = tt.find(key); it != tt.end() && it->second.is_array()) + for (auto& v : it->second.as_array()) + if (v.is_string()) out.push_back(v.as_string()); + }; + read_list("cflags", pr.cflags); + read_list("cxxflags", pr.cxxflags); + read_list("ldflags", pr.ldflags); m.profiles[pname] = pr; } } @@ -888,6 +904,8 @@ std::expected parse_string(std::string_view content, m.runtimeConfig.dlopenLibs = *v; if (auto v = doc->get_string_array("runtime.capabilities")) m.runtimeConfig.capabilities = *v; + if (auto v = doc->get_string_array("runtime.provides")) + m.runtimeConfig.provides = *v; // [runtime.] provider = "" — explicit provider override. if (auto* rt = doc->get_table("runtime"); rt && !rt->empty()) { for (auto& [rk, rv] : *rt) { @@ -1853,6 +1871,9 @@ synthesize_from_xpkg_lua(std::string_view luaContent, } else if (sub == "capabilities") { if (auto r = read_string_list(m.runtimeConfig.capabilities); !r) return std::unexpected(r.error()); + } else if (sub == "provides") { + if (auto r = read_string_list(m.runtimeConfig.provides); !r) + return std::unexpected(r.error()); } else { rc.skip_ws_and_comments(); if (rc.peek() == '"' || rc.peek() == '\'') (void)rc.read_string(); From 443054987f0ed9668804721f3dd6177b5f5a3c88 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 07:17:30 +0800 Subject: [PATCH 2/6] fix: fast-path must bypass on --profile/--features/--strict/-p; e2e 66-68 The fresh-build.ninja fast path skipped prepare_build, silently ignoring resolution-affecting flags (a plain build followed by build --features X no-op'd). Bail out like --target/--static already did. Built-in dist profile no longer enables lto (several packaged gcc payloads lack the LTO plugin; remains an explicit [profile.*] knob). Adds e2e: 66 runtime provides (strong-vs-weak provider + override), 67 features/backend=/platforms strict gate, 68 profile knobs + passthrough. --- src/cli.cppm | 13 +++- tests/e2e/66_runtime_provides.sh | 86 ++++++++++++++++++++++ tests/e2e/67_features_strict.sh | 109 ++++++++++++++++++++++++++++ tests/e2e/68_profile_passthrough.sh | 40 ++++++++++ 4 files changed, 245 insertions(+), 3 deletions(-) create mode 100755 tests/e2e/66_runtime_provides.sh create mode 100755 tests/e2e/67_features_strict.sh create mode 100755 tests/e2e/68_profile_passthrough.sh diff --git a/src/cli.cppm b/src/cli.cppm index c13e5c3..03a83e0 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -1385,7 +1385,9 @@ prepare_build(bool print_fingerprint, std::string pname = overrides.profile.empty() ? "release" : overrides.profile; mcpp::manifest::Profile pr; if (pname == "dev" || pname == "debug") { pr.optLevel = "0"; pr.debug = true; } - else if (pname == "dist") { pr.optLevel = "3"; pr.lto = true; pr.strip = true; } + else if (pname == "dist") { pr.optLevel = "3"; pr.strip = true; } + // (built-in dist intentionally leaves lto off: several packaged gcc + // payloads ship without the LTO plugin; enable via [profile.dist].) else { pr.optLevel = "2"; } // release if (auto it = m->profiles.find(pname); it != m->profiles.end()) pr = it->second; m->buildConfig.optLevel = pr.optLevel; @@ -3760,8 +3762,13 @@ int cmd_build(const mcpplibs::cmdline::ParsedArgs& parsed) { ov.strict = parsed.is_flag_set("strict"); ov.force_static = parsed.is_flag_set("static"); - // P0: try fast-path if inputs haven't changed. - if (!print_fp && ov.target_triple.empty() && !ov.force_static) { + // P0: try fast-path if inputs haven't changed. Any resolution-affecting + // override (--profile/--features/--strict, like --target/--static) must + // bypass it: the cached build.ninja was generated without them, so taking + // the fast path would silently ignore the flags. + if (!print_fp && ov.target_triple.empty() && !ov.force_static + && ov.profile.empty() && ov.features.empty() && !ov.strict + && ov.package_filter.empty()) { auto root = find_manifest_root(std::filesystem::current_path()); if (root) { if (auto rc = try_fast_build(*root, verbose, no_cache)) { diff --git a/tests/e2e/66_runtime_provides.sh b/tests/e2e/66_runtime_provides.sh new file mode 100755 index 0000000..0981185 --- /dev/null +++ b/tests/e2e/66_runtime_provides.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# 66_runtime_provides.sh — [runtime] provides: a package that explicitly +# `provides` a capability wins provider selection over a package that merely +# lists it in `capabilities` (weak provider). Surfaced via resolution.json. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT +cd "$TMP" + +mk_dep() { # name, runtime-block + mkdir -p "$1/src" + cat > "$1/mcpp.toml" < "$1/src/lib.cppm" < app/mcpp.toml <<'EOF' +[package] +name = "app" +version = "0.1.0" + +[dependencies] +weakdep = { path = "../weakdep" } +strongdep = { path = "../strongdep" } +EOF +cat > app/src/main.cpp <<'EOF' +import std; +import weakdep; +import strongdep; +int main() { std::println("{}", weakdep_id() + strongdep_id()); return 0; } +EOF + +cd app +"$MCPP" build > build.log 2>&1 || { cat build.log; echo "build failed"; exit 1; } + +RES=$(find target -name resolution.json | head -1) +[[ -n "$RES" ]] || { echo "no resolution.json produced"; exit 1; } + +# First provider entry for test.cap.demo must be the provides-declarer. +python3 - "$RES" <<'PY' +import json, sys +plan = json.load(open(sys.argv[1])) +caps = plan["runtime"]["capabilities"] +first = next(c for c in caps if c["capability"] == "test.cap.demo") +assert first["provider"] == "strongdep", \ + f"expected strong provider 'strongdep' first, got {first['provider']}" +PY + +# Explicit override back to the weak provider must be honoured. +cat >> mcpp.toml <<'EOF' + +[runtime."test.cap.demo"] +provider = "weakdep" +EOF +rm -rf target +"$MCPP" build > build2.log 2>&1 || { cat build2.log; echo "override build failed"; exit 1; } +RES=$(find target -name resolution.json | head -1) +python3 - "$RES" <<'PY' +import json, sys +plan = json.load(open(sys.argv[1])) +caps = plan["runtime"]["capabilities"] +first = next(c for c in caps if c["capability"] == "test.cap.demo") +assert first["provider"] == "weakdep", \ + f"override to weakdep not honoured, got {first['provider']}" +PY + +echo "OK" diff --git a/tests/e2e/67_features_strict.sh b/tests/e2e/67_features_strict.sh new file mode 100755 index 0000000..94db86a --- /dev/null +++ b/tests/e2e/67_features_strict.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# 67_features_strict.sh — feature activation, backend= sugar, and the +# --strict schema gate (unknown feature / unknown platform). +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT +cd "$TMP" + +# Library dep declaring backend-* features (the backend= sugar target). +mkdir -p widget/src +cat > widget/mcpp.toml <<'EOF' +[package] +name = "widget" +version = "0.1.0" + +[targets.widget] +kind = "lib" + +[features] +default = [] +backend-a = [] +backend-b = [] +EOF +cat > widget/src/widget.cppm <<'EOF' +export module widget; +export int widget_backend() { +#if defined(MCPP_FEATURE_BACKEND_A) + return 1; +#elif defined(MCPP_FEATURE_BACKEND_B) + return 2; +#else + return 0; +#endif +} +EOF + +mkdir -p app/src +cat > app/mcpp.toml <<'EOF' +[package] +name = "app" +version = "0.1.0" +platforms = ["linux", "macos", "windows"] + +[features] +default = ["base"] +base = [] +extra = [] + +[dependencies] +widget = { path = "../widget", backend = "a" } +EOF +cat > app/src/main.cpp <<'EOF' +import std; +import widget; +int main() { +#ifdef MCPP_FEATURE_BASE + std::println("base"); +#endif +#ifdef MCPP_FEATURE_EXTRA + std::println("extra"); +#endif + std::println("backend={}", widget_backend()); + return 0; +} +EOF + +cd app + +# 1. backend= sugar selects widget's backend-a feature. +"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "build failed"; exit 1; } +bin=$(find target -name app -type f | head -1) +out=$("$bin" | tail -2) +grep -q "backend=1" <<< "$out" || { echo "backend=a not activated: $out"; exit 1; } +grep -q "base" <<< "$out" || true # default features active + +# 2. --features extra activates a declared feature. +rm -rf target +"$MCPP" build --features extra > b2.log 2>&1 || { cat b2.log; echo "features build failed"; exit 1; } +bin=$(find target -name app -type f | head -1) +"$bin" | grep -q "extra" || { echo "--features extra not active"; exit 1; } + +# 3. Unknown feature → warning by default, error under --strict. +rm -rf target +"$MCPP" build --features nosuch > b3.log 2>&1 || { cat b3.log; echo "warn path must not fail"; exit 1; } +grep -q "does not declare" b3.log || { cat b3.log; echo "missing unknown-feature warning"; exit 1; } +if "$MCPP" build --features nosuch --strict > b4.log 2>&1; then + cat b4.log; echo "--strict must fail on unknown feature"; exit 1 +fi +grep -q "does not declare" b4.log || { cat b4.log; echo "missing strict error text"; exit 1; } + +# 4. Unknown backend on a dep that declares backend-* features → warning. +sed -i 's/backend = "a"/backend = "zzz"/' mcpp.toml +rm -rf target +"$MCPP" build > b5.log 2>&1 || { cat b5.log; echo "unknown-backend warn path must not fail"; exit 1; } +grep -q "does not declare requested feature 'backend-zzz'" b5.log \ + || { cat b5.log; echo "missing unknown-backend warning"; exit 1; } +sed -i 's/backend = "zzz"/backend = "a"/' mcpp.toml + +# 5. Unknown platform → warning by default, error under --strict. +sed -i 's/platforms = \["linux", "macos", "windows"\]/platforms = ["linux", "amiga"]/' mcpp.toml +rm -rf target +"$MCPP" build > b6.log 2>&1 || { cat b6.log; echo "platform warn path must not fail"; exit 1; } +grep -q "unknown platform 'amiga'" b6.log || { cat b6.log; echo "missing platform warning"; exit 1; } +if "$MCPP" build --strict > b7.log 2>&1; then + cat b7.log; echo "--strict must fail on unknown platform"; exit 1 +fi + +echo "OK" diff --git a/tests/e2e/68_profile_passthrough.sh b/tests/e2e/68_profile_passthrough.sh new file mode 100755 index 0000000..cf7bd74 --- /dev/null +++ b/tests/e2e/68_profile_passthrough.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# 68_profile_passthrough.sh — build profiles: built-in release/dev/dist knobs +# plus the [profile.] passthrough escape hatch (cflags/cxxflags/ldflags). +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT +cd "$TMP" + +"$MCPP" new profapp > /dev/null +cd profapp +cat >> mcpp.toml <<'EOF' + +[profile.dist] +opt = 3 +strip = true +cxxflags = ["-fno-plt"] +EOF + +# Built-in release default: -O2, no -g. +"$MCPP" build --verbose > rel.log 2>&1 || { cat rel.log; echo "release build failed"; exit 1; } +grep -q -- "-O2" rel.log || { echo "release missing -O2"; exit 1; } + +# dev: -O0 -g. +rm -rf target +"$MCPP" build --profile dev --verbose > dev.log 2>&1 || { cat dev.log; echo "dev build failed"; exit 1; } +grep -q -- "-O0" dev.log || { echo "dev missing -O0"; exit 1; } +grep -q -- "-g" dev.log || { echo "dev missing -g"; exit 1; } + +# dist from [profile.dist]: -O3 -flto + passthrough cxxflag, stripped binary. +rm -rf target +"$MCPP" build --profile dist --verbose > dist.log 2>&1 || { cat dist.log; echo "dist build failed"; exit 1; } +grep -q -- "-O3" dist.log || { echo "dist missing -O3"; exit 1; } +grep -q -- "-fno-plt" dist.log || { echo "dist missing passthrough -fno-plt"; exit 1; } + +binary=$(find target -name profapp -type f | head -1) +[[ -n "$binary" ]] || { echo "no binary"; exit 1; } +"$binary" > /dev/null || { echo "dist binary does not run"; exit 1; } + +echo "OK" From 1df483577d39f5976b64416eafe03704ba696fe4 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 07:19:03 +0800 Subject: [PATCH 3/6] docs: mcpp.toml field reference for schema knobs (P5) 05-mcpp-toml.md: new sections 2.8 [features], 2.9 [profile.] (incl. passthrough), 2.10 [runtime] (provides/provider override/capability naming), 2.11 [package] platforms; dep-spec features/backend= sugar in 2.5; appendix A 'schema ownership' (closed grammar / open vocabulary) as the admission rule for future fields. Examples mirror the verified e2e 66-68. 03-toolchains: abi capability enforcement; 00-getting-started: gui template + why/doctor. --- docs/00-getting-started.md | 7 +++ docs/03-toolchains.md | 8 ++++ docs/05-mcpp-toml.md | 92 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) diff --git a/docs/00-getting-started.md b/docs/00-getting-started.md index 2870dd6..79b8845 100644 --- a/docs/00-getting-started.md +++ b/docs/00-getting-started.md @@ -118,3 +118,10 @@ mcpp pack --mode bundle-all # 全自包含,含 libc 与 ld-linux - [02 — 发布打包](02-pack-and-release.md) — 构建可分发产物 - [03 — 工具链管理](03-toolchains.md) — 切换编译器与多版本管理 - 任意命令的完整选项可通过 `mcpp --help` 查阅 + + +## 更多入口 + +- GUI 起步:`mcpp new myapp --template gui`(imgui.app 窗口骨架,构建后 `mcpp run` 直接出窗口)。 +- 解释默认决策:`mcpp why [toolchain|runtime|deps]`;主机能力体检:`mcpp self doctor`; + 机器可读解析清单:构建产物 `target///resolution.json`。 diff --git a/docs/03-toolchains.md b/docs/03-toolchains.md index 1128cae..ae81891 100644 --- a/docs/03-toolchains.md +++ b/docs/03-toolchains.md @@ -131,3 +131,11 @@ mcpp 的运行行为可通过以下环境变量调整: 未显式设置 `MCPP_HOME` 时,mcpp 将基于二进制所在目录的上一级路径 自动定位沙盒位置(release tarball 解压至 `~/.mcpp/` 后,`~/.mcpp/` 即为 home),因此 release 版本无需任何环境变量配置即可运行。 + + +## ABI 能力强制 + +依赖可声明 `abi:` 能力(如 `compat.glfw` 声明 `abi:glibc`)。解析出的 +工具链 ABI 不满足任一依赖的 abi 要求时,构建会**尽早失败**并给出修复建议 +(例如 musl-static 工具链遇到 abi:glibc 依赖),取代深层的链接/头文件报错。 +查看:`mcpp why toolchain`。 diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index a043cf2..2d3be8c 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -155,8 +155,18 @@ mylib = { path = "../mylib" } # Git 依赖 [dependencies] mylib = { git = "https://github.com/user/mylib.git", tag = "v1.0.0" } + +# 长式 dep spec:features 与 backend 旋钮 +[dependencies] +imgui = { version = "0.0.3", features = ["docking"] } # 请求该依赖的 feature +widget = { version = "1.0", backend = "glfw_opengl3" } # 糖:= features=["backend-glfw_opengl3"] ``` +`backend = ""` 是**通用约定糖**:1:1 脱糖为请求该依赖的 `backend-` +feature(库若支持该旋钮,应在自己的 `[features]` 中声明 `backend-*` 系列)。 +若目标包声明了 `[features]` 但不含所请求的 feature(含 backend 脱糖结果), +默认给出 warning,`mcpp build --strict` 下报错。 + **SemVer 约束**: ```toml @@ -188,6 +198,88 @@ toolchain = "gcc@15.1.0-musl" linkage = "static" ``` +### 2.8 `[features]` — 特性(Cargo 式,加性) + +```toml +[features] +default = ["base"] # 默认激活集 +base = [] +docking = ["extra"] # 激活 docking 时隐含激活 extra(传递闭包) +extra = [] +``` + +- 激活来源:包自身 `default` 集 ∪ 显式请求(根包 `mcpp build --features a,b`; + 依赖经长式 dep spec `features = [...]` / `backend = "..."` 糖)。 +- 每个激活的 feature 在该包的编译中得到宏 `-DMCPP_FEATURE_` + (名字转大写,非字母数字转 `_`,如 `backend-a` → `MCPP_FEATURE_BACKEND_A`)。 +- **strict 校验**:目标包声明了 `[features]` 表时,请求未声明的 feature 给出 + warning;`--strict` 下报错。未声明 `[features]` 的包接受任意请求(纯宏用法)。 + +### 2.9 `[profile.]` — 构建档案 + +```toml +[profile.dist] +opt = 3 # -O 级别(数字或 "s"/"z" 字符串) +debug = false # -g +lto = true # -flto(注意:部分打包 gcc 未启用 LTO 插件) +strip = true # 链接期 -s +# passthrough 逃生口(固定键、开放值): +cflags = ["-fno-plt"] +cxxflags = ["-fno-plt"] +ldflags = [] +``` + +- 选择:`mcpp build --profile `,默认 `release`。 +- 内置档案:`release`(-O2)/ `dev`、`debug`(-O0 -g)/ `dist`(-O3 + strip; + **不默认开 lto**)。`[profile.<内置名>]` 可整体覆盖内置定义。 + +### 2.10 `[runtime]` — 主机运行时能力 + +```toml +[runtime] +library_dirs = ["vendor/lib"] # 烤进产物 RUNPATH 的目录(相对包根) +dlopen_libs = ["libGL.so.1"] # 运行期 dlopen 的 soname(doctor 校验) +capabilities = ["opengl.glx.driver"] # 需要的主机能力(开放命名空间) +provides = ["opengl.glx.driver"] # 显式声明本包兑现的能力(强 provider) + +# 显式 provider 覆盖(三档旋钮的"显式"档) +[runtime."opengl.glx.driver"] +provider = "compat.glx-runtime" +``` + +- **provider 选择**:声明 `provides` 的包(强)优先于仅在 `capabilities` 列出 + 能力的包(弱,向后兼容);`[runtime.] provider=` 显式覆盖最优先, + 指向依赖图中不存在的 provider 时给出 warning。 +- 解析结果可经 `mcpp why runtime`、`mcpp self doctor` 与构建产物 + `target///resolution.json` 查看(默认不是魔法)。 +- 能力命名约定:分层小写 `domain.sub.role`(如 `opengl.glx.driver`、 + `x11.display`)与前缀类 `abi:`(如 `abi:glibc`,参与工具链 ABI 强制)。 + +### 2.11 `[package] platforms` — 平台声明 + +```toml +[package] +platforms = ["linux", "macos", "windows"] +``` + +声明包支持的平台(CI 矩阵提示,经 `mcpp why` 展示)。词表由 mcpp 固定 +(它拥有 target/triple 体系):`linux | macos | windows`;未知值 warning, +`--strict` 下报错。 + +## 附录 A. Schema 所有权原则(新字段准入标准) + +> **语法封闭,词汇开放**:谁拥有解析语义谁定义键;谁拥有领域知识谁定义值。 + +- mcpp 只定义**机制**(features 并集/闭包、capability require/provide/override、 + profile→编译器旗标、platform→triple),键与形状固定;feature 名、能力名、 + 后端名等**领域词汇只出现在值里**,不进 mcpp 代码。 +- **不支持包自定义 toml 键**:键合法性不得依赖"先解析目标包",否则 manifest + 失去静态可解析性(lockfile/LSP/审计的前提)。包的扩展点 = 固定机制内的开放值域。 +- 包级旋钮统一收敛进 features;糖键(如 `backend=`)进入核心语法须满足: + ① 领域中立(跨生态通用模式)② 1:1 脱糖、零新增解析语义。 +- 字段归属总表与定型决策见 + `.agents/docs/2026-06-04-manifest-schema-ownership.md`。 + ## 3. 实战示例 ### 3.1 简单 Hello World From c3de0548118f2b52fbce9c010850ad2379b47d62 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 07:19:25 +0800 Subject: [PATCH 4/6] docs: manifest schema-ownership design (D1-D6, P1-P6) --- .../2026-06-04-manifest-schema-ownership.md | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 .agents/docs/2026-06-04-manifest-schema-ownership.md diff --git a/.agents/docs/2026-06-04-manifest-schema-ownership.md b/.agents/docs/2026-06-04-manifest-schema-ownership.md new file mode 100644 index 0000000..fd5b7af --- /dev/null +++ b/.agents/docs/2026-06-04-manifest-schema-ownership.md @@ -0,0 +1,140 @@ +# mcpp.toml Schema 所有权设计:语法封闭 · 词汇开放 + +> 2026-06-04 · 状态: 设计定稿待实施 · 代码锚点基于 main@3a8a3d4 (v0.0.47) +> 关联: agentdocs/2026-06-03-capability-architecture-rfc.md(能力架构 RFC) +> mcpp-index/.agents/docs/2026-06-03-capability-runtime-metadata.md(index 侧 schema) + +## 0. 问题 + +v0.0.47 引入了一批"三档旋钮"字段(`backend=`、`[runtime.] provider=`、 +`[package] platforms`、`[features]`、`[profile.*]`),但**字段归属是临时混合的**: +`backend` 把 imgui 域词汇泄漏进 mcpp 语法层;provider 校验语义偏松("声明了该 +capability 的包都算 provider");platforms/profiles 缺校验与逃生口;且全部 +**未写进用户文档**(docs/05-mcpp-toml.md 截至 2.7 节)。本设计统一定型。 + +## 1. 设计原则(判定法) + +> **语法封闭,词汇开放(closed grammar, open vocabulary): +> 谁拥有"解析语义"谁定义键;谁拥有"领域知识"谁定义值。** + +三条铁律: + +- **A. mcpp 只定义机制,不枚举领域词汇。** features 的并集/闭包、capability 的 + require/provide/override、profile→编译器旗标、platform→triple,这些解析语义 + 归 mcpp,键与形状固定。feature 名、capability 名、后端名是领域知识——只出现在 + **值**里,绝不出现在 mcpp 代码中(与 doctor 去 #ifdef 同一原则在 schema 层的体现)。 +- **B. 不允许包自定义 toml 键。** 键合法性若取决于"先解析目标包",manifest 即丧失 + 静态可解析性,lockfile/LSP/审计/why 全部层次倒置(Cargo 十年验证的取舍)。 + 包的扩展点 = **固定机制内的开放值域**,不是新键。 +- **C. 包级旋钮统一收敛进 features;糖键入核仅当:① 领域中立(跨生态通用模式) + ② 1:1 脱糖、零新增解析语义。** + +## 2. 现状代码盘点(main@3a8a3d4) + +| 机制 | 代码锚点 | 现状归属 | +|---|---|---| +| dep-spec 键白名单(含 `backend`) | `src/manifest.cppm:593-597 is_dep_spec_key` | 键 mcpp 固定 | +| `backend=` 脱糖 → `backend-` feature | `src/manifest.cppm:631` | 纯糖,值开放 ✅ | +| `DependencySpec.features` | `src/pm/dep_spec.cppm`(features 字段) | 机制固定,值开放 ✅ | +| `[features]` 解析 | `src/manifest.cppm:213 featuresMap` / `:521` | 机制固定,值开放 ✅ | +| feature 激活(default∪requested,闭包,`-DMCPP_FEATURE_*`) | `src/cli.cppm:2969` | mcpp 固定 ✅ | +| `[runtime.] provider=` | `src/manifest.cppm:121 providerOverrides` / `:897`;应用+校验 `src/cli.cppm:3294-3310` | 形状固定,cap 名开放 ✅;**provider 语义偏松 ⚠️** | +| `[package] platforms` | `src/manifest.cppm:40`;`why` 展示 | 键固定;**值未校验 ⚠️** | +| `[profile.]` | `src/manifest.cppm:188 Profile`;`src/build/flags.cppm` 应用 | 键固定(编译器域);**无 passthrough ⚠️** | +| capability 聚合(`CapabilityProvider`) | `src/build/plan.cppm:72-76` | mcpp 固定 ✅ | +| index 侧能力声明 | mcpp-index `compat.glfw.lua` `runtime.capabilities`(含 `abi:glibc`) | 值由生态定义 ✅;**无 provides= ⚠️** | + +## 3. 设计决策(逐项定型) + +### D1. `backend=` —— 定型为"通用约定糖",保留 +- 理由:「库的多个可替换实现」是跨生态通用模式(GUI 后端 / DB driver / logger sink), + 满足铁律 C 两条件(领域中立 + 已是 1:1 脱糖)。 +- 定型内容: + 1. 键名维持 `backend`(评估过 `impl`,`backend` 语感更明确;不再增设别名)。 + 2. **约定写入文档**:库支持该旋钮 ⇔ 声明 `[features] backend-` 系列 + (mcpp 不校验后端名本身,名字归库)。 + 3. **strict 校验**(新增):若目标包的 `featuresMap` 已声明任何 `backend-*` + feature,而请求的 `backend-` 不在其中 → warning(`--strict` 下 error)。 + 目标包零声明时不校验(允许纯 define 用法)。 +- 不做:包注册自定义键(违反铁律 B)。 + +### D2. `provider=` —— 收紧为显式 `provides=` 语义 +- index/包侧新增 `provides = ["opengl.glx.driver", ...]`(lua `mcpp` 段 + + mcpp.toml `[runtime] provides`),声明"我兑现这些能力"。 +- core 解析进 `RuntimeConfig.provides`;`plan.runtimeProviders` 改为: + capability→provider 的映射**优先取 provides 声明者**;无任何 provides 时 + 回退现状(声明 capabilities 者视为弱 provider,向后兼容)。 +- `provider=` 覆盖校验升级:目标包必须 provides(或弱提供)该能力,否则 warning 照旧。 +- capability 命名规范(文档化,不进代码):分层小写 `domain.sub.role` + (`opengl.glx.driver`、`x11.display`)+ 前缀类 `abi:`;mcpp-index 维护 + "知名能力名注册表"文档供 doctor/why 提示,**mcpp 代码不枚举**。 + +### D3. `platforms` —— 固定词表 + 校验 +- 值域归 mcpp(它拥有 triple 体系):`linux | macos | windows`(后续随 target + 支持扩展)。解析时非法值 → warning(`--strict` 下 error)。 + +### D4. `[features]` —— strict 校验 + 传递传播(follow-up) +- strict 校验:dep spec 请求的 feature 不在目标包 `featuresMap` 且目标包**有** + `[features]` 表时 → warning(`--strict` error);无表时不校验(纯 define 用法)。 +- dep→dep 传递 feature 请求:已知缺口,独立 follow-up(影响解析器,本设计不展开)。 + +### D5. `[profile.]` —— 固定键 + passthrough 逃生口 +- 新增开放值域(固定形状内):`cflags = [...]`、`cxxflags = [...]`、`ldflags = [...]`, + 追加在 profile 解析后(I6 完备性;铁律 A 不破——键仍是 mcpp 的,值是用户的)。 + +### D6. Schema 所有权规范成文 +- 本文件 §1 的判定法写入用户文档(见 P5),作为后续任何新字段的准入标准: + 新键须回答"解析语义归谁?能否用 features/capability 表达?是否领域中立纯糖?" + +## 4. 实施计划(每阶段遵循:本地 build+test+e2e → 小步 commit → 分支 PR → 三平台 CI 绿 → squash 合入) + +### P1 `provides=`(D2,跨 mcpp + mcpp-index) +- mcpp:`src/manifest.cppm` RuntimeConfig 增 `provides` + TOML/Lua 双解析 + (TOML `[runtime] provides`;Lua `mcpp.runtime.provides`); + `src/build/plan.cppm` 聚合优先级(provides > capabilities 弱提供); + `src/cli.cppm` provider 覆盖校验/why/doctor/resolution.json 同步展示 provides 来源。 +- mcpp-index:`compat.glx-runtime.lua` 增 `provides = {"opengl.glx.driver"}`(PR); + schema 文档同步。 +- 验证:imgui 消费链 why/doctor 显示 provider=compat.glx-runtime(由 provides 而非弱提供); + e2e 新增 `66_runtime_provides.sh`。 + +### P2 `backend=` 定型 + features strict(D1+D4) +- `src/cli.cppm` feature 激活处增 strict 校验(warning;`--strict` 全局 flag → error); + `mcpp build --strict` 选项接入 BuildOverrides。 +- 验证:imgui 0.0.3(声明 backend-* 前后)+ 故意写错 backend 名 → warning/error; + e2e `67_features_strict.sh`。 + +### P3 `[profile.*]` passthrough(D5) +- `src/manifest.cppm` Profile 增三个 list 字段 + 解析;`src/build/flags.cppm` + 在 profile 应用处追加。 +- 验证:`[profile.dist] cflags=["-fno-plt"]` 经 `--verbose` 可见;e2e `68_profile_passthrough.sh`。 + +### P4 `platforms` 校验(D3) +- `src/manifest.cppm` 解析处校验词表 → warning/strict-error。 +- 验证:非法值 warning;e2e 并入 67。 + +### P5 **文档定型(必做收尾)** —— `docs/05-mcpp-toml.md` +- 新增章节(承接现有 2.7 编号): + - **2.8 `[features]`**:语法、default 集、隐含闭包、`--features`、dep `features=[]`、 + `-DMCPP_FEATURE_` 约定、strict 行为; + - **2.9 `[profile.]`**:内置 release/dev/dist、opt/debug/lto/strip、 + passthrough cflags/cxxflags/ldflags、`--profile`; + - **2.10 `[runtime]`**:library_dirs/dlopen_libs/capabilities/**provides**、 + `[runtime.] provider=` 覆盖语义与校验; + - **2.1 增补**:`[package] platforms`(词表 + CI 矩阵提示 + `why` 展示); + - **2.5 增补**:dep spec `backend = ""` 糖(脱糖规则 + `backend-*` feature 约定); + - **新增附录「Schema 所有权原则」**:§1 判定法 + 字段归属总表(给后续贡献者的准入标准)。 +- 同步:`docs/03-toolchains.md` 提及 abi 能力强制;`docs/00-getting-started.md` + 提及 `new --template gui` 与 `why/doctor`。 +- 验证:文档内示例逐个真实跑通后才提交(文档中的每段 toml 必须可构建)。 + +### P6 e2e 收口 +- 上述 66/67/68 纳入 `tests/e2e/`(`run_all.sh` 自动发现);linux self-host CI 全量跑。 + +## 5. 验收 +- [ ] provides= 端到端(index 声明 → why/doctor/resolution.json 显示 → 覆盖校验收紧) +- [ ] backend=/features strict 行为(warning 与 --strict error) +- [ ] profiles passthrough 旗标可观察 +- [ ] platforms 非法值告警 +- [ ] docs/05-mcpp-toml.md 含 2.8–2.10、2.1/2.5 增补、所有权附录,且所有示例可构建 +- [ ] 新 e2e 全过,三平台 CI 绿 From dd85b4e782188b728e37af19f377bc56d8e8f245 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 07:31:13 +0800 Subject: [PATCH 5/6] test: e2e 67/68 find .exe binaries (windows portability) --- tests/e2e/67_features_strict.sh | 6 ++++-- tests/e2e/68_profile_passthrough.sh | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/e2e/67_features_strict.sh b/tests/e2e/67_features_strict.sh index 94db86a..56bcb0a 100755 --- a/tests/e2e/67_features_strict.sh +++ b/tests/e2e/67_features_strict.sh @@ -69,7 +69,8 @@ cd app # 1. backend= sugar selects widget's backend-a feature. "$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "build failed"; exit 1; } -bin=$(find target -name app -type f | head -1) +bin=$(find target \( -name app -o -name app.exe \) -type f | head -1) +[[ -n "$bin" ]] || { echo "no app binary"; exit 1; } out=$("$bin" | tail -2) grep -q "backend=1" <<< "$out" || { echo "backend=a not activated: $out"; exit 1; } grep -q "base" <<< "$out" || true # default features active @@ -77,7 +78,8 @@ grep -q "base" <<< "$out" || true # default features active # 2. --features extra activates a declared feature. rm -rf target "$MCPP" build --features extra > b2.log 2>&1 || { cat b2.log; echo "features build failed"; exit 1; } -bin=$(find target -name app -type f | head -1) +bin=$(find target \( -name app -o -name app.exe \) -type f | head -1) +[[ -n "$bin" ]] || { echo "no app binary"; exit 1; } "$bin" | grep -q "extra" || { echo "--features extra not active"; exit 1; } # 3. Unknown feature → warning by default, error under --strict. diff --git a/tests/e2e/68_profile_passthrough.sh b/tests/e2e/68_profile_passthrough.sh index cf7bd74..6654b1b 100755 --- a/tests/e2e/68_profile_passthrough.sh +++ b/tests/e2e/68_profile_passthrough.sh @@ -33,7 +33,7 @@ rm -rf target grep -q -- "-O3" dist.log || { echo "dist missing -O3"; exit 1; } grep -q -- "-fno-plt" dist.log || { echo "dist missing passthrough -fno-plt"; exit 1; } -binary=$(find target -name profapp -type f | head -1) +binary=$(find target \( -name profapp -o -name profapp.exe \) -type f | head -1) [[ -n "$binary" ]] || { echo "no binary"; exit 1; } "$binary" > /dev/null || { echo "dist binary does not run"; exit 1; } From b03e578fea85eb1607c301d2e5d951927b372f15 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 07:47:02 +0800 Subject: [PATCH 6/6] test: e2e 67 avoid GNU sed -i (macOS BSD sed portability) --- tests/e2e/67_features_strict.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/e2e/67_features_strict.sh b/tests/e2e/67_features_strict.sh index 56bcb0a..d6023b2 100755 --- a/tests/e2e/67_features_strict.sh +++ b/tests/e2e/67_features_strict.sh @@ -92,15 +92,15 @@ fi grep -q "does not declare" b4.log || { cat b4.log; echo "missing strict error text"; exit 1; } # 4. Unknown backend on a dep that declares backend-* features → warning. -sed -i 's/backend = "a"/backend = "zzz"/' mcpp.toml +sed 's/backend = "a"/backend = "zzz"/' mcpp.toml > mcpp.toml.tmp && mv mcpp.toml.tmp mcpp.toml rm -rf target "$MCPP" build > b5.log 2>&1 || { cat b5.log; echo "unknown-backend warn path must not fail"; exit 1; } grep -q "does not declare requested feature 'backend-zzz'" b5.log \ || { cat b5.log; echo "missing unknown-backend warning"; exit 1; } -sed -i 's/backend = "zzz"/backend = "a"/' mcpp.toml +sed 's/backend = "zzz"/backend = "a"/' mcpp.toml > mcpp.toml.tmp && mv mcpp.toml.tmp mcpp.toml # 5. Unknown platform → warning by default, error under --strict. -sed -i 's/platforms = \["linux", "macos", "windows"\]/platforms = ["linux", "amiga"]/' mcpp.toml +sed 's/platforms = \["linux", "macos", "windows"\]/platforms = ["linux", "amiga"]/' mcpp.toml > mcpp.toml.tmp && mv mcpp.toml.tmp mcpp.toml rm -rf target "$MCPP" build > b6.log 2>&1 || { cat b6.log; echo "platform warn path must not fail"; exit 1; } grep -q "unknown platform 'amiga'" b6.log || { cat b6.log; echo "missing platform warning"; exit 1; }