Skip to content

Commit 61202d5

Browse files
committed
feat: Implement cargo update --breaking.
1 parent 76d5567 commit 61202d5

File tree

11 files changed

+683
-88
lines changed

11 files changed

+683
-88
lines changed

Diff for: crates/cargo-test-support/src/compare.rs

+1
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ static E2E_LITERAL_REDACTIONS: &[(&str, &str)] = &[
176176
("[DIRTY]", " Dirty"),
177177
("[LOCKING]", " Locking"),
178178
("[UPDATING]", " Updating"),
179+
("[UPGRADING]", " Upgrading"),
179180
("[ADDING]", " Adding"),
180181
("[REMOVING]", " Removing"),
181182
("[REMOVED]", " Removed"),

Diff for: src/bin/cargo/commands/update.rs

+29-2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ pub fn cli() -> Command {
3535
.value_name("PRECISE")
3636
.requires("package-group"),
3737
)
38+
.arg(
39+
flag(
40+
"breaking",
41+
"Upgrade [SPEC] to latest breaking versions, unless pinned (unstable)",
42+
)
43+
.short('b'),
44+
)
3845
.arg_silent_suggestion()
3946
.arg(
4047
flag("workspace", "Only update the workspace packages")
@@ -59,7 +66,8 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
5966
gctx.cli_unstable().msrv_policy,
6067
)?;
6168
}
62-
let ws = args.workspace(gctx)?;
69+
70+
let mut ws = args.workspace(gctx)?;
6371

6472
if args.is_present_with_zero_values("package") {
6573
print_available_packages(&ws)?;
@@ -84,11 +92,30 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
8492
let update_opts = UpdateOptions {
8593
recursive: args.flag("recursive"),
8694
precise: args.get_one::<String>("precise").map(String::as_str),
95+
breaking: args.flag("breaking"),
8796
to_update,
8897
dry_run: args.dry_run(),
8998
workspace: args.flag("workspace"),
9099
gctx,
91100
};
92-
ops::update_lockfile(&ws, &update_opts)?;
101+
102+
if update_opts.breaking {
103+
gctx.cli_unstable()
104+
.fail_if_stable_opt("--breaking", 12425)?;
105+
106+
let upgrades = ops::upgrade_manifests(&mut ws, &update_opts)?;
107+
ops::resolve_ws(&ws, update_opts.dry_run)?;
108+
ops::write_manifest_upgrades(&ws, &update_opts, &upgrades)?;
109+
110+
if update_opts.dry_run {
111+
update_opts
112+
.gctx
113+
.shell()
114+
.warn("aborting update due to dry run")?;
115+
}
116+
} else {
117+
ops::update_lockfile(&ws, &update_opts)?;
118+
}
119+
93120
Ok(())
94121
}

Diff for: src/cargo/core/summary.rs

+13-3
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,25 @@ impl Summary {
103103
Rc::make_mut(&mut self.inner).checksum = Some(cksum);
104104
}
105105

106-
pub fn map_dependencies<F>(mut self, f: F) -> Summary
106+
pub fn map_dependencies<F>(self, mut f: F) -> Summary
107107
where
108108
F: FnMut(Dependency) -> Dependency,
109+
{
110+
self.try_map_dependencies(|dep| Ok(f(dep))).unwrap()
111+
}
112+
113+
pub fn try_map_dependencies<F>(mut self, f: F) -> CargoResult<Summary>
114+
where
115+
F: FnMut(Dependency) -> CargoResult<Dependency>,
109116
{
110117
{
111118
let slot = &mut Rc::make_mut(&mut self.inner).dependencies;
112-
*slot = mem::take(slot).into_iter().map(f).collect();
119+
*slot = mem::take(slot)
120+
.into_iter()
121+
.map(f)
122+
.collect::<CargoResult<_>>()?;
113123
}
114-
self
124+
Ok(self)
115125
}
116126

117127
pub fn map_source(self, to_replace: SourceId, replace_with: SourceId) -> Summary {

Diff for: src/cargo/ops/cargo_update.rs

+253-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::core::dependency::Dependency;
12
use crate::core::registry::PackageRegistry;
23
use crate::core::resolver::features::{CliFeatures, HasDevUnits};
34
use crate::core::shell::Verbosity;
@@ -8,17 +9,25 @@ use crate::ops;
89
use crate::sources::source::QueryKind;
910
use crate::util::cache_lock::CacheLockMode;
1011
use crate::util::context::GlobalContext;
11-
use crate::util::style;
12-
use crate::util::CargoResult;
12+
use crate::util::toml_mut::dependency::{MaybeWorkspace, Source};
13+
use crate::util::toml_mut::manifest::LocalManifest;
14+
use crate::util::toml_mut::upgrade::upgrade_requirement;
15+
use crate::util::{style, OptVersionReq};
16+
use crate::util::{CargoResult, VersionExt};
17+
use itertools::Itertools;
18+
use semver::{Op, Version, VersionReq};
1319
use std::cmp::Ordering;
14-
use std::collections::{BTreeMap, HashSet};
15-
use tracing::debug;
20+
use std::collections::{BTreeMap, HashMap, HashSet};
21+
use tracing::{debug, trace};
22+
23+
pub type UpgradeMap = HashMap<(String, SourceId), Version>;
1624

1725
pub struct UpdateOptions<'a> {
1826
pub gctx: &'a GlobalContext,
1927
pub to_update: Vec<String>,
2028
pub precise: Option<&'a str>,
2129
pub recursive: bool,
30+
pub breaking: bool,
2231
pub dry_run: bool,
2332
pub workspace: bool,
2433
}
@@ -207,6 +216,246 @@ pub fn print_lockfile_changes(
207216
}
208217
}
209218

219+
pub fn upgrade_manifests(
220+
ws: &mut Workspace<'_>,
221+
opts: &UpdateOptions<'_>,
222+
) -> CargoResult<UpgradeMap> {
223+
let mut upgrades = HashMap::new();
224+
let mut upgrade_messages = HashSet::new();
225+
226+
// Updates often require a lot of modifications to the registry, so ensure
227+
// that we're synchronized against other Cargos.
228+
let _lock = ws
229+
.gctx()
230+
.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
231+
232+
let mut registry = PackageRegistry::new(opts.gctx)?;
233+
registry.lock_patches();
234+
235+
for member in ws.members_mut().sorted() {
236+
debug!("upgrading manifest for `{}`", member.name());
237+
238+
*member.manifest_mut().summary_mut() = member
239+
.manifest()
240+
.summary()
241+
.clone()
242+
.try_map_dependencies(|d| {
243+
upgrade_dependency(&mut registry, &mut upgrades, &mut upgrade_messages, opts, d)
244+
})?;
245+
}
246+
247+
Ok(upgrades)
248+
}
249+
250+
fn upgrade_dependency(
251+
registry: &mut PackageRegistry<'_>,
252+
upgrades: &mut UpgradeMap,
253+
upgrade_messages: &mut HashSet<String>,
254+
opts: &UpdateOptions<'_>,
255+
dependency: Dependency,
256+
) -> CargoResult<Dependency> {
257+
let name = dependency.package_name();
258+
let renamed_to = dependency.name_in_toml();
259+
260+
if name != renamed_to {
261+
trace!(
262+
"skipping dependency renamed from `{}` to `{}`",
263+
name,
264+
renamed_to
265+
);
266+
return Ok(dependency);
267+
}
268+
269+
if !opts.to_update.is_empty() && !opts.to_update.contains(&name.to_string()) {
270+
trace!("skipping dependency `{}` not selected for upgrading", name);
271+
return Ok(dependency);
272+
}
273+
274+
if !dependency.source_id().is_registry() {
275+
trace!("skipping non-registry dependency: {}", name);
276+
return Ok(dependency);
277+
}
278+
279+
let version_req = dependency.version_req();
280+
281+
let OptVersionReq::Req(current) = version_req else {
282+
trace!(
283+
"skipping dependency `{}` without a simple version requirement: {}",
284+
name,
285+
version_req
286+
);
287+
return Ok(dependency);
288+
};
289+
290+
let [comparator] = &current.comparators[..] else {
291+
trace!(
292+
"skipping dependency `{}` with multiple version comparators: {:?}",
293+
name,
294+
&current.comparators
295+
);
296+
return Ok(dependency);
297+
};
298+
299+
if comparator.op != Op::Caret {
300+
trace!("skipping non-caret dependency `{}`: {}", name, comparator);
301+
return Ok(dependency);
302+
}
303+
304+
let query =
305+
crate::core::dependency::Dependency::parse(name, None, dependency.source_id().clone())?;
306+
307+
let possibilities = {
308+
loop {
309+
match registry.query_vec(&query, QueryKind::Exact) {
310+
std::task::Poll::Ready(res) => {
311+
break res?;
312+
}
313+
std::task::Poll::Pending => registry.block_until_ready()?,
314+
}
315+
}
316+
};
317+
318+
let latest = if !possibilities.is_empty() {
319+
possibilities
320+
.iter()
321+
.map(|s| s.as_summary())
322+
.map(|s| s.version())
323+
.filter(|v| !v.is_prerelease())
324+
.max()
325+
} else {
326+
None
327+
};
328+
329+
let Some(latest) = latest else {
330+
trace!(
331+
"skipping dependency `{}` without any published versions",
332+
name
333+
);
334+
return Ok(dependency);
335+
};
336+
337+
if current.matches(&latest) {
338+
trace!(
339+
"skipping dependency `{}` without a breaking update available",
340+
name
341+
);
342+
return Ok(dependency);
343+
}
344+
345+
let Some(new_req_string) = upgrade_requirement(&current.to_string(), latest)? else {
346+
trace!(
347+
"skipping dependency `{}` because the version requirement didn't change",
348+
name
349+
);
350+
return Ok(dependency);
351+
};
352+
353+
let upgrade_message = format!("{} {} -> {}", name, current, new_req_string);
354+
trace!(upgrade_message);
355+
356+
if upgrade_messages.insert(upgrade_message.clone()) {
357+
opts.gctx
358+
.shell()
359+
.status_with_color("Upgrading", &upgrade_message, &style::GOOD)?;
360+
}
361+
362+
upgrades.insert((name.to_string(), dependency.source_id()), latest.clone());
363+
364+
let req = OptVersionReq::Req(VersionReq::parse(&latest.to_string())?);
365+
let mut dep = dependency.clone();
366+
dep.set_version_req(req);
367+
Ok(dep)
368+
}
369+
370+
/// Update manifests with upgraded versions, and write to disk. Based on cargo-edit.
371+
/// Returns true if any file has changed.
372+
pub fn write_manifest_upgrades(
373+
ws: &Workspace<'_>,
374+
opts: &UpdateOptions<'_>,
375+
upgrades: &UpgradeMap,
376+
) -> CargoResult<bool> {
377+
if upgrades.is_empty() {
378+
return Ok(false);
379+
}
380+
381+
let mut any_file_has_changed = false;
382+
383+
let manifest_paths = std::iter::once(ws.root_manifest())
384+
.chain(ws.members().map(|member| member.manifest_path()))
385+
.collect::<Vec<_>>();
386+
387+
for manifest_path in manifest_paths {
388+
trace!(
389+
"updating TOML manifest at `{:?}` with upgraded dependencies",
390+
manifest_path
391+
);
392+
393+
let crate_root = manifest_path
394+
.parent()
395+
.expect("manifest path is absolute")
396+
.to_owned();
397+
398+
let mut local_manifest = LocalManifest::try_new(&manifest_path)?;
399+
let mut manifest_has_changed = false;
400+
401+
for dep_table in local_manifest.get_dependency_tables_mut() {
402+
for (mut dep_key, dep_item) in dep_table.iter_mut() {
403+
let dep_key_str = dep_key.get();
404+
let dependency = crate::util::toml_mut::dependency::Dependency::from_toml(
405+
&manifest_path,
406+
dep_key_str,
407+
dep_item,
408+
)?;
409+
410+
let Some(current) = dependency.version() else {
411+
trace!("skipping dependency without a version: {}", dependency.name);
412+
continue;
413+
};
414+
415+
let (MaybeWorkspace::Other(source_id), Some(Source::Registry(source))) =
416+
(dependency.source_id(opts.gctx)?, dependency.source())
417+
else {
418+
trace!("skipping non-registry dependency: {}", dependency.name);
419+
continue;
420+
};
421+
422+
let Some(latest) = upgrades.get(&(dependency.name.to_owned(), source_id)) else {
423+
trace!(
424+
"skipping dependency without an upgrade: {}",
425+
dependency.name
426+
);
427+
continue;
428+
};
429+
430+
let Some(new_req_string) = upgrade_requirement(current, latest)? else {
431+
trace!(
432+
"skipping dependency `{}` because the version requirement didn't change",
433+
dependency.name
434+
);
435+
continue;
436+
};
437+
438+
let mut dep = dependency.clone();
439+
let mut source = source.clone();
440+
source.version = new_req_string;
441+
dep.source = Some(Source::Registry(source));
442+
443+
trace!("upgrading dependency {}", dependency.name);
444+
dep.update_toml(&crate_root, &mut dep_key, dep_item);
445+
manifest_has_changed = true;
446+
any_file_has_changed = true;
447+
}
448+
}
449+
450+
if manifest_has_changed && !opts.dry_run {
451+
debug!("writing upgraded manifest to {}", manifest_path.display());
452+
local_manifest.write()?;
453+
}
454+
}
455+
456+
Ok(any_file_has_changed)
457+
}
458+
210459
fn print_lockfile_generation(
211460
ws: &Workspace<'_>,
212461
resolve: &Resolve,

Diff for: src/cargo/ops/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ pub use self::cargo_uninstall::uninstall;
1919
pub use self::cargo_update::generate_lockfile;
2020
pub use self::cargo_update::print_lockfile_changes;
2121
pub use self::cargo_update::update_lockfile;
22+
pub use self::cargo_update::upgrade_manifests;
23+
pub use self::cargo_update::write_manifest_upgrades;
2224
pub use self::cargo_update::UpdateOptions;
2325
pub use self::fix::{fix, fix_exec_rustc, fix_get_proxy_lock_addr, FixOptions};
2426
pub use self::lockfile::{load_pkg_lockfile, resolve_to_string, write_pkg_lockfile};

0 commit comments

Comments
 (0)