Skip to content

← Modules, Visibility, Testing and Docs step 16 of 22

Hard Framework

Cargo features and the additivity rule

Enabling a Cargo feature can enable other features and optional dependencies. Those can enable more features in turn. Implement a resolver that follows these links until nothing new becomes active:

pub fn resolve_features(
    features: Vec<(String, Vec<String>)>,
    optional_deps: Vec<String>,
    requested: Vec<String>,
    no_default: bool,
) -> Vec<String>

This is a simplified model of one package's feature table. It does not read Cargo manifests or resolve dependency versions, targets or dependency feature tables. Return a sorted, duplicate-free Vec<String> of activation markers.

Build the feature table

features contains (name, entries) pairs. If a name appears more than once, use its last entry list. optional_deps names optional dependencies.

Before resolving requests, add an implicit feature for each optional dependency that is never mentioned as dep:name anywhere in the table. The implicit feature is named name and contains the single entry dep:name. Keep an explicitly defined feature of that name instead of replacing it.

For example, an optional dependency named log normally adds the feature log = ["dep:log"]. A dep:log entry in any explicit feature suppresses that automatic addition, even if that explicit feature is never activated.

Start with the requested names that exist in the resulting table. Also enable default if it exists and no_default is false. Setting no_default only skips that automatic request: default may still be requested explicitly or enabled by another feature. Ignore unknown requested names.

Expand active features

Each entry has one of four forms:

Entry Effect Output marker
name Enable this package's named feature if it exists name
dep:pkg Enable the optional dependency dep:pkg
pkg/feat Enable the dependency feature; also enable pkg if optional pkg/feat, plus dep:pkg if optional
pkg?/feat Enable the dependency feature only if dep:pkg becomes active pkg/feat when enabled

The weak form, pkg?/feat, never enables the dependency itself. Inputs use it only with optional dependencies, and dep:pkg also names an optional dependency. Other slash entries may refer to non-optional dependencies, which are treated as already available and do not get a dep: marker.

Names and entries are well-formed and contain no surrounding whitespace. Plain feature names do not contain : or /. Unknown plain names in entry lists are ignored. Dependency feature markers are terminal: record them, but do not try to expand another package's feature table.

Revisit weak entries when dependencies become active

Suppose serde is optional and the feature table contains:

[features]
default = ["json"]
json = ["dep:serde", "serde?/derive"]
extra = ["serde?/rc"]

With automatic defaults disabled, the results are:

Requested features Sorted result
[] []
["extra"] ["extra"]
["extra", "json"] ["dep:serde", "extra", "json", "serde/derive", "serde/rc"]
["default"] ["default", "dep:serde", "json", "serde/derive"]

extra itself is active even when its weak entry has no effect. If you visit extra before json, you cannot discard serde?/rc permanently: json will later activate serde.

One approach is to expand every active feature repeatedly until the set stops growing. This stable set is called a fixed point. Ordinary feature chains also need to be followed beyond their first link. Cycles such as a -> b -> a must terminate once both names have been seen. Duplicate requests and repeated entries must not duplicate the output.

The starter is missing implicit features and performs only one sweep. Fix both parts. A work queue is another valid approach if it revisits weak entries when their dependencies become active.

Why Cargo features should be additive

Real Cargo combines feature requests for a dependency within a resolution context. One caller cannot subtract a feature enabled by another. Design features to add compatible functionality so combined requests still work.

For optional standard-library support, a common design is a std feature that adds std-dependent APIs. It may be enabled by default. A caller using default-features = false stops requesting those defaults on its own dependency edge; another caller can still enable them. A no_std feature that removes APIs others need makes such combinations harder to support.

This is not an unconditional union across an entire workspace. Resolver 2 separates normal dependencies from build dependencies and proc macros, ignores features for inactive target-specific dependencies, and includes dev-dependency features when building targets that need them. Resolver 3 retains these distinctions and changes how incompatible Rust versions are handled. Package versions and selected build targets also matter.

Implicit feature names can become part of a crate's public API. Removing one can break users who request it. Using dep:name behind an explicit feature lets you choose the feature name you expose; changing a published feature still needs compatibility consideration.

Finally, this exercise intentionally ignores unknown feature names. Cargo validates real manifests and requests and can reject them instead. The exercise's duplicate-entry and explicit-feature rules likewise define this function's input behavior, not a complete Cargo manifest validator.