ADR-0017: Features belong to the package that declares them; dep/feature forwards
Status: Accepted
Context
[features] was resolved once, from the root package, into one flat set of
names. Every feature.<name> reference in every package’s dowel.build was
answered from that single set.
Two things followed, neither intended.
- A feature of one package answered for another.
feature.zlibin a dependency was true whenever anything in the build had enabled a feature spelledzlib. Two packages that happen to name a feature the same way were not two features dep/featuredid nothing. Writingdeep = ["core/deep"]— the Cargo-shaped spelling for “enabledeepincore” — put the literal stringcore/deepin the set. Nothing translated it, sofeature.deepinsidecorestayed false. It appeared to work whenever the parent and child feature names coincided, because the parent’s own name was already in the shared set
The second is the worse of the two: the manifest reads as though the
dependency is being configured, and nothing says otherwise. Validation
already treated features as package-scoped — feature.<name> must be
declared in that package’s [features] — so the reference side and the
activation side disagreed.
Decision
A feature belongs to the package that declares it. Activation is resolved
per package, and a value in [features] may name a dependency’s feature:
- A plain name (
fast) enables that feature in this package, and closes transitively over this package’s own[features] dep/featenablesfeatin the dependencydep. It does not become a feature of the declaring packagedepmust be declared in[[dependencies]]; otherwiseundeclared-dependency, the same code adep("...")reference getsfeatmust be declared in that dependency’s[features]; otherwiseunknown-feature, reported at the forwarding site
The active set is carried as <package>/<feature> pairs, and
feature.<name> is answered by qualifying with the package whose manifest
the value was declared in. Specialization therefore happens per package
(Config::for_package), which is where the two sides are reconciled.
Because a forwarded feature can activate an optional dependency inside the
dependency — which changes what gets loaded — loading and feature resolution
are mutually dependent. The walk is repeated until the requested sets stop
growing. Sets only grow, so this terminates; loading, git fetching, and
pkg-config resolution are memoized, so later rounds have no external
effects.
Consequences
- Two packages may use the same feature name for unrelated things. This was
already what the documentation implied and what
feature.<name>validation enforced; only activation had to catch up - A manifest that relied on the old leakage — a dependency’s
feature.<name>being satisfied by the root’s identically-named feature — changes behavior. That reliance was not expressible on purpose, and the fix is to forward explicitly - Forwarding is one level deep per declaration, but composes: a dependency
may itself forward onward with its own
dep/featentries - The configuration identifier now carries qualified names. It stays one
path component — the
/is folded, as issue #68 required — and it distinguishes configurations that the flat set could not - The build directory name changes for any build that enables a feature —
-zlibbecomes-app--zlib. It is an opaque identifier, and the change costs one rebuild