Skip to content

Commit 031b410

Browse files
committed
feat: Implement cargo update --breaking.
1 parent c694ff0 commit 031b410

File tree

11 files changed

+686
-88
lines changed

11 files changed

+686
-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

+28-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)?;
@@ -89,6 +97,24 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
8997
workspace: args.flag("workspace"),
9098
gctx,
9199
};
92-
ops::update_lockfile(&ws, &update_opts)?;
100+
101+
if args.flag("breaking") {
102+
gctx.cli_unstable()
103+
.fail_if_stable_opt("--breaking", 12425)?;
104+
105+
let upgrades = ops::upgrade_manifests(&mut ws, &update_opts.to_update)?;
106+
ops::resolve_ws(&ws, update_opts.dry_run)?;
107+
ops::write_manifest_upgrades(&ws, &upgrades, update_opts.dry_run)?;
108+
109+
if update_opts.dry_run {
110+
update_opts
111+
.gctx
112+
.shell()
113+
.warn("aborting update due to dry run")?;
114+
}
115+
} else {
116+
ops::update_lockfile(&ws, &update_opts)?;
117+
}
118+
93119
Ok(())
94120
}

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

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

210463
fn print_lockfile_generation(
211464
ws: &Workspace<'_>,

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)