use crate::compiler::{CompileKind, RustcTargetData};
use crate::context::FeatureUnification;
use crate::ops;
use crate::resolver::PublishAgePolicy;
use crate::resolver::features::{
CliFeatures, FeatureOpts, FeatureResolver, ForceAllTargets, RequestedFeatures, ResolvedFeatures,
};
use crate::resolver::{
self, HasDevUnits, Resolve, ResolveOpts, ResolveVersion, VersionOrdering, VersionPreferences,
};
use crate::sources::RecursivePathSource;
use crate::util::CanonicalUrl;
use crate::util::cache_lock::CacheLockMode;
use crate::util::data_structures::{HashMap, HashSet};
use crate::util::errors::CargoResult;
use crate::workspace::Dependency;
use crate::workspace::GitReference;
use crate::workspace::PackageId;
use crate::workspace::PackageIdSpec;
use crate::workspace::PackageIdSpecQuery;
use crate::workspace::PackageSet;
use crate::workspace::SourceId;
use crate::workspace::Workspace;
use crate::workspace::registry::{LockedPatchDependency, PackageRegistry};
use crate::workspace::summary::Summary;
use anyhow::Context as _;
use cargo_util::paths;
use cargo_util_schemas::core::PartialVersion;
use cargo_util_terminal::report::Group;
use cargo_util_terminal::report::Level;
use std::borrow::Cow;
use std::rc::Rc;
use tracing::{debug, trace};
type Keep<'a> = &'a dyn Fn(&PackageId) -> bool;
pub struct WorkspaceResolve<'gctx> {
pub pkg_set: PackageSet<'gctx>,
pub workspace_resolve: Option<Resolve>,
pub targeted_resolve: Resolve,
pub specs_and_features: Vec<SpecsAndResolvedFeatures>,
}
pub struct SpecsAndResolvedFeatures {
pub specs: Vec<PackageIdSpec>,
pub resolved_features: ResolvedFeatures,
}
const UNUSED_PATCH_WARNING: &str = "\
Check that the patched package version and available features are compatible
with the dependency requirements. If the patch has a different version from
what is locked in the Cargo.lock file, run `cargo update` to use the new
version. This may also occur with an optional dependency that is not enabled.";
pub fn resolve_ws<'a>(ws: &Workspace<'a>, dry_run: bool) -> CargoResult<(PackageSet<'a>, Resolve)> {
let mut registry = ws.package_registry()?;
let resolve = resolve_with_registry(ws, &mut registry, dry_run)?;
let packages = get_resolved_packages(&resolve, registry)?;
Ok((packages, resolve))
}
pub fn resolve_ws_with_opts<'gctx>(
ws: &Workspace<'gctx>,
target_data: &mut RustcTargetData<'gctx>,
requested_targets: &[CompileKind],
cli_features: &CliFeatures,
specs: &[PackageIdSpec],
has_dev_units: HasDevUnits,
force_all_targets: ForceAllTargets,
dry_run: bool,
) -> CargoResult<WorkspaceResolve<'gctx>> {
let feature_unification = ws.resolve_feature_unification();
let individual_specs = match feature_unification {
FeatureUnification::Selected => vec![specs.to_owned()],
FeatureUnification::Workspace => {
vec![ops::Packages::All(Vec::new()).to_package_id_specs(ws)?]
}
FeatureUnification::Package => specs.iter().map(|spec| vec![spec.clone()]).collect(),
};
let specs: Vec<_> = individual_specs
.iter()
.map(|specs| specs.iter())
.flatten()
.cloned()
.collect();
let specs = &specs[..];
let mut registry = ws.package_registry()?;
let (resolve, resolved_with_overrides) = if ws.ignore_lock() {
let add_patches = true;
let resolve = None;
let resolved_with_overrides = resolve_with_previous(
&mut registry,
ws,
cli_features,
has_dev_units,
resolve.as_ref(),
None,
specs,
add_patches,
)?;
ops::print_lockfile_changes(ws, None, &resolved_with_overrides, &mut registry)?;
(resolve, resolved_with_overrides)
} else if ws.require_optional_deps() {
let resolve = resolve_with_registry(ws, &mut registry, dry_run)?;
let add_patches = false;
add_overrides(&mut registry, ws)?;
for (replace_spec, dep) in ws.root_replace() {
if !resolve
.iter()
.any(|r| replace_spec.matches(r) && !dep.matches_id(r))
{
ws.gctx()
.shell()
.warn(format!("package replacement is not used: {}", replace_spec))?
}
let mut unused_fields = Vec::new();
if dep.features().len() != 0 {
unused_fields.push("`features`");
}
if !dep.uses_default_features() {
unused_fields.push("`default-features`")
}
if !unused_fields.is_empty() {
ws.gctx().shell().print_report(
&[Level::WARNING
.secondary_title(format!(
"unused field in replacement for `{}`: {}",
dep.package_name(),
unused_fields.join(", ")
))
.element(Level::NOTE.message(format!(
"configure {} in the `dependencies` entry",
unused_fields.join(", ")
)))],
false,
)?;
}
}
let resolved_with_overrides = resolve_with_previous(
&mut registry,
ws,
cli_features,
has_dev_units,
Some(&resolve),
None,
specs,
add_patches,
)?;
(Some(resolve), resolved_with_overrides)
} else {
let add_patches = true;
let resolve = ops::load_pkg_lockfile(ws)?;
let resolved_with_overrides = resolve_with_previous(
&mut registry,
ws,
cli_features,
has_dev_units,
resolve.as_ref(),
None,
specs,
add_patches,
)?;
(resolve, resolved_with_overrides)
};
let pkg_set = get_resolved_packages(&resolved_with_overrides, registry)?;
let members_with_features = ws.members_with_features(specs, cli_features)?;
let member_ids = members_with_features
.iter()
.map(|(p, _fts)| p.package_id())
.collect::<Vec<_>>();
for pkg_id in resolved_with_overrides.iter() {
for kind in resolved_with_overrides
.bindeps(pkg_id)
.filter_map(|(_dep_id, dep)| dep.artifact()?.target()?.to_compile_kind())
{
let _ = target_data.merge_compile_kind(kind);
}
}
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)?;
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,
)?;
pkg_set.warn_no_lib_packages_and_artifact_libs_overlapping_deps(
ws,
&resolved_with_overrides,
&member_ids,
has_dev_units,
requested_targets,
target_data,
force_all_targets,
)?;
specs_and_features.push(SpecsAndResolvedFeatures {
specs,
resolved_features,
});
}
Ok(WorkspaceResolve {
pkg_set,
workspace_resolve: resolve,
targeted_resolve: resolved_with_overrides,
specs_and_features,
})
}
#[tracing::instrument(skip_all)]
fn resolve_with_registry<'gctx>(
ws: &Workspace<'gctx>,
registry: &mut PackageRegistry<'gctx>,
dry_run: bool,
) -> CargoResult<Resolve> {
let prev = ops::load_pkg_lockfile(ws)?;
let mut resolve = resolve_with_previous(
registry,
ws,
&CliFeatures::new_all(true),
HasDevUnits::Yes,
prev.as_ref(),
None,
&[],
true,
)?;
let print = if !ws.is_ephemeral() && ws.require_optional_deps() {
if !dry_run {
ops::write_pkg_lockfile(ws, &mut resolve)?
} else {
true
}
} else {
false
};
if print {
ops::print_lockfile_changes(ws, prev.as_ref(), &resolve, registry)?;
}
Ok(resolve)
}
#[tracing::instrument(skip_all)]
pub fn resolve_with_previous<'gctx>(
registry: &mut PackageRegistry<'gctx>,
ws: &Workspace<'gctx>,
cli_features: &CliFeatures,
has_dev_units: HasDevUnits,
previous: Option<&Resolve>,
keep_previous: Option<Keep<'_>>,
specs: &[PackageIdSpec],
register_patches: bool,
) -> CargoResult<Resolve> {
let _lock = ws
.gctx()
.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
ws.preload(registry);
for member in ws.members() {
registry.add_sources(Some(member.package_id().source_id()))?;
}
let keep_previous = keep_previous.unwrap_or(&|_| true);
let mut version_prefs = VersionPreferences::default();
if ws.gctx().cli_unstable().minimal_versions {
version_prefs.version_ordering(VersionOrdering::MinimumVersionsFirst)
}
if ws.resolve_honors_rust_version() {
let mut rust_versions: Vec<_> = ws
.members()
.filter_map(|p| p.rust_version().map(|rv| rv.to_partial()))
.collect();
if rust_versions.is_empty() {
let rustc = ws.gctx().load_global_rustc(Some(ws))?;
let rust_version: PartialVersion = rustc.version.clone().into();
rust_versions.push(rust_version);
}
version_prefs.rust_versions(rust_versions);
}
if let Some(publish_time) = ws.resolve_publish_time() {
version_prefs.publish_time(publish_time);
}
if ws.resolve_honors_publish_age() {
if let Some(policy) = PublishAgePolicy::new(ws.resolve_publish_time(), ws.gctx())? {
version_prefs.publish_age(policy);
}
}
let avoid_patch_ids = if register_patches {
register_patch_entries(registry, ws, previous, &mut version_prefs, keep_previous)?
} else {
HashSet::default()
};
let keep = |p: &PackageId| keep_previous(p) && !avoid_patch_ids.contains(p);
let dev_deps = ws.require_optional_deps() || has_dev_units == HasDevUnits::Yes;
if let Some(r) = previous {
trace!("previous: {:?}", r);
register_previous_locks(ws, registry, r, &keep, dev_deps);
let _span = tracing::span!(tracing::Level::TRACE, "prefer_package_id").entered();
for id in r.iter().filter(keep) {
debug!("attempting to prefer {}", id);
version_prefs.prefer_package_id(id);
}
}
if register_patches {
registry.lock_patches();
}
let summaries: Vec<(Summary, ResolveOpts)> = {
let _span = tracing::span!(tracing::Level::TRACE, "registry.lock").entered();
ws.members_with_features(specs, cli_features)?
.into_iter()
.map(|(member, features)| {
let summary = registry.lock(member.summary().clone());
(
summary,
ResolveOpts {
dev_deps,
features: RequestedFeatures::CliFeatures(features),
},
)
})
.collect()
};
let replace = lock_replacements(ws, previous, &keep);
let mut resolved = resolver::resolve(
&summaries,
&replace,
registry,
&version_prefs,
ResolveVersion::with_rust_version(ws.lowest_rust_version()),
ws.gctx(),
)?;
let patches = registry.patches().values().flat_map(|v| v.iter());
resolved.register_used_patches(patches);
if register_patches && !resolved.unused_patches().is_empty() {
emit_warnings_of_unused_patches(ws, &resolved, registry)?;
}
if let Some(previous) = previous {
resolved.merge_from(previous)?;
}
let gctx = ws.gctx();
let mut deferred = gctx.deferred_global_last_use()?;
deferred.save_no_error(gctx);
Ok(resolved)
}
#[tracing::instrument(skip_all)]
pub fn add_overrides<'a>(
registry: &mut PackageRegistry<'a>,
ws: &Workspace<'a>,
) -> CargoResult<()> {
let gctx = ws.gctx();
let Some(paths) = gctx.paths_overrides()? else {
return Ok(());
};
let paths = paths.val.iter().map(|(s, def)| {
(paths::normalize_path(&def.root(gctx.cwd()).join(s)), def)
});
for (path, definition) in paths {
let id = SourceId::for_path(&path)?;
let source = RecursivePathSource::new(&path, id, ws.gctx());
source.load().with_context(|| {
format!(
"failed to update path override `{}` \
(defined in `{}`)",
path.display(),
definition
)
})?;
registry.add_override(Box::new(source));
}
Ok(())
}
pub fn get_resolved_packages<'gctx>(
resolve: &Resolve,
registry: PackageRegistry<'gctx>,
) -> CargoResult<PackageSet<'gctx>> {
let ids: Vec<PackageId> = resolve.iter().collect();
registry.get(&ids)
}
#[tracing::instrument(skip_all)]
fn register_previous_locks(
ws: &Workspace<'_>,
registry: &mut PackageRegistry<'_>,
resolve: &Resolve,
keep: Keep<'_>,
dev_deps: bool,
) {
let path_pkg = |id: SourceId| {
if !id.is_path() {
return None;
}
if let Ok(path) = id.url().to_file_path() {
if let Ok(pkg) = ws.load(&path.join("Cargo.toml")) {
return Some(pkg);
}
}
None
};
let mut avoid_locking = HashSet::default();
for node in resolve.iter() {
if !keep(&node) {
add_deps(resolve, node, &mut avoid_locking);
}
}
{
let _span = tracing::span!(tracing::Level::TRACE, "poison").entered();
let mut path_deps = ws.members().cloned().collect::<Vec<_>>();
let mut visited = HashSet::default();
while let Some(member) = path_deps.pop() {
if !visited.insert(member.package_id()) {
continue;
}
let is_ws_member = ws.is_member(&member);
for dep in member.dependencies() {
if !is_ws_member && (dep.is_optional() || !dep.is_transitive()) {
continue;
}
if !dep.is_transitive() && !dev_deps {
continue;
}
if let Some(pkg) = path_pkg(dep.source_id()) {
path_deps.push(pkg);
continue;
}
if resolve.iter().any(|id| dep.matches_ignoring_source(id)) {
continue;
}
debug!(
"poisoning {} because {} looks like it changed {}",
dep.source_id(),
member.package_id(),
dep.package_name()
);
for id in resolve
.iter()
.filter(|id| id.source_id() == dep.source_id())
{
add_deps(resolve, id, &mut avoid_locking);
}
}
}
}
for node in resolve.iter() {
if let Some(pkg) = path_pkg(node.source_id()) {
if pkg.package_id() != node {
avoid_locking.insert(node);
}
}
}
let keep = |id: &PackageId| keep(id) && !avoid_locking.contains(id);
registry.clear_lock();
{
let _span = tracing::span!(tracing::Level::TRACE, "register_lock").entered();
for node in resolve.iter().filter(keep) {
let deps = resolve
.deps_not_replaced(node)
.map(|p| p.0)
.filter(keep)
.collect::<Vec<_>>();
if let Some(node) = master_branch_git_source(node, resolve) {
registry.register_lock(node, deps.clone());
}
registry.register_lock(node, deps);
}
}
fn add_deps(resolve: &Resolve, node: PackageId, set: &mut HashSet<PackageId>) {
if !set.insert(node) {
return;
}
debug!("ignoring any lock pointing directly at {}", node);
for (dep, _) in resolve.deps_not_replaced(node) {
add_deps(resolve, dep, set);
}
}
}
fn master_branch_git_source(id: PackageId, resolve: &Resolve) -> Option<PackageId> {
if resolve.version() <= ResolveVersion::V2 {
let source = id.source_id();
if let Some(GitReference::DefaultBranch) = source.git_reference() {
let new_source =
SourceId::for_git(source.url(), GitReference::Branch("master".to_string()))
.unwrap()
.with_precise_from(source);
return Some(id.with_source_id(new_source));
}
}
None
}
fn emit_warnings_of_unused_patches(
ws: &Workspace<'_>,
resolve: &Resolve,
registry: &PackageRegistry<'_>,
) -> CargoResult<()> {
const MESSAGE: &str = "was not used in the crate graph";
let mut patch_pkgid_to_urls = HashMap::default();
for (url, summaries) in registry.patches().iter() {
for summary in summaries.iter() {
patch_pkgid_to_urls
.entry(summary.package_id())
.or_insert_with(HashSet::default)
.insert(url);
}
}
let mut source_ids_grouped_by_pkg_name = HashMap::default();
for pkgid in resolve.iter() {
source_ids_grouped_by_pkg_name
.entry(pkgid.name())
.or_insert_with(HashSet::default)
.insert(pkgid.source_id());
}
let mut unemitted_unused_patches = Vec::new();
for unused in resolve.unused_patches().iter() {
match (
source_ids_grouped_by_pkg_name.get(&unused.name()),
patch_pkgid_to_urls.get(unused),
) {
(Some(ids), Some(patched_urls))
if ids
.iter()
.all(|id| !patched_urls.contains(id.canonical_url())) =>
{
let mut help = "perhaps you meant one of the following:".to_owned();
for id in ids {
help.push_str("\n\t");
help.push_str(&id.display_registry_name());
}
ws.gctx().shell().print_report(
&[Level::WARNING
.secondary_title(format!("patch `{unused}` {MESSAGE}"))
.element(Level::HELP.message(help))],
false,
)?;
}
_ => unemitted_unused_patches.push(unused),
}
}
if !unemitted_unused_patches.is_empty() {
let mut warnings: Vec<_> = unemitted_unused_patches
.iter()
.map(|pkgid| {
Group::with_title(
Level::WARNING.secondary_title(format!("patch `{pkgid}` {MESSAGE}")),
)
})
.collect();
warnings.push(Group::with_title(
Level::HELP.secondary_title(UNUSED_PATCH_WARNING),
));
ws.gctx().shell().print_report(&warnings, false)?;
}
Ok(())
}
#[tracing::instrument(level = "debug", skip_all, ret)]
fn register_patch_entries(
registry: &mut PackageRegistry<'_>,
ws: &Workspace<'_>,
previous: Option<&Resolve>,
version_prefs: &mut VersionPreferences,
keep_previous: Keep<'_>,
) -> CargoResult<HashSet<PackageId>> {
let mut avoid_patch_ids = HashSet::default();
for (url, patches) in ws.root_patch()?.iter() {
for patch in patches {
version_prefs.prefer_dependency(patch.dep.clone());
}
let Some(previous) = previous else {
let patches: Vec<_> = patches.iter().map(|p| (p, None)).collect();
let unlock_ids = registry.patch(url, &patches)?;
assert!(unlock_ids.is_empty());
continue;
};
let mut registrations = Vec::new();
for patch in patches {
let dep = &patch.dep;
let candidates = || {
previous
.iter()
.chain(previous.unused_patches().iter().cloned())
.filter(&keep_previous)
};
let lock = match candidates().find(|id| dep.matches_id(*id)) {
Some(package_id) => {
let mut locked_dep = dep.clone();
locked_dep.lock_to(package_id);
Some(LockedPatchDependency {
dependency: locked_dep,
package_id,
alt_package_id: None,
})
}
None => {
match candidates().find(|&id| match master_branch_git_source(id, previous) {
Some(id) => dep.matches_id(id),
None => false,
}) {
Some(id_using_default) => {
let id_using_master = id_using_default.with_source_id(
dep.source_id()
.with_precise_from(id_using_default.source_id()),
);
let mut locked_dep = dep.clone();
locked_dep.lock_to(id_using_master);
Some(LockedPatchDependency {
dependency: locked_dep,
package_id: id_using_master,
alt_package_id: Some(id_using_default),
})
}
None => None,
}
}
};
registrations.push((patch, lock));
}
let canonical = CanonicalUrl::new(url)?;
for (orig_patch, unlock_id) in registry.patch(url, ®istrations)? {
avoid_patch_ids.insert(unlock_id);
avoid_patch_ids.extend(previous.iter().filter(|id| {
orig_patch.dep.matches_ignoring_source(*id)
&& *id.source_id().canonical_url() == canonical
}));
}
}
Ok(avoid_patch_ids)
}
fn lock_replacements(
ws: &Workspace<'_>,
previous: Option<&Resolve>,
keep: Keep<'_>,
) -> Vec<(PackageIdSpec, Dependency)> {
let root_replace = ws.root_replace();
let replace = match previous {
Some(r) => root_replace
.iter()
.map(|(spec, dep)| {
for (&key, &val) in r.replacements().iter() {
if spec.matches(key) && dep.matches_id(val) && keep(&val) {
let mut dep = dep.clone();
dep.lock_to(val);
return (spec.clone(), dep);
}
}
(spec.clone(), dep.clone())
})
.collect::<Vec<_>>(),
None => root_replace.to_vec(),
};
replace
}