Skip to content

Commit

Permalink
feat: Implement cargo update --breaking.
Browse files Browse the repository at this point in the history
  • Loading branch information
torhovland committed Jun 6, 2024
1 parent f3659a9 commit 3a6142f
Show file tree
Hide file tree
Showing 11 changed files with 701 additions and 86 deletions.
1 change: 1 addition & 0 deletions crates/cargo-test-support/src/compare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ static E2E_LITERAL_REDACTIONS: &[(&str, &str)] = &[
("[DIRTY]", " Dirty"),
("[LOCKING]", " Locking"),
("[UPDATING]", " Updating"),
("[UPGRADING]", " Upgrading"),
("[ADDING]", " Adding"),
("[REMOVING]", " Removing"),
("[REMOVED]", " Removed"),
Expand Down
31 changes: 29 additions & 2 deletions src/bin/cargo/commands/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ pub fn cli() -> Command {
.value_name("PRECISE")
.requires("package-group"),
)
.arg(
flag(
"breaking",
"Upgrade [SPEC] to latest breaking versions, unless pinned (unstable)",
)
.short('b'),
)
.arg_silent_suggestion()
.arg(
flag("workspace", "Only update the workspace packages")
Expand All @@ -59,7 +66,8 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
gctx.cli_unstable().msrv_policy,
)?;
}
let ws = args.workspace(gctx)?;

let mut ws = args.workspace(gctx)?;

if args.is_present_with_zero_values("package") {
print_available_packages(&ws)?;
Expand All @@ -84,11 +92,30 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
let update_opts = UpdateOptions {
recursive: args.flag("recursive"),
precise: args.get_one::<String>("precise").map(String::as_str),
breaking: args.flag("breaking"),
to_update,
dry_run: args.dry_run(),
workspace: args.flag("workspace"),
gctx,
};
ops::update_lockfile(&ws, &update_opts)?;

if update_opts.breaking {
gctx.cli_unstable()
.fail_if_stable_opt("--breaking", 12425)?;

let upgrades = ops::upgrade_manifests(&mut ws, &update_opts)?;
ops::resolve_ws(&ws, update_opts.dry_run)?;
ops::write_manifest_upgrades(&ws, &update_opts, &upgrades)?;

if update_opts.dry_run {
update_opts
.gctx
.shell()
.warn("aborting update due to dry run")?;
}
} else {
ops::update_lockfile(&ws, &update_opts)?;
}

Ok(())
}
16 changes: 13 additions & 3 deletions src/cargo/core/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,25 @@ impl Summary {
Rc::make_mut(&mut self.inner).checksum = Some(cksum);
}

pub fn map_dependencies<F>(mut self, f: F) -> Summary
pub fn map_dependencies<F>(self, mut f: F) -> Summary
where
F: FnMut(Dependency) -> Dependency,
{
self.try_map_dependencies(|dep| Ok(f(dep))).unwrap()
}

pub fn try_map_dependencies<F>(mut self, f: F) -> CargoResult<Summary>
where
F: FnMut(Dependency) -> CargoResult<Dependency>,
{
{
let slot = &mut Rc::make_mut(&mut self.inner).dependencies;
*slot = mem::take(slot).into_iter().map(f).collect();
*slot = mem::take(slot)
.into_iter()
.map(f)
.collect::<CargoResult<_>>()?;
}
self
Ok(self)
}

pub fn map_source(self, to_replace: SourceId, replace_with: SourceId) -> Summary {
Expand Down
257 changes: 253 additions & 4 deletions src/cargo/ops/cargo_update.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::core::dependency::Dependency;
use crate::core::registry::PackageRegistry;
use crate::core::resolver::features::{CliFeatures, HasDevUnits};
use crate::core::shell::Verbosity;
Expand All @@ -8,17 +9,25 @@ use crate::ops;
use crate::sources::source::QueryKind;
use crate::util::cache_lock::CacheLockMode;
use crate::util::context::GlobalContext;
use crate::util::style;
use crate::util::CargoResult;
use crate::util::toml_mut::dependency::{MaybeWorkspace, Source};
use crate::util::toml_mut::manifest::LocalManifest;
use crate::util::toml_mut::upgrade::upgrade_requirement;
use crate::util::{style, OptVersionReq};
use crate::util::{CargoResult, VersionExt};
use itertools::Itertools;
use semver::{Op, Version, VersionReq};
use std::cmp::Ordering;
use std::collections::{BTreeMap, HashSet};
use tracing::debug;
use std::collections::{BTreeMap, HashMap, HashSet};
use tracing::{debug, trace};

pub type UpgradeMap = HashMap<(String, SourceId), Version>;

pub struct UpdateOptions<'a> {
pub gctx: &'a GlobalContext,
pub to_update: Vec<String>,
pub precise: Option<&'a str>,
pub recursive: bool,
pub breaking: bool,
pub dry_run: bool,
pub workspace: bool,
}
Expand Down Expand Up @@ -207,6 +216,246 @@ pub fn print_lockfile_changes(
}
}

pub fn upgrade_manifests(
ws: &mut Workspace<'_>,
opts: &UpdateOptions<'_>,
) -> CargoResult<UpgradeMap> {
let mut upgrades = HashMap::new();
let mut upgrade_messages = HashSet::new();

// Updates often require a lot of modifications to the registry, so ensure
// that we're synchronized against other Cargos.
let _lock = ws
.gctx()
.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;

let mut registry = PackageRegistry::new(opts.gctx)?;
registry.lock_patches();

for member in ws.members_mut().sorted() {
debug!("upgrading manifest for `{}`", member.name());

*member.manifest_mut().summary_mut() = member
.manifest()
.summary()
.clone()
.try_map_dependencies(|d| {
upgrade_dependency(&mut registry, &mut upgrades, &mut upgrade_messages, opts, d)
})?;
}

Ok(upgrades)
}

fn upgrade_dependency(
registry: &mut PackageRegistry<'_>,
upgrades: &mut UpgradeMap,
upgrade_messages: &mut HashSet<String>,
opts: &UpdateOptions<'_>,
dependency: Dependency,
) -> CargoResult<Dependency> {
let name = dependency.package_name();
let renamed_to = dependency.name_in_toml();

if name != renamed_to {
trace!(
"skipping dependency renamed from `{}` to `{}`",
name,
renamed_to
);
return Ok(dependency);
}

if !opts.to_update.is_empty() && !opts.to_update.contains(&name.to_string()) {
trace!("skipping dependency `{}` not selected for upgrading", name);
return Ok(dependency);
}

if !dependency.source_id().is_registry() {
trace!("skipping non-registry dependency: {}", name);
return Ok(dependency);
}

let version_req = dependency.version_req();

let OptVersionReq::Req(current) = version_req else {
trace!(
"skipping dependency `{}` without a simple version requirement: {}",
name,
version_req
);
return Ok(dependency);
};

let [comparator] = &current.comparators[..] else {
trace!(
"skipping dependency `{}` with multiple version comparators: {:?}",
name,
&current.comparators
);
return Ok(dependency);
};

if comparator.op != Op::Caret {
trace!("skipping non-caret dependency `{}`: {}", name, comparator);
return Ok(dependency);
}

let query =
crate::core::dependency::Dependency::parse(name, None, dependency.source_id().clone())?;

let possibilities = {
loop {
match registry.query_vec(&query, QueryKind::Exact) {
std::task::Poll::Ready(res) => {
break res?;
}
std::task::Poll::Pending => registry.block_until_ready()?,
}
}
};

let latest = if !possibilities.is_empty() {
possibilities
.iter()
.map(|s| s.as_summary())
.map(|s| s.version())
.filter(|v| !v.is_prerelease())
.max()
} else {
None
};

let Some(latest) = latest else {
trace!(
"skipping dependency `{}` without any published versions",
name
);
return Ok(dependency);
};

if current.matches(&latest) {
trace!(
"skipping dependency `{}` without a breaking update available",
name
);
return Ok(dependency);
}

let Some(new_req_string) = upgrade_requirement(&current.to_string(), latest)? else {
trace!(
"skipping dependency `{}` because the version requirement didn't change",
name
);
return Ok(dependency);
};

let upgrade_message = format!("{} {} -> {}", name, current, new_req_string);
trace!(upgrade_message);

if upgrade_messages.insert(upgrade_message.clone()) {
opts.gctx
.shell()
.status_with_color("Upgrading", &upgrade_message, &style::GOOD)?;
}

upgrades.insert((name.to_string(), dependency.source_id()), latest.clone());

let req = OptVersionReq::Req(VersionReq::parse(&latest.to_string())?);
let mut dep = dependency.clone();
dep.set_version_req(req);
Ok(dep)
}

/// Update manifests with upgraded versions, and write to disk. Based on cargo-edit.
/// Returns true if any file has changed.
pub fn write_manifest_upgrades(
ws: &Workspace<'_>,
opts: &UpdateOptions<'_>,
upgrades: &UpgradeMap,
) -> CargoResult<bool> {
if upgrades.is_empty() {
return Ok(false);
}

let mut any_file_has_changed = false;

let manifest_paths = std::iter::once(ws.root_manifest())
.chain(ws.members().map(|member| member.manifest_path()))
.collect::<Vec<_>>();

for manifest_path in manifest_paths {
trace!(
"updating TOML manifest at `{:?}` with upgraded dependencies",
manifest_path
);

let crate_root = manifest_path
.parent()
.expect("manifest path is absolute")
.to_owned();

let mut local_manifest = LocalManifest::try_new(&manifest_path)?;
let mut manifest_has_changed = false;

for dep_table in local_manifest.get_dependency_tables_mut() {
for (mut dep_key, dep_item) in dep_table.iter_mut() {
let dep_key_str = dep_key.get();
let dependency = crate::util::toml_mut::dependency::Dependency::from_toml(
&manifest_path,
dep_key_str,
dep_item,
)?;

let Some(current) = dependency.version() else {
trace!("skipping dependency without a version: {}", dependency.name);
continue;
};

let (MaybeWorkspace::Other(source_id), Some(Source::Registry(source))) =
(dependency.source_id(opts.gctx)?, dependency.source())
else {
trace!("skipping non-registry dependency: {}", dependency.name);
continue;
};

let Some(latest) = upgrades.get(&(dependency.name.to_owned(), source_id)) else {
trace!(
"skipping dependency without an upgrade: {}",
dependency.name
);
continue;
};

let Some(new_req_string) = upgrade_requirement(current, latest)? else {
trace!(
"skipping dependency `{}` because the version requirement didn't change",
dependency.name
);
continue;
};

let mut dep = dependency.clone();
let mut source = source.clone();
source.version = new_req_string;
dep.source = Some(Source::Registry(source));

trace!("upgrading dependency {}", dependency.name);
dep.update_toml(&crate_root, &mut dep_key, dep_item);
manifest_has_changed = true;
any_file_has_changed = true;
}
}

if manifest_has_changed && !opts.dry_run {
debug!("writing upgraded manifest to {}", manifest_path.display());
local_manifest.write()?;
}
}

Ok(any_file_has_changed)
}

fn print_lockfile_generation(
ws: &Workspace<'_>,
resolve: &Resolve,
Expand Down
2 changes: 2 additions & 0 deletions src/cargo/ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pub use self::cargo_uninstall::uninstall;
pub use self::cargo_update::generate_lockfile;
pub use self::cargo_update::print_lockfile_changes;
pub use self::cargo_update::update_lockfile;
pub use self::cargo_update::upgrade_manifests;
pub use self::cargo_update::write_manifest_upgrades;
pub use self::cargo_update::UpdateOptions;
pub use self::fix::{fix, fix_exec_rustc, fix_get_proxy_lock_addr, FixOptions};
pub use self::lockfile::{load_pkg_lockfile, resolve_to_string, write_pkg_lockfile};
Expand Down
Loading

0 comments on commit 3a6142f

Please sign in to comment.