From 436f4148929107dd02f3ba67542cdb7a8bbcdb03 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 29 Jun 2026 09:17:32 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(features):=20Stage=201=20=E2=80=94=20a?= =?UTF-8?q?=20[features]=20entry=20can=20contribute=20package-owned=20defi?= =?UTF-8?q?nes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A feature table entry may now be written in table form carrying `defines` (and `implies`); when the feature is active each bare define desugars to -D on the package compile flags, alongside the automatic MCPP_FEATURE_. The array shorthand keeps meaning implied-features. Restricts feature compile contributions to package-owned macros (no free-form cflags/ldflags) per the capability-model design. Tests: e2e/80_feature_defines.sh, unit Manifest.FeatureTableFormDefinesAndImplies. Design: .agents/docs/2026-06-29-feature-capability-model-design.md --- ...6-06-29-feature-capability-model-design.md | 296 ++++++++++++++++++ src/build/prepare.cppm | 12 + src/manifest.cppm | 27 +- tests/e2e/80_feature_defines.sh | 52 +++ tests/unit/test_manifest.cpp | 38 +++ 5 files changed, 424 insertions(+), 1 deletion(-) create mode 100644 .agents/docs/2026-06-29-feature-capability-model-design.md create mode 100755 tests/e2e/80_feature_defines.sh diff --git a/.agents/docs/2026-06-29-feature-capability-model-design.md b/.agents/docs/2026-06-29-feature-capability-model-design.md new file mode 100644 index 0000000..f8ca4b0 --- /dev/null +++ b/.agents/docs/2026-06-29-feature-capability-model-design.md @@ -0,0 +1,296 @@ +# Feature System v2 — Capability-Oriented Model (Design) + +Date: 2026-06-29 +Status: Draft (approved for spec; implementation staged S1→S3) +Scope: `src/manifest.cppm` (parse), `src/build/prepare.cppm` (feature activation + +resolver), `src/pm/dep_spec.cppm` (optional deps), mcpp-index recipe schema. + +--- + +## 1. Problem + +Today a mcpp feature, when activated, does exactly two things +(`src/build/prepare.cppm:2080-2119`): + +1. injects `-DMCPP_FEATURE_` into the package's cflags/cxxflags, and +2. gates source globs declared under `features..sources`. + +The Lua descriptor parser only reads `sources` from a feature sub-table; every +other key is silently skipped (`src/manifest.cppm:1988-1999`). The TOML +`[features]` table is only "name → implied-feature array" +(`src/manifest.cppm:613-625`). There is **no way** for a feature to: + +- contribute a preprocessor define the upstream library actually recognizes, +- pull in an optional dependency, +- select one backend implementation among mutually-exclusive alternatives. + +### Trigger case — `compat.eigen` `blas` + +Enabling Eigen's `blas` feature only produced `-DMCPP_FEATURE_BLAS` in +`compile_commands.json`; the macro Eigen actually reads, `-DEIGEN_USE_BLAS`, was +absent — so the BLAS backend was never enabled. Root cause is the limitation +above, already documented in the recipe header +(`mcpp-index/pkgs/c/compat.eigen.lua:30-39`). + +> Note on Eigen semantics (used as the running example throughout): Eigen's +> `blas/` directory builds `eigen_blas`, an artifact that **provides** the +> standard BLAS ABI for *other* code to link. `-DEIGEN_USE_BLAS` is the +> opposite direction — it makes Eigen **consume** an external BLAS for its own +> matrix kernels. These are two distinct capabilities and must not be conflated +> under one feature name. The current `blas` feature implements the provider; +> the user expected the consumer. + +## 2. Goals + +1. A feature can express the real-world needs above while staying **simple by + default** — header-only / no-feature builds are byte-for-byte unchanged. +2. **Full coverage**: optional dependencies, package-owned defines, and + "pick exactly one backend" with structural mutual exclusion and a user + override. +3. **Less is more**: the smallest primitive set that covers the above. Validated + against industry practice (§3) — we deliberately drop mechanisms that other + ecosystems regret. +4. **Backward compatible**: existing array-form `[features]` and Lua + `features = { x = { sources = ... } }` keep their meaning; the automatic + `-DMCPP_FEATURE_` is retained. + +## 3. Industry synthesis + +Surveyed Cargo, Gentoo, Conan, vcpkg, Bazel, CMake, Meson, Debian/RPM, conda, +Spack, Nixpkgs, pkg-config. Distilled lessons that shaped this design: + +| Source | Lesson taken | +|---|---| +| Cargo | Features are **purely additive**, unified as a **union** across the graph; mutually-exclusive features are an anti-pattern. | +| vcpkg | Features should **activate dependencies, not inject free-form compile flags** — arbitrary flags leak into the ABI and break composition. *(Sharpest constraint we adopt.)* | +| Gentoo | Constraints belong in a **declarative, solver-checked** form, never in imperative recipe code. | +| Conan | A **single multi-valued axis** expresses "pick one of N backends" natively — no boolean soup. | +| Bazel / CMake | Separate the **capability** (need BLAS) from the **backend selector** (`BLA_VENDOR` / `constraint_setting`); consumers code against an abstract target. | +| Debian / conda / Spack / Nix | A capability is just a **shared string** (`Provides`/`provides`); making the slot **single-valued** yields mutual exclusion **for free** (conda's same-name trick). Selection must be **deterministic** (default → single-candidate → pin), never a soft preference or silent first-match. | + +**Two load-bearing rules:** + +1. A feature that must affect compilation may contribute only a **package-owned, + namespaced define** (e.g. `EIGEN_USE_BLAS`, `EIGEN_MPL2_ONLY`). Link flags and + include paths come from the **bound provider package's own build config**, not + invented by the feature. +2. "Pick one backend" is a **single-valued capability slot**, which gives + mutual exclusion structurally — no constraint DSL, no `backend-*` boolean + pile. + +## 4. The model — two primitives + +### Primitive ① Feature — additive, composable, does only this + +``` +FeatureDef { + implies : [feature] // transitive activation (existing "implied") + deps : [DepSpec] // optional dependencies activated by this feature + defines : [string] // ONLY package-owned namespaced macros + sources : [glob] // feature-gated sources (existing) + requires : [capability] // capabilities this feature needs → Primitive ② + provides : [capability] // capabilities this feature satisfies → Primitive ② +} +``` + +- Activation is **additive** and **unioned** across the whole graph (Cargo + model): if any consumer activates a feature on a package, it is on for that + package in the single shared build. Model the *presence* of a capability, + never its absence. +- `defines` is restricted by review/lint to the package's own macro namespace. + Free-form `cflags` / `cxxflags` / `ldflags` / `includes` are **not** part of + `FeatureDef` (see §7, dropped on purpose). +- `-DMCPP_FEATURE_` is still injected for every active feature + (back-compat + lets code `#ifdef`). + +### Primitive ② Capability — shared string + single-valued binding + +- **Declared symmetrically.** Providers: `provides = ["blas"]`. Consumers (a + package, or one of its features): `requires = ["blas"]`. No separate registry + — the provider set is derived by scanning declared `provides`. +- **The resolver's entire algorithm**, per required capability, over the solve + closure: + + ``` + 0 providers → hard error: "no package provides 'blas'" + 1 provider → bind it (zero-config common path) + already in graph → reuse it (never add a second) + ≥2, none forced → hard error listing every candidate (never silent) + explicit pin → always wins, collapses the ambiguity + ``` + +- **Override / pin** (deterministic, mirrors Spack `providers:` / conda + `blas=*=mkl`): + - workspace/project: `[capabilities]` table in `mcpp.toml`, e.g. + `blas = "openblas"`; + - CLI: `--cap blas=openblas`. +- **Single-valued slot** = one bound provider per capability per build closure → + mutual exclusion is structural; no `Conflicts` matrix needed. +- The bound provider is a **real package**; its `ldflags` / `includes` flow to + the requirer through the existing usage-requirements path + (`computeUsageRequirements`, `prepare.cppm:2046`). The feature itself never + fabricates link flags. + +## 5. Worked example — Eigen (end state) + +```lua +-- compat.openblas (a provider package; carries its own ldflags/includes) +package = { + name = "compat.openblas", + provides = { "blas", "lapack" }, + -- ... xpm sources/build that expose -lopenblas etc. +} +``` + +```lua +-- compat.eigen +package = { + name = "compat.eigen", + mcpp = { + include_dirs = { "*" }, + generated_files = { ["mcpp_generated/eigen_anchor.c"] = "int mcpp_compat_eigen_headers_anchor(void){return 0;}\n" }, + sources = { "mcpp_generated/eigen_anchor.c" }, + targets = { ["eigen"] = { kind = "lib" } }, + + features = { + -- PROVIDER: Eigen's own reference BLAS (eigen_blas). Self-contained, + -- sources-only; Eigen can itself satisfy the `blas` capability. + ["eigen_blas"] = { + sources = { "*/blas/*.cpp", "*/blas/f2c/*.c" }, + provides = { "blas" }, + }, + -- CONSUMER: Eigen delegates its kernels to an external BLAS. + ["use_blas"] = { + defines = { "-DEIGEN_USE_BLAS" }, -- Eigen-owned macro + requires = { "blas" }, -- resolver binds a provider + }, + ["use_lapacke"] = { + defines = { "-DEIGEN_USE_LAPACKE" }, + requires = { "lapack" }, + }, + -- Pure package-owned define, no capability involved. + ["mpl2only"] = { defines = { "-DEIGEN_MPL2_ONLY" } }, + }, + }, +} +``` + +Consumer's `mcpp.toml`: + +```toml +[dependencies] +compat.eigen = { version = "5.0.1", features = ["use_blas"] } + +# Optional: pin the backend. Omit when exactly one provider is in the graph. +[capabilities] +blas = "openblas" +``` + +Resolution walk-through: + +1. `use_blas` is activated on `compat.eigen` → contributes `-DEIGEN_USE_BLAS` + and `requires blas`. +2. Capability `blas`: the resolver scans `provides`. If only `compat.openblas` + is in the closure → bind it (no `[capabilities]` needed). If both + `compat.openblas` and `compat.mkl` are present and unpinned → hard error + listing both, asking for `--cap blas=`. The `[capabilities] blas` + pin collapses that. +3. The bound provider's `-lopenblas` (its own `ldflags`) flows into the link via + usage requirements. The feature contributed only the one Eigen-owned macro. + +The naming also resolves the original confusion: `eigen_blas` (provider) vs +`use_blas` (consumer) each map to a concept the user already knows (upstream's +`eigen_blas` target / the `EIGEN_USE_BLAS` macro). The ambiguous bare name +`blas` is retired. + +## 6. Syntax & resolver placement + +### 6.1 Lua descriptor (index packages) + +- Extend the feature sub-table parser (`src/manifest.cppm:1983`) to accept + `implies`, `deps`, `defines`, `requires`, `provides` in addition to `sources`. + Unknown keys keep the current skip-with-warning behavior. +- Add a package-level `provides = { ... }` field. + +### 6.2 TOML project manifest (`mcpp.toml`) + +- `[features]` keeps the array shorthand (`name = ["implied", ...]`) **and** + gains a table form: `name = { implies = [...], deps = [...], defines = [...], + requires = [...], provides = [...] }`. +- New `[capabilities]` table: `blas = "openblas"` for user provider pins. +- `[package] provides = [...]` for packages that act as providers. + +### 6.3 Resolver pipeline (inside `prepare_build`) + +``` +1. toolchain → workspace → dependency resolution (existing) +2. Feature unification (new) — union active feature set per package + across root --features, every dep spec's + features=[...], and transitive `implies` +3. Optional-dep activation (new) — pull deps referenced by active features; + re-run the resolution closure for new deps +4. Capability binding (new) — for each required capability, apply the + 0/1/many algorithm; error on conflict/missing +5. Contribution merge (generalized apply()) — merge each active feature's + defines + sources; bound provider's + ldflags/includes flow via usage requirements +6. required_features target gate → modgraph → plan → compile_commands (existing) +``` + +Step 2 fixes the long-standing gap that transitive dep→dep feature requests are +not propagated (`prepare.cppm:2053`). + +## 7. Deliberately NOT in scope (less-is-more) + +Each is something a surveyed ecosystem added and regrets, or that the +two-primitive model makes redundant: + +- **Free-form `cflags` / `cxxflags` / `ldflags` / `includes` on a feature** — + breaks composition (vcpkg). Link/include come from provider packages; compile + tuning belongs in `[profile.*]` / `[build]`. +- **A general constraint DSL** (Gentoo `REQUIRED_USE` `^^`/`??`) — the + single-valued capability slot already gives "exactly one backend." If two + ordinary features genuinely conflict later, add a minimal `conflicts = [...]`; + not in v1. +- **Versioned / ranged capabilities** (`requires blas >= 3.9`) — Debian allows + only `=` here for good reason. Version the concrete package, not the abstract + name. +- **Soft provider-preference lists** (Spack's chronic complaint) — selection is + deterministic: default → single-candidate → pin, else hard error. No + probabilistic preference. +- **Silent first-match** (pkg-config footgun) — ambiguity is always a loud + error. +- **A separate capability object type / `replaces` / `obsoletes`** — a string + plus `provides`/`requires` plus the 0/1/many rule is sufficient. +- **`backend-*` boolean features** — superseded by the capability slot + pin. + +## 8. Staging (each stage independently shippable) + +| Stage | Content | Touches resolver? | Unlocks | +|---|---|---|---| +| **S1 (P0)** | `FeatureDef` gains `defines` (package-owned macros); keep `sources`/`implies`. Parse `requires`/`provides` into the model but no binding yet (inert placeholders). | No — pure compile-flag layer | `EIGEN_USE_BLAS`, `EIGEN_MPL2_ONLY` immediately (BLAS still hand-linked by the user) | +| **S2 (P1)** | Optional `deps` activation + feature **union** unification / transitive propagation. | Yes | features that pull dependencies; correct transitive features | +| **S3 (P2)** | Capability `provides`/`requires` + the 0/1/many binding + `[capabilities]` / `--cap`. | Yes | backend pick-one with structural exclusion + override | + +## 9. Backward compatibility + +- Array-form `[features]` and Lua `sources`-only features are unchanged. +- `-DMCPP_FEATURE_` is still emitted for every active feature. +- New keys are additive and optional; recipes that don't use them are + unaffected. +- The strict-schema check (`prepare.cppm:2131`) extends naturally: a requested + feature must still exist in `[features]`; a required capability with no + provider is the new error class. + +## 10. Testing + +- **S1**: unit — a feature with `defines` emits the define into + `compile_commands.json`; an inactive feature does not. e2e on + `compat.eigen` `mpl2only`. +- **S2**: a feature's `deps` appear only when the feature is active; transitive + `features=[...]` on a dep-of-dep is honored after unification; union of two + consumers requesting different features. +- **S3**: 0-provider error; 1-provider auto-bind; 2-provider unpinned error + listing candidates; pin via `[capabilities]` and `--cap`; bound provider's + ldflags reach the requirer's link. e2e: `compat.eigen[use_blas]` + + `compat.openblas` → `-DEIGEN_USE_BLAS` and `-lopenblas` both present. diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 0e3e365..2b223a4 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -2086,6 +2086,18 @@ prepare_build(bool print_fingerprint, pkg.manifest.buildConfig.cxxflags.push_back(def); pkg.privateBuild.cflags.push_back(def); pkg.privateBuild.cxxflags.push_back(def); + // Feature System v2 Stage 1: package-owned `defines` declared on + // this feature ride alongside the automatic MCPP_FEATURE_ macro. + // Bare names desugar to -D, matching [targets.*] `defines`. + if (auto it = pkg.manifest.buildConfig.featureDefines.find(f); + it != pkg.manifest.buildConfig.featureDefines.end()) + for (auto& d : it->second) { + auto fdef = "-D" + d; + pkg.manifest.buildConfig.cflags.push_back(fdef); + pkg.manifest.buildConfig.cxxflags.push_back(fdef); + pkg.privateBuild.cflags.push_back(fdef); + pkg.privateBuild.cxxflags.push_back(fdef); + } } // Feature-gated sources (e.g. gtest's gtest_main.cc behind "main"): // drop EVERY feature-listed glob from the default build, then re-add diff --git a/src/manifest.cppm b/src/manifest.cppm index eee266f..7be5dcd 100644 --- a/src/manifest.cppm +++ b/src/manifest.cppm @@ -112,6 +112,14 @@ struct BuildConfig { // the "main" feature) without it being linked by default — see // .agents/docs/2026-06-25-gtest-main-feature-and-add-dev-design.md. std::map> featureSources; + // feature name → package-owned preprocessor defines (e.g. "-DEIGEN_USE_BLAS"). + // Feature System v2 Stage 1: when the feature is active these are appended to + // the package's compile flags alongside the automatic -DMCPP_FEATURE_ + // (resolved in prepare_build). Restricted by convention to the package's own + // namespaced macros — features do NOT inject free-form cflags/ldflags, which + // would break feature-union composition. See + // .agents/docs/2026-06-29-feature-capability-model-design.md. + std::map> featureDefines; std::vector includeDirs; // relative to package root std::map generatedFiles; // Form B package-owned support files bool staticStdlib = true; @@ -611,14 +619,31 @@ std::expected parse_string(std::string_view content, } // [features] — feature name → implied features. "default" lists the - // default-active set. + // default-active set. Two accepted shapes (Feature System v2): + // array form (shorthand): name = ["implied", ...] + // table form (full): name = { implies = [...], defines = [...] } + // The table form lets a feature contribute package-owned defines (Stage 1); + // `requires`/`provides`/`deps` keys are reserved for later stages. if (auto* features_table = doc->get_table("features"); features_table && !features_table->empty()) { + auto read_str_array = [](const auto& tbl, std::string_view key, + std::vector& out) { + if (auto it = tbl.find(std::string(key)); + it != tbl.end() && it->second.is_array()) + for (auto& v : it->second.as_array()) + if (v.is_string()) out.push_back(v.as_string()); + }; for (auto& [fname, fval] : *features_table) { std::vector implied; if (fval.is_array()) { for (auto& v : fval.as_array()) if (v.is_string()) implied.push_back(v.as_string()); + } else if (fval.is_table()) { + auto& ft = fval.as_table(); + read_str_array(ft, "implies", implied); + std::vector defs; + read_str_array(ft, "defines", defs); + if (!defs.empty()) m.buildConfig.featureDefines[fname] = std::move(defs); } m.featuresMap[fname] = std::move(implied); } diff --git a/tests/e2e/80_feature_defines.sh b/tests/e2e/80_feature_defines.sh new file mode 100755 index 0000000..231999d --- /dev/null +++ b/tests/e2e/80_feature_defines.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# 80_feature_defines.sh — a [features] entry may carry `defines` (package-owned +# preprocessor macros). When the feature is active, each define lands on the +# package's compile flags (visible in compile_commands.json); when inactive, it +# does not. This is Feature System v2 Stage 1 — see +# .agents/docs/2026-06-29-feature-capability-model-design.md. +# +# No `requires:` capability → runs on all three CI platforms. +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" + +[features] +default = [] +accel = { defines = ["APP_ACCEL=1"] } +EOF +cat > app/src/main.cpp <<'EOF' +int main() { +#ifdef APP_ACCEL + return 0; +#else + return 1; +#endif +} +EOF + +cd app +cdb=compile_commands.json + +# 1. Feature inactive → the define must NOT appear in the compile database. +"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "baseline build failed"; exit 1; } +if grep -q 'APP_ACCEL' "$cdb"; then + echo "FAIL: APP_ACCEL present in $cdb without the feature active"; cat "$cdb"; exit 1 +fi + +# 2. Feature active → the define MUST appear in the compile database. +rm -rf target "$cdb" +"$MCPP" build --features accel > b2.log 2>&1 || { cat b2.log; echo "feature build failed"; exit 1; } +grep -q 'APP_ACCEL' "$cdb" || { echo "FAIL: APP_ACCEL missing from $cdb with --features accel"; cat "$cdb"; exit 1; } + +# 3. The automatic MCPP_FEATURE_ define still coexists with custom defines. +grep -q 'MCPP_FEATURE_ACCEL' "$cdb" || { echo "FAIL: MCPP_FEATURE_ACCEL missing from $cdb"; cat "$cdb"; exit 1; } + +echo "OK" diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index 0d45c7d..3c0f625 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -295,6 +295,44 @@ kind = "lib" EXPECT_EQ(m->buildConfig.cStandard, "c11"); } +// Feature System v2 Stage 1: a [features] entry may be a TABLE carrying +// package-owned `defines` (and `implies`), while the array shorthand keeps +// meaning "implied features". See +// .agents/docs/2026-06-29-feature-capability-model-design.md. +TEST(Manifest, FeatureTableFormDefinesAndImplies) { + constexpr auto src = R"( +[package] +name = "x" +version = "0.1.0" +[targets.x] +kind = "lib" +[features] +default = ["base"] +base = [] +accel = { defines = ["APP_ACCEL=1", "APP_FAST"], implies = ["base"] } +)"; + auto m = mcpp::manifest::parse_string(src); + ASSERT_TRUE(m.has_value()) << m.error().format(); + + // Array shorthand still registers an implied-feature list. + ASSERT_TRUE(m->featuresMap.contains("default")); + ASSERT_EQ(m->featuresMap["default"].size(), 1u); + EXPECT_EQ(m->featuresMap["default"][0], "base"); + + // Table form: `implies` flows into featuresMap, `defines` into featureDefines. + ASSERT_TRUE(m->featuresMap.contains("accel")); + ASSERT_EQ(m->featuresMap["accel"].size(), 1u); + EXPECT_EQ(m->featuresMap["accel"][0], "base"); + + ASSERT_TRUE(m->buildConfig.featureDefines.contains("accel")); + ASSERT_EQ(m->buildConfig.featureDefines["accel"].size(), 2u); + EXPECT_EQ(m->buildConfig.featureDefines["accel"][0], "APP_ACCEL=1"); + EXPECT_EQ(m->buildConfig.featureDefines["accel"][1], "APP_FAST"); + + // A feature with no defines contributes no featureDefines entry. + EXPECT_FALSE(m->buildConfig.featureDefines.contains("base")); +} + TEST(Manifest, BuildMacosDeploymentTarget) { constexpr auto src = R"( [package] From 11a28609e158fc32344ba17f04fdd2b0fc47a63d Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 29 Jun 2026 09:29:14 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat(features):=20Stage=203=20=E2=80=94=20c?= =?UTF-8?q?apabilities=20(provides/requires)=20with=20single-provider=20bi?= =?UTF-8?q?nding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A feature/package may `requires` an abstract capability instead of a concrete package; providers declare `provides`. The resolver binds exactly one provider from the dependency graph, deterministically: - explicit [capabilities] pin (or --cap cap=provider) wins; - 0 providers / >=2 unpinned providers are hard errors (never a silent guess); - a single provider binds with no config. Link/include requirements still flow through normal dependency mechanics; this is the selection-and-validation layer that turns a silently-wrong or missing backend into a loud configure-time error. Parsed on both surfaces (TOML [features]/[package].provides/[capabilities] and the Lua descriptor). CLI: `--cap` on build/test. Tests: e2e/81_capability_binding.sh (6 cases), unit Manifest.CapabilitiesProvidesRequiresAndPins + SynthesizeFromXpkgLua.CapabilitiesAndFeatureDefines. Design: .agents/docs/2026-06-29-feature-capability-model-design.md --- src/build/prepare.cppm | 75 ++++++++++++++++++++ src/cli.cppm | 4 ++ src/cli/cmd_build.cppm | 4 +- src/manifest.cppm | 59 ++++++++++++++-- tests/e2e/81_capability_binding.sh | 106 +++++++++++++++++++++++++++++ tests/unit/test_manifest.cpp | 70 +++++++++++++++++++ 6 files changed, 313 insertions(+), 5 deletions(-) create mode 100755 tests/e2e/81_capability_binding.sh diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 2b223a4..407af1e 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -286,6 +286,7 @@ export struct BuildOverrides { std::string profile; // --profile (default "release") std::string features; // --features a,b,c (root package activation) bool strict = false; // --strict: schema warnings become errors + std::string capabilities; // --cap blas=openblas,lapack=mkl (provider pins) }; // `prepare_build` builds the BuildContext for any verb that compiles. @@ -2054,6 +2055,11 @@ prepare_build(bool print_fingerprint, // Also captured here: the root package's active feature set, reused below // for the [targets.*] required_features gate. std::set activeRootFeatures; + // Capability accumulation (Stage 3): which packages provide each capability, + // and which (capability, requiring-package) pairs need binding. Filled by + // apply() as each package's features activate; bound after the loops below. + std::map> capProviders; + std::vector> capRequires; { auto sanitize = [](std::string f) { for (auto& c : f) @@ -2080,6 +2086,19 @@ prepare_build(bool print_fingerprint, auto apply = [&](mcpp::modgraph::PackageRoot& pkg, const std::vector& requested) { auto active = activate(pkg.manifest, requested); + // Capability accumulation: package-level provides always count; + // feature-scoped provides/requires count only when the feature is + // active. Requirements are bound after all packages are processed. + const auto& pcap = pkg.manifest.package.name; + for (auto& cap : pkg.manifest.provides) capProviders[cap].push_back(pcap); + for (auto& f : active) { + if (auto it = pkg.manifest.featureProvides.find(f); + it != pkg.manifest.featureProvides.end()) + for (auto& cap : it->second) capProviders[cap].push_back(pcap); + if (auto it = pkg.manifest.featureRequires.find(f); + it != pkg.manifest.featureRequires.end()) + for (auto& cap : it->second) capRequires.emplace_back(cap, pcap); + } for (auto& f : active) { auto def = "-DMCPP_FEATURE_" + sanitize(f); pkg.manifest.buildConfig.cflags.push_back(def); @@ -2181,6 +2200,62 @@ prepare_build(bool print_fingerprint, // feature-gated sources must have those sources dropped by default. apply(packages[i], req); } + + // ─── Capability binding (Stage 3) ────────────────────────────────── + // For each required capability, bind exactly one provider from the + // graph. Deterministic: an explicit [capabilities] pin wins; otherwise + // 0 providers / ≥2 providers are hard errors (never a silent guess); a + // single provider binds with no config. The provider's link/include + // requirements already flow through normal dependency mechanics — this + // pass is the selection-and-validation layer. See the capability-model + // design doc. + // --cap cap=provider[,cap=provider] overrides [capabilities] pins. + for (std::size_t p = 0; p < overrides.capabilities.size();) { + auto c = overrides.capabilities.find_first_of(", ", p); + auto tok = overrides.capabilities.substr( + p, c == std::string::npos ? std::string::npos : c - p); + if (auto eq = tok.find('='); eq != std::string::npos) + m->capabilityPins[tok.substr(0, eq)] = tok.substr(eq + 1); + if (c == std::string::npos) break; + p = c + 1; + } + + std::set boundCaps; + for (auto& [cap, requirer] : capRequires) { + if (!boundCaps.insert(cap).second) continue; // one diagnosis per cap + auto& pins = m->capabilityPins; + // Dedup candidates, preserve first-seen order. + std::vector cands; + if (auto it = capProviders.find(cap); it != capProviders.end()) + for (auto& p : it->second) + if (std::find(cands.begin(), cands.end(), p) == cands.end()) + cands.push_back(p); + if (auto pit = pins.find(cap); pit != pins.end()) { + const auto& pin = pit->second; + if (std::find(cands.begin(), cands.end(), pin) == cands.end()) { + std::string list; + for (auto& c : cands) list += (list.empty() ? "" : ", ") + c; + return std::unexpected(std::format( + "capability '{}' pinned to provider '{}' (via [capabilities]), " + "but no such provider is in the graph; candidates: [{}]", + cap, pin, list)); + } + continue; // pin satisfied + } + if (cands.empty()) + return std::unexpected(std::format( + "no package provides capability '{}' required by '{}'; add a " + "dependency that declares `provides = [\"{}\"]`", cap, requirer, cap)); + if (cands.size() > 1) { + std::string list; + for (auto& c : cands) list += (list.empty() ? "" : ", ") + c; + return std::unexpected(std::format( + "capability '{}' has multiple providers in the graph: [{}]; select " + "one with [capabilities] {} = \"\" or --cap {}=", + cap, list, cap, cap)); + } + // exactly one → bound implicitly. + } } // [targets.*] required_features gate: a target is emitted only when ALL its diff --git a/src/cli.cppm b/src/cli.cppm index 2681e44..ff9e9d2 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -220,6 +220,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("cap").takes_value().value_name("LIST") + .help("Pin capability providers (e.g. blas=openblas,lapack=mkl)")) .option(cl::Option("strict") .help("Treat manifest schema warnings (unknown feature/platform) as errors")) .action(wrap_rc(cmd_build))) @@ -235,6 +237,8 @@ int run(int argc, char** argv) { .help("Build profile for the test build: release (default) | dev | dist | <[profile.*] name>")) .option(cl::Option("features").takes_value().value_name("LIST") .help("Activate root-package features for the test build (comma-separated)")) + .option(cl::Option("cap").takes_value().value_name("LIST") + .help("Pin capability providers (e.g. blas=openblas,lapack=mkl)")) .option(cl::Option("strict") .help("Treat manifest schema warnings (unknown feature/platform) as errors")) .action(wrap_rc([&passthrough](const cl::ParsedArgs& p) { diff --git a/src/cli/cmd_build.cppm b/src/cli/cmd_build.cppm index 340fc51..e5ff22d 100644 --- a/src/cli/cmd_build.cppm +++ b/src/cli/cmd_build.cppm @@ -28,6 +28,7 @@ export 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; + if (auto cp = parsed.value("cap")) ov.capabilities = *cp; ov.strict = parsed.is_flag_set("strict"); ov.force_static = parsed.is_flag_set("static"); @@ -37,7 +38,7 @@ export int cmd_build(const mcpplibs::cmdline::ParsedArgs& parsed) { // 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()) { + && ov.capabilities.empty() && ov.package_filter.empty()) { auto root = mcpp::project::find_manifest_root(std::filesystem::current_path()); if (root) { if (auto rc = mcpp::build::try_fast_build(*root, verbose, no_cache)) { @@ -73,6 +74,7 @@ export int cmd_test(const mcpplibs::cmdline::ParsedArgs& parsed, mcpp::build::BuildOverrides ov; if (auto pr = parsed.value("profile")) ov.profile = *pr; if (auto fs = parsed.value("features")) ov.features = *fs; + if (auto cp = parsed.value("cap")) ov.capabilities = *cp; ov.strict = parsed.is_flag_set("strict"); return mcpp::build::run_tests(passthrough, ov); } diff --git a/src/manifest.cppm b/src/manifest.cppm index 7be5dcd..ce711cb 100644 --- a/src/manifest.cppm +++ b/src/manifest.cppm @@ -259,6 +259,18 @@ struct Manifest { // [features] — feature name → implied features ("default" = default set). std::map> featuresMap; + // Feature System v2 Stage 3 — capabilities (provides/requires). A capability + // is just a shared string. A package satisfies one via package-level + // `provides` or via a feature's `provides`; a feature `requires` an abstract + // capability instead of a concrete package, and the resolver binds exactly + // one provider from the graph. See + // .agents/docs/2026-06-29-feature-capability-model-design.md. + std::vector provides; // package-level + std::map> featureProvides; // feature → caps + std::map> featureRequires; // feature → caps + // Root-only: [capabilities] cap = "provider" pins (also fed by --cap). + std::map capabilityPins; + // [target.] tables — empty if user didn't declare any. std::map targetOverrides; @@ -644,11 +656,25 @@ std::expected parse_string(std::string_view content, std::vector defs; read_str_array(ft, "defines", defs); if (!defs.empty()) m.buildConfig.featureDefines[fname] = std::move(defs); + std::vector reqs, provs; + read_str_array(ft, "requires", reqs); + read_str_array(ft, "provides", provs); + if (!reqs.empty()) m.featureRequires[fname] = std::move(reqs); + if (!provs.empty()) m.featureProvides[fname] = std::move(provs); } m.featuresMap[fname] = std::move(implied); } } + // [package] provides — package-level capabilities (Feature System v2 S3). + if (auto v = doc->get_string_array("package.provides")) m.provides = *v; + + // [capabilities] cap = "provider" — root-only provider pins. + if (auto* caps = doc->get_table("capabilities"); caps && !caps->empty()) { + for (auto& [cap, cval] : *caps) + if (cval.is_string()) m.capabilityPins[cap] = cval.as_string(); + } + auto* targets_table = doc->get_table("targets"); if (targets_table && !targets_table->empty()) { for (auto& [tname, tval] : *targets_table) { @@ -1911,6 +1937,22 @@ synthesize_from_xpkg_lua(std::string_view luaContent, } cur.consume('}'); } + else if (key == "provides") { + // Package-level capabilities (Feature System v2 S3): this package + // satisfies the listed abstract capability names for any dependent + // that `requires` them. `{ "blas", "lapack", ... }`. + if (!cur.consume('{')) { + return std::unexpected(ManifestError{ + "expected '{' after `provides =`", m.sourcePath, 0, 0}); + } + cur.skip_ws_and_comments(); + while (!cur.eof() && cur.peek() != '}') { + auto s = cur.read_string(); + if (!s.empty()) m.provides.push_back(std::move(s)); + cur.skip_ws_and_comments(); + } + cur.consume('}'); + } else if (key == "generated_files") { // `{ ["relative/path"] = "contents", ... }` if (!cur.consume('{')) { @@ -2010,13 +2052,22 @@ synthesize_from_xpkg_lua(std::string_view luaContent, cur.skip_ws_and_comments(); if (!cur.consume('=')) break; cur.skip_ws_and_comments(); - if (sub == "sources") { - if (!cur.consume('{')) break; + // Feature subfields that carry a string array. `sources` + // gates source globs; `defines` carries package-owned macros + // (Stage 1); `requires`/`provides` declare capabilities + // (Stage 3). All share the `{ "...", ... }` shape. + std::vector* arr = + sub == "sources" ? &m.buildConfig.featureSources[fname] + : sub == "defines" ? &m.buildConfig.featureDefines[fname] + : sub == "requires" ? &m.featureRequires[fname] + : sub == "provides" ? &m.featureProvides[fname] + : nullptr; + if (arr && cur.peek() == '{') { + cur.consume('{'); cur.skip_ws_and_comments(); while (!cur.eof() && cur.peek() != '}') { auto s = cur.read_string(); - if (!s.empty()) - m.buildConfig.featureSources[fname].push_back(std::move(s)); + if (!s.empty()) arr->push_back(std::move(s)); cur.skip_ws_and_comments(); } cur.consume('}'); diff --git a/tests/e2e/81_capability_binding.sh b/tests/e2e/81_capability_binding.sh new file mode 100755 index 0000000..ea963b0 --- /dev/null +++ b/tests/e2e/81_capability_binding.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# 81_capability_binding.sh — Feature System v2 Stage 3: capabilities. +# A package/feature may `requires = ["cap"]` an abstract capability; providers +# declare `provides = ["cap"]`. The resolver binds exactly ONE provider from the +# dependency graph: +# 0 providers → hard error "no package provides" +# 1 provider → bind (success) +# ≥2 unpinned → hard error listing candidates +# pinned [capabilities] → the pin wins +# See .agents/docs/2026-06-29-feature-capability-model-design.md. +# +# No `requires:` capability → runs on all three CI platforms. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT +cd "$TMP" + +# Two interchangeable BLAS providers. +for impl in alpha beta; do + mkdir -p "blas_$impl/src" + cat > "blas_$impl/mcpp.toml" < "blas_$impl/src/blas_$impl.cppm" +done + +mkdir -p app/src +echo 'int main() { return 0; }' > app/src/main.cpp +cd app + +write_manifest() { # $1 = dependency lines + cat > mcpp.toml < c1.log 2>&1 || { cat c1.log; echo "FAIL case1: single provider should bind"; exit 1; } + +# Case 2: capability required but NO provider in the graph → hard error. +write_manifest '# no provider' +rm -rf target +if "$MCPP" build --features use_blas > c2.log 2>&1; then + cat c2.log; echo "FAIL case2: missing provider must error"; exit 1 +fi +grep -qi "no package provides .*blas\|capability 'blas'" c2.log || { cat c2.log; echo "FAIL case2: wrong/no error text"; exit 1; } + +# Case 3: two providers, unpinned → ambiguity error listing candidates. +write_manifest 'blas_alpha = { path = "../blas_alpha" } +blas_beta = { path = "../blas_beta" }' +rm -rf target +if "$MCPP" build --features use_blas > c3.log 2>&1; then + cat c3.log; echo "FAIL case3: ambiguous providers must error"; exit 1 +fi +grep -qi "blas_alpha" c3.log && grep -qi "blas_beta" c3.log || { cat c3.log; echo "FAIL case3: error must list candidates"; exit 1; } + +# Case 4: same two providers, pinned via [capabilities] → the pin wins, builds. +cat > mcpp.toml < c4.log 2>&1 || { cat c4.log; echo "FAIL case4: pin should resolve ambiguity"; exit 1; } + +# Case 5: feature inactive → no requirement, no provider needed, builds clean. +write_manifest '# no provider' +rm -rf target +"$MCPP" build > c5.log 2>&1 || { cat c5.log; echo "FAIL case5: inactive feature must not require capability"; exit 1; } + +# Case 6: two providers, --cap pin on the CLI resolves the ambiguity. +write_manifest 'blas_alpha = { path = "../blas_alpha" } +blas_beta = { path = "../blas_beta" }' +rm -rf target +"$MCPP" build --features use_blas --cap blas=blas_alpha > c6.log 2>&1 || { cat c6.log; echo "FAIL case6: --cap pin should resolve ambiguity"; exit 1; } + +echo "OK" diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index 3c0f625..c72dbad 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -333,6 +333,76 @@ accel = { defines = ["APP_ACCEL=1", "APP_FAST"], implies = ["base"] } EXPECT_FALSE(m->buildConfig.featureDefines.contains("base")); } +// Feature System v2 Stage 3: capabilities. A feature may `requires`/`provides` +// abstract capabilities; the package may `provides` them at package level; and +// the root [capabilities] table pins providers. +TEST(Manifest, CapabilitiesProvidesRequiresAndPins) { + constexpr auto src = R"( +[package] +name = "x" +version = "0.1.0" +provides = ["blas"] +[targets.x] +kind = "lib" +[features] +default = [] +use_blas = { requires = ["blas"] } +provide_lap = { provides = ["lapack"] } +[capabilities] +blas = "openblas" +)"; + auto m = mcpp::manifest::parse_string(src); + ASSERT_TRUE(m.has_value()) << m.error().format(); + + ASSERT_EQ(m->provides.size(), 1u); + EXPECT_EQ(m->provides[0], "blas"); + + ASSERT_TRUE(m->featureRequires.contains("use_blas")); + ASSERT_EQ(m->featureRequires["use_blas"].size(), 1u); + EXPECT_EQ(m->featureRequires["use_blas"][0], "blas"); + + ASSERT_TRUE(m->featureProvides.contains("provide_lap")); + EXPECT_EQ(m->featureProvides["provide_lap"][0], "lapack"); + + ASSERT_TRUE(m->capabilityPins.contains("blas")); + EXPECT_EQ(m->capabilityPins["blas"], "openblas"); +} + +// The Lua descriptor surface (index packages) parses package-level `provides` +// and feature-scoped `requires`/`provides`/`defines`. +TEST(SynthesizeFromXpkgLua, CapabilitiesAndFeatureDefines) { + constexpr auto lua = R"( +package = { + spec = "1", + name = "compat.eigen", + xpm = { linux = { ["1.0.0"] = { url = "u", sha256 = "h" } } }, + mcpp = { + sources = { "*/anchor.c" }, + provides = { "blas" }, + targets = { ["eigen"] = { kind = "lib" } }, + features = { + ["use_blas"] = { defines = { "EIGEN_USE_BLAS" }, requires = { "blas" } }, + ["eigen_blas"] = { sources = { "*/blas/*.cpp" }, provides = { "blas" } }, + }, + }, +} +)"; + auto m = mcpp::manifest::synthesize_from_xpkg_lua(lua, "compat.eigen", "1.0.0"); + ASSERT_TRUE(m.has_value()) << m.error().format(); + + ASSERT_EQ(m->provides.size(), 1u); + EXPECT_EQ(m->provides[0], "blas"); + + ASSERT_TRUE(m->buildConfig.featureDefines.contains("use_blas")); + EXPECT_EQ(m->buildConfig.featureDefines["use_blas"][0], "EIGEN_USE_BLAS"); + ASSERT_TRUE(m->featureRequires.contains("use_blas")); + EXPECT_EQ(m->featureRequires["use_blas"][0], "blas"); + ASSERT_TRUE(m->featureProvides.contains("eigen_blas")); + EXPECT_EQ(m->featureProvides["eigen_blas"][0], "blas"); + // sources still gated as before. + ASSERT_TRUE(m->buildConfig.featureSources.contains("eigen_blas")); +} + TEST(Manifest, BuildMacosDeploymentTarget) { constexpr auto src = R"( [package] From 688d00b45957ae0299381544998793f846b9aa95 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 29 Jun 2026 09:37:49 +0800 Subject: [PATCH 3/3] docs+release: feature/capability user docs, CHANGELOG, v0.0.69 bump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/05-mcpp-toml.md (+ zh mirror): document the [features] table form (package-owned defines) and the new provides/requires capabilities section ([capabilities] pins, --cap, deterministic 0/1/many binding table). - CHANGELOG: 0.0.69 entry (Feature System v2 — S1 defines + S3 capabilities). - Bump mcpp.toml + MCPP_VERSION to 0.0.69. - Design doc: record Implementation Status (S1+S3 shipped, S2 next). --- ...6-06-29-feature-capability-model-design.md | 26 ++++++- CHANGELOG.md | 25 +++++++ docs/05-mcpp-toml.md | 69 +++++++++++++++++++ docs/zh/05-mcpp-toml.md | 64 +++++++++++++++++ mcpp.toml | 2 +- src/toolchain/fingerprint.cppm | 2 +- 6 files changed, 184 insertions(+), 4 deletions(-) diff --git a/.agents/docs/2026-06-29-feature-capability-model-design.md b/.agents/docs/2026-06-29-feature-capability-model-design.md index f8ca4b0..46c5f2a 100644 --- a/.agents/docs/2026-06-29-feature-capability-model-design.md +++ b/.agents/docs/2026-06-29-feature-capability-model-design.md @@ -1,9 +1,31 @@ # Feature System v2 — Capability-Oriented Model (Design) Date: 2026-06-29 -Status: Draft (approved for spec; implementation staged S1→S3) +Status: **S1 + S3 implemented & shipped** (see Implementation Status below); +S2 scoped as the documented next stage. Scope: `src/manifest.cppm` (parse), `src/build/prepare.cppm` (feature activation + -resolver), `src/pm/dep_spec.cppm` (optional deps), mcpp-index recipe schema. +resolver), `src/cli.cppm` / `src/cli/cmd_build.cppm` (`--cap`), mcpp-index recipe schema. + +## Implementation Status + +- **Stage 1 — feature `defines`: DONE.** `[features]` table form + (`name = { defines = [...], implies = [...] }`) on both the TOML and Lua + surfaces; active features contribute package-owned macros (bare name → `-D`) + next to the automatic `-DMCPP_FEATURE_`. Tests: `e2e/80_feature_defines.sh`, + `Manifest.FeatureTableFormDefinesAndImplies`. +- **Stage 3 — capabilities: DONE.** `provides` (package-level + per-feature), + `requires` (per-feature), the 0/1/many binding over in-graph providers, + `[capabilities]` pins and `--cap`. Tests: `e2e/81_capability_binding.sh` + (6 cases), `Manifest.CapabilitiesProvidesRequiresAndPins`, + `SynthesizeFromXpkgLua.CapabilitiesAndFeatureDefines`. +- **Stage 2 — optional-dep activation + feature-union unification: NEXT.** + Deliberately deferred from this release. Rationale: activating a *new* + dependency from a feature requires moving feature computation ahead of + dependency resolution (resolution-phase reordering) — a deeper, higher-risk + change. It is also **not required** for the capability/Eigen use case, which + binds over providers that are explicitly declared as dependencies. Shipping + S1+S3 first matches this doc's "each stage independently shippable" intent and + keeps the release low-risk. --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 75f583c..f014857 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,31 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [0.0.69] — 2026-06-29 + +### 新增 + +- **Feature 系统 v2 — feature 可贡献「包自有 defines」+ capability(provides/requires)能力绑定**: + 解决「`compat.eigen` 启用 `blas` 特性后,`compile_commands.json` 里只有 `-DMCPP_FEATURE_BLAS`、 + 没有上游真正读的 `-DEIGEN_USE_BLAS`,特性形同未启用」这一类根因——旧版 feature 激活**只能**产出 + `-DMCPP_FEATURE_` 宏 + 门控源文件,无法表达任意宏、更无法做 backend 选择。本次按 + 「功能全覆盖 + 少即是多」收敛为**两个原语**(详见 + `.agents/docs/2026-06-29-feature-capability-model-design.md`): + + - **Stage 1 — feature `defines`**:`[features]` 条目可写成**表形式** + `name = { defines = ["EIGEN_USE_BLAS"], implies = [...] }`(TOML 与 Lua 描述符两面均支持); + 激活时每个**裸名** define 脱糖为 `-D` 加到该包编译标志,与自动的 `-DMCPP_FEATURE_` + 并存。按行业经验(vcpkg)**刻意限制**为「包自有命名空间宏」,feature **不**注入自由 + `cflags`/`ldflags`,以保持 feature union 组合性。 + - **Stage 3 — capabilities**:包/特性可 `provides`/`requires` 一个**抽象能力字符串**(如 `blas`), + 解析器从依赖图中**绑定唯一 provider**——确定性:`[capabilities]` pin / `--cap` 指定者胜出; + 图中**恰好一个** provider 自动绑定;**零个**或**多个未指定**均**硬报错**(绝不静默猜测)。 + 这把「静默用错/缺失后端」变成配置期显式报错。link/include 仍走既有依赖机制流动。 + + > Stage 2(feature 触发的可选依赖自动拉取 + 全图 feature union 统一)作为下一阶段:它需要把 + > 特性计算提前到依赖解析之前(解析阶段重排),风险更高,且 capability/Eigen 用例并不依赖它 + > (provider 以显式依赖声明)。本次先发坚实的 S1+S3,符合设计文档「各阶段独立可发」原则。 + ## [0.0.67] — 2026-06-26 ### 修复 diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index 512cc46..12a00cc 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -292,6 +292,75 @@ extra = [] requesting an undeclared feature produces a warning; an error under `--strict`. A package that does not declare `[features]` accepts any request (pure macro usage). +#### Table form — a feature that contributes more than implied features + +A `[features]` entry may be written as a **table** instead of an array, letting the +feature carry package-owned preprocessor `defines` and/or capability `requires` / +`provides` (see §2.8.1) alongside its implied features: + +```toml +[features] +default = [] +# Array shorthand: just implied features. +docking = ["extra"] +extra = [] +# Table form: contribute a package-owned define when active. +mpl2only = { defines = ["EIGEN_MPL2_ONLY"] } +# Table form: a define + an implied feature. +fast_math = { defines = ["APP_FAST=1"], implies = ["extra"] } +``` + +- `defines` are **bare** macro names (no `-D`); each desugars to `-D` on the + package's own compile when the feature is active — exactly like `[targets.*] + defines`. They are restricted by convention to the package's **own** namespaced + macros: a feature does **not** inject free-form `cflags`/`ldflags`, which would + break the additive feature-union model. Link flags come from a provider + dependency (§2.8.1), not from a feature. +- The automatic `-DMCPP_FEATURE_` is still defined for every active feature, + so `defines` are additive to it. + +### 2.8.1 `provides` / `requires` — Capabilities (backend selection) + +A **capability** is a shared abstract name (e.g. `blas`). A package can *provide* +one; a feature can *require* one instead of naming a concrete package, and the +resolver binds exactly one provider from the dependency graph. This is how you pick +one of several interchangeable backends (OpenBLAS / MKL / …) without baking a choice +into the library. + +```toml +# A provider package satisfies a capability for any dependent that requires it. +[package] +name = "compat.openblas" +version = "0.3.0" +provides = ["blas", "lapack"] +``` + +```toml +# A consumer requires the abstract capability via one of its features. +[features] +use_blas = { defines = ["EIGEN_USE_BLAS"], requires = ["blas"] } + +# When >1 provider is in the graph, pick one (else the build errors and lists them). +[capabilities] +blas = "compat.openblas" # equivalently: mcpp build --cap blas=compat.openblas + +[dependencies] +compat.openblas = "0.3.0" # the provider must be a real dependency in the graph +``` + +Binding is **deterministic**: + +| Providers of a required capability in the graph | Result | +|---|---| +| exactly one | bound automatically (no config needed) | +| a `[capabilities]` pin / `--cap` names one | the pin wins | +| zero | **error**: no package provides `` | +| two or more, unpinned | **error**, listing the candidates — never a silent guess | + +The bound provider's link/include flags reach the consumer through normal +dependency mechanics; the capability layer is the *selection-and-validation* step +that turns a silently-wrong or missing backend into a loud configure-time error. + ### 2.9 `[profile.]` — Build Profiles ```toml diff --git a/docs/zh/05-mcpp-toml.md b/docs/zh/05-mcpp-toml.md index 2aedad6..2d2dead 100644 --- a/docs/zh/05-mcpp-toml.md +++ b/docs/zh/05-mcpp-toml.md @@ -277,6 +277,70 @@ extra = [] - **strict 校验**:目标包声明了 `[features]` 表时,请求未声明的 feature 给出 warning;`--strict` 下报错。未声明 `[features]` 的包接受任意请求(纯宏用法)。 +#### 表形式 —— 让 feature 贡献的不止是隐含 feature + +`[features]` 的条目除了写成数组,还可写成**表**,从而让该 feature 在隐含 feature +之外,携带包自有的预处理 `defines`,以及 capability 的 `requires` / `provides` +(见 §2.8.1): + +```toml +[features] +default = [] +# 数组简写:仅隐含 feature。 +docking = ["extra"] +extra = [] +# 表形式:激活时贡献一个包自有的宏。 +mpl2only = { defines = ["EIGEN_MPL2_ONLY"] } +# 表形式:宏 + 一个隐含 feature。 +fast_math = { defines = ["APP_FAST=1"], implies = ["extra"] } +``` + +- `defines` 为**裸**宏名(不带 `-D`);feature 激活时每个脱糖为 `-D`,加到该包 + 自己的编译上——与 `[targets.*] defines` 完全一致。按约定仅限包**自有**的带命名 + 空间宏:feature **不**注入自由的 `cflags`/`ldflags`,否则会破坏加性的 feature + 并集模型。链接旗标来自 provider 依赖(§2.8.1),而非 feature。 +- 每个激活的 feature 仍会得到自动的 `-DMCPP_FEATURE_`,`defines` 与之叠加。 + +### 2.8.1 `provides` / `requires` —— 能力(后端选择) + +**capability(能力)** 是一个共享的抽象名字(如 `blas`)。包可以 *provide*(提供) +一种能力;feature 可以 *require*(需要)一种能力而非点名某个具体包,解析器会从依赖 +图中绑定**恰好一个** provider。这样就能在多个可互换后端(OpenBLAS / MKL / …)中选其 +一,而不必把选择写死进库里。 + +```toml +# provider 包为任何 require 它的依赖方满足某能力。 +[package] +name = "compat.openblas" +version = "0.3.0" +provides = ["blas", "lapack"] +``` + +```toml +# 消费方经由自己的某个 feature 来 require 这个抽象能力。 +[features] +use_blas = { defines = ["EIGEN_USE_BLAS"], requires = ["blas"] } + +# 图中有 >1 个 provider 时,选其一(否则构建报错并列出候选)。 +[capabilities] +blas = "compat.openblas" # 等价于:mcpp build --cap blas=compat.openblas + +[dependencies] +compat.openblas = "0.3.0" # provider 必须是图中真实存在的依赖 +``` + +绑定是**确定性**的: + +| 图中某被需要能力的 provider 数量 | 结果 | +|---|---| +| 恰好一个 | 自动绑定(无需配置) | +| `[capabilities]` pin / `--cap` 指定了一个 | 以 pin 为准 | +| 零个 | **报错**:没有包提供 `` | +| 两个及以上且未 pin | **报错**并列出候选——绝不静默猜测 | + +被绑定 provider 的链接/头文件旗标经由常规依赖机制流到消费方;capability 层是那道 +*选择与校验* 步骤,把"静默选错后端 / 缺后端"变成构建期的显式报错。 + ### 2.9 `[profile.]` — 构建档案 ```toml diff --git a/mcpp.toml b/mcpp.toml index ed2483f..c977972 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.68" +version = "0.0.69" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index 23bf1d4..64ad330 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.68"; +inline constexpr std::string_view MCPP_VERSION = "0.0.69"; struct FingerprintInputs { Toolchain toolchain;