blob: c115291cb7ab29c21e19e64b71895abb3a275bd6 [file] [log] [blame]
use cargo::core::dependency::DepKind;
use cargo::core::PackageIdSpec;
use cargo::core::Resolve;
use cargo::core::Workspace;
use cargo::ops::cargo_remove::remove;
use cargo::ops::cargo_remove::RemoveOptions;
use cargo::ops::resolve_ws;
use cargo::util::command_prelude::*;
use cargo::util::print_available_packages;
use cargo::util::toml_mut::dependency::Dependency;
use cargo::util::toml_mut::dependency::MaybeWorkspace;
use cargo::util::toml_mut::dependency::Source;
use cargo::util::toml_mut::manifest::DepTable;
use cargo::util::toml_mut::manifest::LocalManifest;
use cargo::CargoResult;
pub fn cli() -> clap::Command {
clap::Command::new("remove")
// Subcommand aliases are handled in `aliased_command()`.
// .alias("rm")
.about("Remove dependencies from a Cargo.toml manifest file")
.args([clap::Arg::new("dependencies")
.action(clap::ArgAction::Append)
.required(true)
.num_args(1..)
.value_name("DEP_ID")
.help("Dependencies to be removed")])
.arg_dry_run("Don't actually write the manifest")
.arg_quiet()
.next_help_heading("Section")
.args([
clap::Arg::new("dev")
.long("dev")
.conflicts_with("build")
.action(clap::ArgAction::SetTrue)
.group("section")
.help("Remove from dev-dependencies"),
clap::Arg::new("build")
.long("build")
.conflicts_with("dev")
.action(clap::ArgAction::SetTrue)
.group("section")
.help("Remove from build-dependencies"),
clap::Arg::new("target")
.long("target")
.num_args(1)
.value_name("TARGET")
.value_parser(clap::builder::NonEmptyStringValueParser::new())
.help("Remove from target-dependencies"),
])
.arg_package("Package to remove from")
.arg_manifest_path()
.after_help(color_print::cstr!(
"Run `<cyan,bold>cargo help remove</>` for more detailed information.\n"
))
}
pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult {
let dry_run = args.dry_run();
let workspace = args.workspace(config)?;
if args.is_present_with_zero_values("package") {
print_available_packages(&workspace)?;
}
let packages = args.packages_from_flags()?;
let packages = packages.get_packages(&workspace)?;
let spec = match packages.len() {
0 => {
return Err(CliError::new(
anyhow::format_err!(
"no packages selected to modify. Please specify one with `-p <PKGID>`"
),
101,
));
}
1 => packages[0],
_ => {
let names = packages.iter().map(|p| p.name()).collect::<Vec<_>>();
return Err(CliError::new(
anyhow::format_err!(
"`cargo remove` could not determine which package to modify. \
Use the `--package` option to specify a package. \n\
available packages: {}",
names.join(", ")
),
101,
));
}
};
let dependencies = args
.get_many::<String>("dependencies")
.expect("required(true)")
.cloned()
.collect::<Vec<_>>();
let section = parse_section(args);
let options = RemoveOptions {
config,
spec,
dependencies,
section,
dry_run,
};
remove(&options)?;
if !dry_run {
// Clean up the workspace
gc_workspace(&workspace)?;
// Reload the workspace since we've changed dependencies
let ws = args.workspace(config)?;
let resolve = {
// HACK: Avoid unused patch warnings by temporarily changing the verbosity.
// In rare cases, this might cause index update messages to not show up
let verbosity = ws.config().shell().verbosity();
ws.config()
.shell()
.set_verbosity(cargo::core::Verbosity::Quiet);
let resolve = resolve_ws(&ws);
ws.config().shell().set_verbosity(verbosity);
resolve?.1
};
// Attempt to gc unused patches and re-resolve if anything is removed
if gc_unused_patches(&workspace, &resolve)? {
let ws = args.workspace(config)?;
resolve_ws(&ws)?;
}
}
Ok(())
}
fn parse_section(args: &ArgMatches) -> DepTable {
let dev = args.flag("dev");
let build = args.flag("build");
let kind = if dev {
DepKind::Development
} else if build {
DepKind::Build
} else {
DepKind::Normal
};
let mut table = DepTable::new().set_kind(kind);
if let Some(target) = args.get_one::<String>("target") {
assert!(!target.is_empty(), "Target specification may not be empty");
table = table.set_target(target);
}
table
}
/// Clean up the workspace.dependencies, profile, patch, and replace sections of the root manifest
/// by removing dependencies which no longer have a reference to them.
fn gc_workspace(workspace: &Workspace<'_>) -> CargoResult<()> {
let mut manifest: toml_edit::Document =
cargo_util::paths::read(workspace.root_manifest())?.parse()?;
let mut is_modified = true;
let members = workspace
.members()
.map(|p| LocalManifest::try_new(p.manifest_path()))
.collect::<CargoResult<Vec<_>>>()?;
let mut dependencies = members
.iter()
.flat_map(|manifest| {
manifest.get_sections().into_iter().flat_map(|(_, table)| {
table
.as_table_like()
.unwrap()
.iter()
.map(|(key, item)| Dependency::from_toml(&manifest.path, key, item))
.collect::<Vec<_>>()
})
})
.collect::<CargoResult<Vec<_>>>()?;
// Clean up the workspace.dependencies section and replace instances of
// workspace dependencies with their definitions
if let Some(toml_edit::Item::Table(deps_table)) = manifest
.get_mut("workspace")
.and_then(|t| t.get_mut("dependencies"))
{
deps_table.set_implicit(true);
for (key, item) in deps_table.iter_mut() {
let ws_dep = Dependency::from_toml(&workspace.root(), key.get(), item)?;
// search for uses of this workspace dependency
let mut is_used = false;
for dep in dependencies.iter_mut().filter(|d| {
d.toml_key() == key.get() && matches!(d.source(), Some(Source::Workspace(_)))
}) {
// HACK: Replace workspace references in `dependencies` to simplify later GC steps:
// 1. Avoid having to look it up again to determine the dependency source / spec
// 2. The entry might get deleted, preventing us from looking it up again
//
// This does lose extra information, like features enabled, but that shouldn't be a
// problem for GC
*dep = ws_dep.clone();
is_used = true;
}
if !is_used {
*item = toml_edit::Item::None;
is_modified = true;
}
}
}
// Clean up the profile section
//
// Example tables:
// - profile.dev.package.foo
// - profile.release.package."foo:2.1.0"
if let Some(toml_edit::Item::Table(profile_section_table)) = manifest.get_mut("profile") {
profile_section_table.set_implicit(true);
for (_, item) in profile_section_table.iter_mut() {
if let toml_edit::Item::Table(profile_table) = item {
profile_table.set_implicit(true);
if let Some(toml_edit::Item::Table(package_table)) =
profile_table.get_mut("package")
{
package_table.set_implicit(true);
for (key, item) in package_table.iter_mut() {
let key = key.get();
// Skip globs. Can't do anything with them.
// For example, profile.release.package."*".
if crate::util::restricted_names::is_glob_pattern(key) {
continue;
}
if !spec_has_match(
&PackageIdSpec::parse(key)?,
&dependencies,
workspace.config(),
)? {
*item = toml_edit::Item::None;
is_modified = true;
}
}
}
}
}
}
// Clean up the replace section
if let Some(toml_edit::Item::Table(table)) = manifest.get_mut("replace") {
table.set_implicit(true);
for (key, item) in table.iter_mut() {
if !spec_has_match(
&PackageIdSpec::parse(key.get())?,
&dependencies,
workspace.config(),
)? {
*item = toml_edit::Item::None;
is_modified = true;
}
}
}
if is_modified {
cargo_util::paths::write_atomic(
workspace.root_manifest(),
manifest.to_string().as_bytes(),
)?;
}
Ok(())
}
/// Check whether or not a package ID spec matches any non-workspace dependencies.
fn spec_has_match(
spec: &PackageIdSpec,
dependencies: &[Dependency],
config: &Config,
) -> CargoResult<bool> {
for dep in dependencies {
if spec.name() != &dep.name {
continue;
}
let version_matches = match (spec.version(), dep.version()) {
(Some(v), Some(vq)) => semver::VersionReq::parse(vq)?.matches(&v),
(Some(_), None) => false,
(None, None | Some(_)) => true,
};
if !version_matches {
continue;
}
match dep.source_id(config)? {
MaybeWorkspace::Other(source_id) => {
if spec.url().map(|u| u == source_id.url()).unwrap_or(true) {
return Ok(true);
}
}
MaybeWorkspace::Workspace(_) => {}
}
}
Ok(false)
}
/// Removes unused patches from the manifest
fn gc_unused_patches(workspace: &Workspace<'_>, resolve: &Resolve) -> CargoResult<bool> {
let mut manifest: toml_edit::Document =
cargo_util::paths::read(workspace.root_manifest())?.parse()?;
let mut modified = false;
// Clean up the patch section
if let Some(toml_edit::Item::Table(patch_section_table)) = manifest.get_mut("patch") {
patch_section_table.set_implicit(true);
for (_, item) in patch_section_table.iter_mut() {
if let toml_edit::Item::Table(patch_table) = item {
patch_table.set_implicit(true);
for (key, item) in patch_table.iter_mut() {
let dep = Dependency::from_toml(&workspace.root_manifest(), key.get(), item)?;
// Generate a PackageIdSpec url for querying
let url = if let MaybeWorkspace::Other(source_id) =
dep.source_id(workspace.config())?
{
format!("{}#{}", source_id.url(), dep.name)
} else {
continue;
};
if PackageIdSpec::query_str(&url, resolve.unused_patches().iter().cloned())
.is_ok()
{
*item = toml_edit::Item::None;
modified = true;
}
}
}
}
}
if modified {
cargo_util::paths::write(workspace.root_manifest(), manifest.to_string().as_bytes())?;
}
Ok(modified)
}