Skip to content

feat(features): Feature System v2 — package-owned defines + capabilities (v0.0.69)#181

Merged
Sunrisepeak merged 3 commits into
mainfrom
feat/feature-capability-model
Jun 29, 2026
Merged

feat(features): Feature System v2 — package-owned defines + capabilities (v0.0.69)#181
Sunrisepeak merged 3 commits into
mainfrom
feat/feature-capability-model

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

What & why

Fixes the class of bug where enabling a feature could not actually configure the
upstream library: e.g. compat.eigen with the blas feature emitted only
-DMCPP_FEATURE_BLAS, never the -DEIGEN_USE_BLAS that Eigen actually reads — so
the backend was never enabled. Root cause: a feature could only ever produce
-DMCPP_FEATURE_<NAME> + gate sources; it could not contribute an arbitrary
define, nor select a backend.

Design (industry-surveyed, "full coverage + less is more"):
.agents/docs/2026-06-29-feature-capability-model-design.md. Converges on two
primitives
and deliberately drops what other ecosystems regret (free-form
per-feature flags → vcpkg; constraint DSLs / soft preferences / silent first-match
→ Spack/pkg-config).

What's in this release (S1 + S3)

Stage 1 — feature defines

[features] entries may be written in table form carrying package-owned
defines (and implies), on both the TOML manifest and the Lua descriptor:

[features]
use_blas = { defines = ["EIGEN_USE_BLAS"], implies = ["base"] }

Bare names desugar to -D<x> (like [targets.*] defines), alongside the automatic
-DMCPP_FEATURE_<NAME>. Restricted by convention to package-owned macros — no
free-form cflags/ldflags, preserving feature-union composability.

Stage 3 — capabilities (provides/requires)

A capability is a shared string. A package/feature provides it; a feature
requires it instead of a concrete package. The resolver binds exactly one
provider from the graph, deterministically:

Providers in graph Result
exactly one bound automatically
[capabilities] pin / --cap the pin wins
zero hard error
≥2 unpinned hard error listing candidates (never a silent guess)

Link/include flow through normal dependency mechanics; this is the
selection-and-validation layer. CLI: --cap blas=openblas on build/test.

Deferred: Stage 2

Optional-dep auto-pull + full feature-union unification need resolution-phase
reordering (higher risk) and are not required for the capability/Eigen use
case (providers are explicit dependencies). Documented as the next stage in the
design doc; this release ships S1+S3 per the "independently shippable stages"
intent.

Tests

  • e2e: 80_feature_defines.sh, 81_capability_binding.sh (6 cases).
  • unit: Manifest.FeatureTableFormDefinesAndImplies,
    Manifest.CapabilitiesProvidesRequiresAndPins,
    SynthesizeFromXpkgLua.CapabilitiesAndFeatureDefines.
  • Full unit suite green; feature + core e2e green locally
    (54_package_owned_ldflags fails pre-existing/environmental — gcc path,
    reproduces on the bootstrap binary unchanged).

Docs

  • docs/05-mcpp-toml.md (+ zh mirror): table-form features + capabilities section.
  • CHANGELOG: 0.0.69. Version bumped (mcpp.toml + MCPP_VERSION).

…wned defines

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<x> on the package compile flags, alongside the automatic MCPP_FEATURE_<NAME>.
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
…e-provider binding

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
- 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).
@Sunrisepeak Sunrisepeak merged commit dea776e into main Jun 29, 2026
4 of 6 checks passed
@Sunrisepeak Sunrisepeak deleted the feat/feature-capability-model branch June 29, 2026 02:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant