blob: 18bbab12dc13ce452a16b0917c97c30eed3bd772 [file] [log] [blame]
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::fmt::Write;
use crate::core::compiler::rustdoc::RustdocScrapeExamples;
use crate::core::compiler::unit_dependencies::IsArtifact;
use crate::core::compiler::UnitInterner;
use crate::core::compiler::{CompileKind, CompileMode, Unit};
use crate::core::dependency::DepKind;
use crate::core::profiles::{Profiles, UnitFor};
use crate::core::resolver::features::{self, FeaturesFor};
use crate::core::resolver::{HasDevUnits, Resolve};
use crate::core::{FeatureValue, Package, PackageSet, Summary, Target};
use crate::core::{TargetKind, Workspace};
use crate::util::restricted_names::is_glob_pattern;
use crate::util::{closest_msg, CargoResult};
use super::compile_filter::{CompileFilter, FilterRule, LibRule};
use super::packages::build_glob;
/// A proposed target.
///
/// Proposed targets are later filtered into actual `Unit`s based on whether or
/// not the target requires its features to be present.
#[derive(Debug)]
struct Proposal<'a> {
pkg: &'a Package,
target: &'a Target,
/// Indicates whether or not all required features *must* be present. If
/// false, and the features are not available, then it will be silently
/// skipped. Generally, targets specified by name (`--bin foo`) are
/// required, all others can be silently skipped if features are missing.
requires_features: bool,
mode: CompileMode,
}
/// The context needed for generating root units,
/// which are packages the user has requested to compile.
///
/// To generate a full [`UnitGraph`],
/// generally you need to call [`generate_root_units`] first,
/// and then provide the output to [`build_unit_dependencies`].
///
/// [`generate_root_units`]: UnitGenerator::generate_root_units
/// [`build_unit_dependencies`]: crate::core::compiler::unit_dependencies::build_unit_dependencies
/// [`UnitGraph`]: crate::core::compiler::unit_graph::UnitGraph
pub(super) struct UnitGenerator<'a, 'gctx> {
pub ws: &'a Workspace<'gctx>,
pub packages: &'a [&'a Package],
pub filter: &'a CompileFilter,
pub requested_kinds: &'a [CompileKind],
pub explicit_host_kind: CompileKind,
pub mode: CompileMode,
pub resolve: &'a Resolve,
pub workspace_resolve: &'a Option<Resolve>,
pub resolved_features: &'a features::ResolvedFeatures,
pub package_set: &'a PackageSet<'gctx>,
pub profiles: &'a Profiles,
pub interner: &'a UnitInterner,
pub has_dev_units: HasDevUnits,
}
impl<'a> UnitGenerator<'a, '_> {
/// Helper for creating a list of `Unit` structures
fn new_units(
&self,
pkg: &Package,
target: &Target,
initial_target_mode: CompileMode,
) -> Vec<Unit> {
// Custom build units are added in `build_unit_dependencies`.
assert!(!target.is_custom_build());
let target_mode = match initial_target_mode {
CompileMode::Test => {
if target.is_example() && !self.filter.is_specific() && !target.tested() {
// Examples are included as regular binaries to verify
// that they compile.
CompileMode::Build
} else {
CompileMode::Test
}
}
CompileMode::Build => match *target.kind() {
TargetKind::Test => CompileMode::Test,
TargetKind::Bench => CompileMode::Bench,
_ => CompileMode::Build,
},
// `CompileMode::Bench` is only used to inform `filter_default_targets`
// which command is being used (`cargo bench`). Afterwards, tests
// and benches are treated identically. Switching the mode allows
// de-duplication of units that are essentially identical. For
// example, `cargo build --all-targets --release` creates the units
// (lib profile:bench, mode:test) and (lib profile:bench, mode:bench)
// and since these are the same, we want them to be de-duplicated in
// `unit_dependencies`.
CompileMode::Bench => CompileMode::Test,
_ => initial_target_mode,
};
let is_local = pkg.package_id().source_id().is_path();
// No need to worry about build-dependencies, roots are never build dependencies.
let features_for = FeaturesFor::from_for_host(target.proc_macro());
let features = self
.resolved_features
.activated_features(pkg.package_id(), features_for);
// If `--target` has not been specified, then the unit
// graph is built almost like if `--target $HOST` was
// specified. See `rebuild_unit_graph_shared` for more on
// why this is done. However, if the package has its own
// `package.target` key, then this gets used instead of
// `$HOST`
let explicit_kinds = if let Some(k) = pkg.manifest().forced_kind() {
vec![k]
} else {
self.requested_kinds
.iter()
.map(|kind| match kind {
CompileKind::Host => pkg
.manifest()
.default_kind()
.unwrap_or(self.explicit_host_kind),
CompileKind::Target(t) => CompileKind::Target(*t),
})
.collect()
};
explicit_kinds
.into_iter()
.map(move |kind| {
let unit_for = if initial_target_mode.is_any_test() {
// NOTE: the `UnitFor` here is subtle. If you have a profile
// with `panic` set, the `panic` flag is cleared for
// tests/benchmarks and their dependencies. If this
// was `normal`, then the lib would get compiled three
// times (once with panic, once without, and once with
// `--test`).
//
// This would cause a problem for doc tests, which would fail
// because `rustdoc` would attempt to link with both libraries
// at the same time. Also, it's probably not important (or
// even desirable?) for rustdoc to link with a lib with
// `panic` set.
//
// As a consequence, Examples and Binaries get compiled
// without `panic` set. This probably isn't a bad deal.
//
// Forcing the lib to be compiled three times during `cargo
// test` is probably also not desirable.
UnitFor::new_test(self.ws.gctx(), kind)
} else if target.for_host() {
// Proc macro / plugin should not have `panic` set.
UnitFor::new_compiler(kind)
} else {
UnitFor::new_normal(kind)
};
let profile = self.profiles.get_profile(
pkg.package_id(),
self.ws.is_member(pkg),
is_local,
unit_for,
kind,
);
self.interner.intern(
pkg,
target,
profile,
kind.for_target(target),
target_mode,
features.clone(),
/*is_std*/ false,
/*dep_hash*/ 0,
IsArtifact::No,
None,
)
})
.collect()
}
/// Given a list of all targets for a package, filters out only the targets
/// that are automatically included when the user doesn't specify any targets.
fn filter_default_targets<'b>(&self, targets: &'b [Target]) -> Vec<&'b Target> {
match self.mode {
CompileMode::Bench => targets.iter().filter(|t| t.benched()).collect(),
CompileMode::Test => targets
.iter()
.filter(|t| t.tested() || t.is_example())
.collect(),
CompileMode::Build | CompileMode::Check { .. } => targets
.iter()
.filter(|t| t.is_bin() || t.is_lib())
.collect(),
CompileMode::Doc { .. } => {
// `doc` does lib and bins (bin with same name as lib is skipped).
targets
.iter()
.filter(|t| {
t.documented()
&& (!t.is_bin()
|| !targets.iter().any(|l| l.is_lib() && l.name() == t.name()))
})
.collect()
}
CompileMode::Doctest | CompileMode::RunCustomBuild | CompileMode::Docscrape => {
panic!("Invalid mode {:?}", self.mode)
}
}
}
/// Filters the set of all possible targets based on the provided predicate.
fn filter_targets(
&self,
predicate: impl Fn(&Target) -> bool,
requires_features: bool,
mode: CompileMode,
) -> Vec<Proposal<'a>> {
self.packages
.iter()
.flat_map(|pkg| {
pkg.targets()
.iter()
.filter(|t| predicate(t))
.map(|target| Proposal {
pkg,
target,
requires_features,
mode,
})
})
.collect()
}
/// Finds the targets for a specifically named target.
fn find_named_targets(
&self,
target_name: &str,
target_desc: &'static str,
is_expected_kind: fn(&Target) -> bool,
mode: CompileMode,
) -> CargoResult<Vec<Proposal<'a>>> {
let is_glob = is_glob_pattern(target_name);
let proposals = if is_glob {
let pattern = build_glob(target_name)?;
let filter = |t: &Target| is_expected_kind(t) && pattern.matches(t.name());
self.filter_targets(filter, true, mode)
} else {
let filter = |t: &Target| t.name() == target_name && is_expected_kind(t);
self.filter_targets(filter, true, mode)
};
if proposals.is_empty() {
let targets = self
.packages
.iter()
.flat_map(|pkg| {
pkg.targets()
.iter()
.filter(|target| is_expected_kind(target))
})
.collect::<Vec<_>>();
let suggestion = closest_msg(target_name, targets.iter(), |t| t.name());
if !suggestion.is_empty() {
anyhow::bail!(
"no {} target {} `{}`{}",
target_desc,
if is_glob { "matches pattern" } else { "named" },
target_name,
suggestion
);
} else {
let mut msg = String::new();
writeln!(
msg,
"no {} target {} `{}`.",
target_desc,
if is_glob { "matches pattern" } else { "named" },
target_name,
)?;
if !targets.is_empty() {
writeln!(msg, "Available {} targets:", target_desc)?;
for target in targets {
writeln!(msg, " {}", target.name())?;
}
}
anyhow::bail!(msg);
}
}
Ok(proposals)
}
/// Returns a list of proposed targets based on command-line target selection flags.
fn list_rule_targets(
&self,
rule: &FilterRule,
target_desc: &'static str,
is_expected_kind: fn(&Target) -> bool,
mode: CompileMode,
) -> CargoResult<Vec<Proposal<'a>>> {
let mut proposals = Vec::new();
match rule {
FilterRule::All => proposals.extend(self.filter_targets(is_expected_kind, false, mode)),
FilterRule::Just(names) => {
for name in names {
proposals.extend(self.find_named_targets(
name,
target_desc,
is_expected_kind,
mode,
)?);
}
}
}
Ok(proposals)
}
/// Create a list of proposed targets given the context in `UnitGenerator`
fn create_proposals(&self) -> CargoResult<Vec<Proposal<'_>>> {
let mut proposals: Vec<Proposal<'_>> = Vec::new();
match *self.filter {
CompileFilter::Default {
required_features_filterable,
} => {
for pkg in self.packages {
let default = self.filter_default_targets(pkg.targets());
proposals.extend(default.into_iter().map(|target| Proposal {
pkg,
target,
requires_features: !required_features_filterable,
mode: self.mode,
}));
if self.mode == CompileMode::Test {
if let Some(t) = pkg
.targets()
.iter()
.find(|t| t.is_lib() && t.doctested() && t.doctestable())
{
proposals.push(Proposal {
pkg,
target: t,
requires_features: false,
mode: CompileMode::Doctest,
});
}
}
}
}
CompileFilter::Only {
all_targets,
ref lib,
ref bins,
ref examples,
ref tests,
ref benches,
} => {
if *lib != LibRule::False {
let mut libs = Vec::new();
for proposal in self.filter_targets(Target::is_lib, false, self.mode) {
let Proposal { target, pkg, .. } = proposal;
if self.mode.is_doc_test() && !target.doctestable() {
let types = target.rustc_crate_types();
let types_str: Vec<&str> = types.iter().map(|t| t.as_str()).collect();
self.ws.gctx().shell().warn(format!(
"doc tests are not supported for crate type(s) `{}` in package `{}`",
types_str.join(", "),
pkg.name()
))?;
} else {
libs.push(proposal)
}
}
if !all_targets && libs.is_empty() && *lib == LibRule::True {
let names = self
.packages
.iter()
.map(|pkg| pkg.name())
.collect::<Vec<_>>();
if names.len() == 1 {
anyhow::bail!("no library targets found in package `{}`", names[0]);
} else {
anyhow::bail!(
"no library targets found in packages: {}",
names.join(", ")
);
}
}
proposals.extend(libs);
}
// If `--tests` was specified, add all targets that would be
// generated by `cargo test`.
let test_filter = match tests {
FilterRule::All => Target::tested,
FilterRule::Just(_) => Target::is_test,
};
let test_mode = match self.mode {
CompileMode::Build => CompileMode::Test,
CompileMode::Check { .. } => CompileMode::Check { test: true },
_ => self.mode,
};
// If `--benches` was specified, add all targets that would be
// generated by `cargo bench`.
let bench_filter = match benches {
FilterRule::All => Target::benched,
FilterRule::Just(_) => Target::is_bench,
};
let bench_mode = match self.mode {
CompileMode::Build => CompileMode::Bench,
CompileMode::Check { .. } => CompileMode::Check { test: true },
_ => self.mode,
};
proposals.extend(self.list_rule_targets(bins, "bin", Target::is_bin, self.mode)?);
proposals.extend(self.list_rule_targets(
examples,
"example",
Target::is_example,
self.mode,
)?);
proposals.extend(self.list_rule_targets(tests, "test", test_filter, test_mode)?);
proposals.extend(self.list_rule_targets(
benches,
"bench",
bench_filter,
bench_mode,
)?);
}
}
Ok(proposals)
}
/// Proposes targets from which to scrape examples for documentation
fn create_docscrape_proposals(&self, doc_units: &[Unit]) -> CargoResult<Vec<Proposal<'a>>> {
// In general, the goal is to scrape examples from (a) whatever targets
// the user is documenting, and (b) Example targets. However, if the user
// is documenting a library with dev-dependencies, those dev-deps are not
// needed for the library, while dev-deps are needed for the examples.
//
// If scrape-examples caused `cargo doc` to start requiring dev-deps, this
// would be a breaking change to crates whose dev-deps don't compile.
// Therefore we ONLY want to scrape Example targets if either:
// (1) No package has dev-dependencies, so this is a moot issue, OR
// (2) The provided CompileFilter requires dev-dependencies anyway.
//
// The next two variables represent these two conditions.
let no_pkg_has_dev_deps = self.packages.iter().all(|pkg| {
pkg.summary()
.dependencies()
.iter()
.all(|dep| !matches!(dep.kind(), DepKind::Development))
});
let reqs_dev_deps = matches!(self.has_dev_units, HasDevUnits::Yes);
let safe_to_scrape_example_targets = no_pkg_has_dev_deps || reqs_dev_deps;
let pkgs_to_scrape = doc_units
.iter()
.filter(|unit| self.ws.unit_needs_doc_scrape(unit))
.map(|u| &u.pkg)
.collect::<HashSet<_>>();
let skipped_examples = RefCell::new(Vec::new());
let can_scrape = |target: &Target| {
match (target.doc_scrape_examples(), target.is_example()) {
// Targets configured by the user to not be scraped should never be scraped
(RustdocScrapeExamples::Disabled, _) => false,
// Targets configured by the user to be scraped should always be scraped
(RustdocScrapeExamples::Enabled, _) => true,
// Example targets with no configuration should be conditionally scraped if
// it's guaranteed not to break the build
(RustdocScrapeExamples::Unset, true) => {
if !safe_to_scrape_example_targets {
skipped_examples
.borrow_mut()
.push(target.name().to_string());
}
safe_to_scrape_example_targets
}
// All other targets are ignored for now. This may change in the future!
(RustdocScrapeExamples::Unset, false) => false,
}
};
let mut scrape_proposals = self.filter_targets(can_scrape, false, CompileMode::Docscrape);
scrape_proposals.retain(|proposal| pkgs_to_scrape.contains(proposal.pkg));
let skipped_examples = skipped_examples.into_inner();
if !skipped_examples.is_empty() {
let mut shell = self.ws.gctx().shell();
let example_str = skipped_examples.join(", ");
shell.warn(format!(
"\
Rustdoc did not scrape the following examples because they require dev-dependencies: {example_str}
If you want Rustdoc to scrape these examples, then add `doc-scrape-examples = true`
to the [[example]] target configuration of at least one example."
))?;
}
Ok(scrape_proposals)
}
/// Checks if the unit list is empty and the user has passed any combination of
/// --tests, --examples, --benches or --bins, and we didn't match on any targets.
/// We want to emit a warning to make sure the user knows that this run is a no-op,
/// and their code remains unchecked despite cargo not returning any errors
fn unmatched_target_filters(&self, units: &[Unit]) -> CargoResult<()> {
let mut shell = self.ws.gctx().shell();
if let CompileFilter::Only {
all_targets,
lib: _,
ref bins,
ref examples,
ref tests,
ref benches,
} = *self.filter
{
if units.is_empty() {
let mut filters = String::new();
let mut miss_count = 0;
let mut append = |t: &FilterRule, s| {
if let FilterRule::All = *t {
miss_count += 1;
filters.push_str(s);
}
};
if all_targets {
filters.push_str(" `all-targets`");
} else {
append(bins, " `bins`,");
append(tests, " `tests`,");
append(examples, " `examples`,");
append(benches, " `benches`,");
filters.pop();
}
return shell.warn(format!(
"target {}{} specified, but no targets matched; this is a no-op",
if miss_count > 1 { "filters" } else { "filter" },
filters,
));
}
}
Ok(())
}
/// Warns if a target's required-features references a feature that doesn't exist.
///
/// This is a warning because historically this was not validated, and it
/// would cause too much breakage to make it an error.
fn validate_required_features(
&self,
target_name: &str,
required_features: &[String],
summary: &Summary,
) -> CargoResult<()> {
let resolve = match self.workspace_resolve {
None => return Ok(()),
Some(resolve) => resolve,
};
let mut shell = self.ws.gctx().shell();
for feature in required_features {
let fv = FeatureValue::new(feature.into());
match &fv {
FeatureValue::Feature(f) => {
if !summary.features().contains_key(f) {
shell.warn(format!(
"invalid feature `{}` in required-features of target `{}`: \
`{}` is not present in [features] section",
fv, target_name, fv
))?;
}
}
FeatureValue::Dep { .. } => {
anyhow::bail!(
"invalid feature `{}` in required-features of target `{}`: \
`dep:` prefixed feature values are not allowed in required-features",
fv,
target_name
);
}
FeatureValue::DepFeature { weak: true, .. } => {
anyhow::bail!(
"invalid feature `{}` in required-features of target `{}`: \
optional dependency with `?` is not allowed in required-features",
fv,
target_name
);
}
// Handling of dependent_crate/dependent_crate_feature syntax
FeatureValue::DepFeature {
dep_name,
dep_feature,
weak: false,
} => {
match resolve.deps(summary.package_id()).find(|(_dep_id, deps)| {
deps.iter().any(|dep| dep.name_in_toml() == *dep_name)
}) {
Some((dep_id, _deps)) => {
let dep_summary = resolve.summary(dep_id);
if !dep_summary.features().contains_key(dep_feature)
&& !dep_summary.dependencies().iter().any(|dep| {
dep.name_in_toml() == *dep_feature && dep.is_optional()
})
{
shell.warn(format!(
"invalid feature `{}` in required-features of target `{}`: \
feature `{}` does not exist in package `{}`",
fv, target_name, dep_feature, dep_id
))?;
}
}
None => {
shell.warn(format!(
"invalid feature `{}` in required-features of target `{}`: \
dependency `{}` does not exist",
fv, target_name, dep_name
))?;
}
}
}
}
}
Ok(())
}
/// Converts proposals to units based on each target's required features.
fn proposals_to_units(&self, proposals: Vec<Proposal<'_>>) -> CargoResult<Vec<Unit>> {
// Only include targets that are libraries or have all required
// features available.
//
// `features_map` is a map of &Package -> enabled_features
// It is computed by the set of enabled features for the package plus
// every enabled feature of every enabled dependency.
let mut features_map = HashMap::new();
// This needs to be a set to de-duplicate units. Due to the way the
// targets are filtered, it is possible to have duplicate proposals for
// the same thing.
let mut units = HashSet::new();
for Proposal {
pkg,
target,
requires_features,
mode,
} in proposals
{
let unavailable_features = match target.required_features() {
Some(rf) => {
self.validate_required_features(target.name(), rf, pkg.summary())?;
let features = features_map.entry(pkg).or_insert_with(|| {
super::resolve_all_features(
self.resolve,
self.resolved_features,
self.package_set,
pkg.package_id(),
)
});
rf.iter().filter(|f| !features.contains(*f)).collect()
}
None => Vec::new(),
};
if target.is_lib() || unavailable_features.is_empty() {
units.extend(self.new_units(pkg, target, mode));
} else if requires_features {
let required_features = target.required_features().unwrap();
let quoted_required_features: Vec<String> = required_features
.iter()
.map(|s| format!("`{}`", s))
.collect();
anyhow::bail!(
"target `{}` in package `{}` requires the features: {}\n\
Consider enabling them by passing, e.g., `--features=\"{}\"`",
target.name(),
pkg.name(),
quoted_required_features.join(", "),
required_features.join(" ")
);
}
// else, silently skip target.
}
let mut units: Vec<_> = units.into_iter().collect();
self.unmatched_target_filters(&units)?;
// Keep the roots in a consistent order, which helps with checking test output.
units.sort_unstable();
Ok(units)
}
/// Generates all the base units for the packages the user has requested to
/// compile. Dependencies for these units are computed later in [`unit_dependencies`].
///
/// [`unit_dependencies`]: crate::core::compiler::unit_dependencies
pub fn generate_root_units(&self) -> CargoResult<Vec<Unit>> {
let proposals = self.create_proposals()?;
self.proposals_to_units(proposals)
}
/// Generates units specifically for doc-scraping.
///
/// This requires a separate entrypoint from [`generate_root_units`] because it
/// takes the documented units as input.
///
/// [`generate_root_units`]: Self::generate_root_units
pub fn generate_scrape_units(&self, doc_units: &[Unit]) -> CargoResult<Vec<Unit>> {
let scrape_proposals = self.create_docscrape_proposals(&doc_units)?;
let scrape_units = self.proposals_to_units(scrape_proposals)?;
Ok(scrape_units)
}
}