Skip to content

Don't require downloading and parsing all packages to resolve features #15834

@epage

Description

@epage

Problem

Cargo's resolver has 3 phases, narrowing down the packages used at each step

  • Resolve for lockfile
  • Resolve for target and some other settings
  • Resolve features

Resolving of features requires downloading and parsing all packages from step (2)

pkg_set.download_accessible(
&resolved_with_overrides,
&member_ids,
has_dev_units,
requested_targets,
target_data,
force_all_targets,
)?;
let mut specs_and_features = Vec::new();
for specs in individual_specs {
let feature_opts = FeatureOpts::new(ws, has_dev_units, force_all_targets)?;
// We want to narrow the features to the current specs so that stuff like `cargo check -p a
// -p b -F a/a,b/b` works and the resolver does not contain that `a` does not have feature
// `b` and vice-versa. However, resolver v1 needs to see even features of unselected
// packages turned on if it was because of working directory being inside the unselected
// package, because they might turn on a feature of a selected package.
let narrowed_features = match feature_unification {
FeatureUnification::Package => {
let mut narrowed_features = cli_features.clone();
let enabled_features = members_with_features
.iter()
.filter_map(|(package, cli_features)| {
specs
.iter()
.any(|spec| spec.matches(package.package_id()))
.then_some(cli_features.features.iter())
})
.flatten()
.cloned()
.collect();
narrowed_features.features = Rc::new(enabled_features);
Cow::Owned(narrowed_features)
}
FeatureUnification::Selected | FeatureUnification::Workspace => {
Cow::Borrowed(cli_features)
}
};
let resolved_features = FeatureResolver::resolve(
ws,
target_data,
&resolved_with_overrides,
&pkg_set,
&*narrowed_features,
&specs,
requested_targets,
feature_opts,
)?;

so we can then know what proc macros there are
/// Whether the given package has any proc macro target, including proc-macro examples.
fn has_any_proc_macro(&self, package_id: PackageId) -> bool {
self.package_set
.get_one(package_id)
.expect("packages downloaded")
.proc_macro()
}
/// Whether the given package is a proc macro lib target.
///
/// This is useful for checking if a dependency is a proc macro,
/// as it is not possible to depend on a non-lib target as a proc-macro.
fn has_proc_macro_lib(&self, package_id: PackageId) -> bool {
self.package_set
.get_one(package_id)
.expect("packages downloaded")
.library()
.map(|lib| lib.proc_macro())
.unwrap_or_default()
}

so we can know whether a package and its features are being resolved for the host-platform or target-platform

Without this, resolving of features could operate on the Index and we could download packages after step 3, reducing how many we download.

Problems with this:

  • Network, file IO, and CPU performance in downloading and parsing more manifests than needed
  • This makes a plumbing command for a feature resolver take in a lot more input than it would otherwise need, slowing down the plumbing command operations
  • The plumbing commands are also providing light on how ways we might be able to better isolate cargo's architecture for easier maintainability and contributor approachability

Proposed Solution

  • Add proc macro information to the Index Summary
  • Have crates.io backfill this Summary entry

Open question

  • How do we deal with non-backfilled data, like third-party registries

Notes

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-featuresArea: features — conditional compilationC-feature-requestCategory: proposal for a feature. Before PR, ping rust-lang/cargo if this is not `Feature accepted`PerformanceGotta go fast!S-needs-designStatus: Needs someone to work further on the design for the feature or fix. NOT YET accepted.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions