1use crate::core::Registry as _;
2use crate::core::dependency::Dependency;
3use crate::core::registry::PackageRegistry;
4use crate::core::resolver::PublishAgePolicy;
5use crate::core::resolver::features::{CliFeatures, HasDevUnits};
6use crate::core::{PackageId, PackageIdSpec, PackageIdSpecQuery};
7use crate::core::{Resolve, SourceId, Workspace};
8use crate::ops;
9use crate::sources::IndexSummary;
10use crate::sources::source::QueryKind;
11use crate::util::cache_lock::CacheLockMode;
12use crate::util::context::GlobalContext;
13use crate::util::toml_mut::dependency::{MaybeWorkspace, Source};
14use crate::util::toml_mut::manifest::LocalManifest;
15use crate::util::toml_mut::upgrade::upgrade_requirement;
16use crate::util::{CargoResult, VersionExt};
17use crate::util::{OptVersionReq, style};
18
19use crate::util::data_structures::{HashMap, HashSet};
20use crate::util::data_structures::{IndexMap, IndexSet};
21use anyhow::Context as _;
22use cargo_util_schemas::core::PartialVersion;
23use cargo_util_terminal::Verbosity;
24use itertools::Itertools;
25use semver::{Op, Version, VersionReq};
26use std::cmp::Ordering;
27use std::collections::BTreeMap;
28use tracing::{debug, trace};
29
30pub type UpgradeMap = HashMap<(String, SourceId), Version>;
31
32pub struct UpdateOptions<'a> {
33 pub gctx: &'a GlobalContext,
34 pub to_update: Vec<String>,
35 pub precise: Option<&'a str>,
36 pub recursive: bool,
37 pub dry_run: bool,
38 pub workspace: bool,
39}
40
41pub fn generate_lockfile(ws: &Workspace<'_>) -> CargoResult<()> {
42 let mut registry = ws.package_registry()?;
43 let previous_resolve = None;
44 let mut resolve = ops::resolve_with_previous(
45 &mut registry,
46 ws,
47 &CliFeatures::new_all(true),
48 HasDevUnits::Yes,
49 previous_resolve,
50 None,
51 &[],
52 true,
53 )?;
54 ops::write_pkg_lockfile(ws, &mut resolve)?;
55 print_lockfile_changes(ws, previous_resolve, &resolve, &mut registry)?;
56 Ok(())
57}
58
59pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoResult<()> {
60 if opts.recursive && opts.precise.is_some() {
61 anyhow::bail!("cannot specify both recursive and precise simultaneously")
62 }
63
64 if ws.members().count() == 0 {
65 anyhow::bail!("you can't generate a lockfile for an empty workspace.")
66 }
67
68 let _lock = ws
71 .gctx()
72 .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
73
74 let previous_resolve = match ops::load_pkg_lockfile(ws)? {
75 Some(resolve) => resolve,
76 None => {
77 match opts.precise {
78 None => return generate_lockfile(ws),
79
80 Some(_) => {
83 let mut registry = ws.package_registry()?;
84 ops::resolve_with_previous(
85 &mut registry,
86 ws,
87 &CliFeatures::new_all(true),
88 HasDevUnits::Yes,
89 None,
90 None,
91 &[],
92 true,
93 )?
94 }
95 }
96 }
97 };
98 let mut registry = ws.package_registry()?;
99 let mut to_avoid = HashSet::default();
100
101 if opts.to_update.is_empty() {
102 if !opts.workspace {
103 to_avoid.extend(previous_resolve.iter());
104 to_avoid.extend(previous_resolve.unused_patches());
105 }
106 } else {
107 let mut sources = Vec::new();
108 for name in opts.to_update.iter() {
109 let pid = previous_resolve.query(name)?;
110 if opts.recursive {
111 fill_with_deps(
112 &previous_resolve,
113 pid,
114 &mut to_avoid,
115 &mut HashSet::default(),
116 );
117 } else {
118 to_avoid.insert(pid);
119 sources.push(match opts.precise {
120 Some(precise) => {
121 if pid.source_id().is_registry() {
125 pid.source_id().with_precise_registry_version(
126 pid.name(),
127 pid.version().clone(),
128 precise,
129 )?
130 } else {
131 pid.source_id().with_git_precise(Some(precise.to_string()))
132 }
133 }
134 None => pid.source_id().without_precise(),
135 });
136 }
137 if let Ok(unused_id) =
138 PackageIdSpec::query_str(name, previous_resolve.unused_patches().iter().cloned())
139 {
140 to_avoid.insert(unused_id);
141 }
142 }
143
144 to_avoid.retain(|id| {
148 for package in ws.members() {
149 let member_id = package.package_id();
150 if id.name() == member_id.name() && id.source_id() == member_id.source_id() {
155 return false;
156 }
157 }
158 true
159 });
160
161 registry.add_sources(sources)?;
162 }
163
164 let to_avoid_sources: HashSet<_> = to_avoid
172 .iter()
173 .map(|p| p.source_id())
174 .filter(|s| !s.is_registry())
175 .collect();
176
177 let keep = |p: &PackageId| !to_avoid_sources.contains(&p.source_id()) && !to_avoid.contains(p);
178
179 let mut resolve = ops::resolve_with_previous(
180 &mut registry,
181 ws,
182 &CliFeatures::new_all(true),
183 HasDevUnits::Yes,
184 Some(&previous_resolve),
185 Some(&keep),
186 &[],
187 true,
188 )?;
189
190 print_lockfile_updates(
191 ws,
192 &previous_resolve,
193 &resolve,
194 opts.precise.is_some(),
195 &mut registry,
196 )?;
197 if opts.dry_run {
198 opts.gctx
199 .shell()
200 .warn("not updating lockfile due to dry run")?;
201 } else {
202 ops::write_pkg_lockfile(ws, &mut resolve)?;
203 }
204 Ok(())
205}
206
207pub fn print_lockfile_changes(
212 ws: &Workspace<'_>,
213 previous_resolve: Option<&Resolve>,
214 resolve: &Resolve,
215 registry: &mut PackageRegistry<'_>,
216) -> CargoResult<()> {
217 let _lock = ws
218 .gctx()
219 .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
220 if let Some(previous_resolve) = previous_resolve {
221 print_lockfile_sync(ws, previous_resolve, resolve, registry)
222 } else {
223 print_lockfile_generation(ws, resolve, registry)
224 }
225}
226pub fn upgrade_manifests(
227 ws: &mut Workspace<'_>,
228 to_update: &Vec<String>,
229) -> CargoResult<UpgradeMap> {
230 let gctx = ws.gctx();
231 let mut upgrades = HashMap::default();
232 let mut upgrade_messages = HashSet::default();
233
234 let to_update = to_update
235 .iter()
236 .map(|spec| {
237 PackageIdSpec::parse(spec)
238 .with_context(|| format!("invalid package ID specification: `{spec}`"))
239 })
240 .collect::<Result<Vec<_>, _>>()?;
241
242 let _lock = gctx.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
245
246 let mut registry = ws.package_registry()?;
247 registry.lock_patches();
248
249 let mut remaining_specs: IndexSet<_> = to_update.iter().cloned().collect();
250
251 for member in ws.members_mut().sorted() {
252 debug!("upgrading manifest for `{}`", member.name());
253
254 *member.manifest_mut().summary_mut() = member
255 .manifest()
256 .summary()
257 .clone()
258 .try_map_dependencies(|d| {
259 upgrade_dependency(
260 &gctx,
261 &to_update,
262 &mut registry,
263 &mut upgrades,
264 &mut upgrade_messages,
265 &mut remaining_specs,
266 d,
267 )
268 })?;
269 }
270
271 if !remaining_specs.is_empty() {
272 let previous_resolve = ops::load_pkg_lockfile(ws)?;
273 let plural = if remaining_specs.len() == 1 { "" } else { "s" };
274
275 let mut error_msg = format!(
276 "package ID specification{plural} did not match any direct dependencies that could be upgraded"
277 );
278
279 let mut transitive_specs = Vec::new();
280 for spec in &remaining_specs {
281 error_msg.push_str(&format!("\n {spec}"));
282
283 let in_lockfile = if let Some(ref resolve) = previous_resolve {
285 spec.query(resolve.iter()).is_ok()
286 } else {
287 false
288 };
289
290 let matches_direct_dep = ws.members().any(|member| {
292 member.dependencies().iter().any(|dep| {
293 spec.name() == dep.package_name().as_str()
294 && dep.source_id().is_registry()
295 && spec.url().map_or(true, |url| url == dep.source_id().url())
296 && spec
297 .version()
298 .map_or(true, |v| dep.version_req().matches(&v))
299 })
300 });
301
302 if in_lockfile && !matches_direct_dep {
304 transitive_specs.push(spec);
305 }
306 }
307
308 for spec in transitive_specs {
309 error_msg.push_str(&format!(
310 "\nnote: `{spec}` exists as a transitive dependency but those are not available for upgrading through `--breaking`"
311 ));
312 }
313
314 anyhow::bail!("{error_msg}");
315 }
316
317 Ok(upgrades)
318}
319
320fn upgrade_dependency(
321 gctx: &GlobalContext,
322 to_update: &Vec<PackageIdSpec>,
323 registry: &mut PackageRegistry<'_>,
324 upgrades: &mut UpgradeMap,
325 upgrade_messages: &mut HashSet<String>,
326 remaining_specs: &mut IndexSet<PackageIdSpec>,
327 dependency: Dependency,
328) -> CargoResult<Dependency> {
329 let name = dependency.package_name();
330 let renamed_to = dependency.name_in_toml();
331
332 if name != renamed_to {
333 trace!("skipping dependency renamed from `{name}` to `{renamed_to}`");
334 return Ok(dependency);
335 }
336
337 if !to_update.is_empty()
338 && !to_update.iter().any(|spec| {
339 spec.name() == name.as_str()
340 && dependency.source_id().is_registry()
341 && spec
342 .url()
343 .map_or(true, |url| url == dependency.source_id().url())
344 && spec
345 .version()
346 .map_or(true, |v| dependency.version_req().matches(&v))
347 })
348 {
349 trace!("skipping dependency `{name}` not selected for upgrading");
350 return Ok(dependency);
351 }
352
353 if !dependency.source_id().is_registry() {
354 trace!("skipping non-registry dependency: {name}");
355 return Ok(dependency);
356 }
357
358 let version_req = dependency.version_req();
359
360 let OptVersionReq::Req(current) = version_req else {
361 trace!("skipping dependency `{name}` without a simple version requirement: {version_req}");
362 return Ok(dependency);
363 };
364
365 let [comparator] = ¤t.comparators[..] else {
366 trace!(
367 "skipping dependency `{name}` with multiple version comparators: {:?}",
368 ¤t.comparators
369 );
370 return Ok(dependency);
371 };
372
373 if comparator.op != Op::Caret {
374 trace!("skipping non-caret dependency `{name}`: {comparator}");
375 return Ok(dependency);
376 }
377
378 let query =
379 crate::core::dependency::Dependency::parse(name, None, dependency.source_id().clone())?;
380
381 let possibilities = crate::util::block_on(registry.query_vec(&query, QueryKind::Exact))?;
382
383 let latest = if !possibilities.is_empty() {
384 possibilities
385 .iter()
386 .filter_map(|s| match s {
387 IndexSummary::Candidate(s) => Some(s),
388 _ => None,
389 })
390 .map(|s| s.version())
391 .filter(|v| !v.is_prerelease())
392 .max()
393 } else {
394 None
395 };
396
397 let Some(latest) = latest else {
398 trace!("skipping dependency `{name}` without any published versions");
399 return Ok(dependency);
400 };
401
402 if current.matches(&latest) {
403 trace!("skipping dependency `{name}` without a breaking update available");
404 return Ok(dependency);
405 }
406
407 let Some((new_req_string, _)) = upgrade_requirement(¤t.to_string(), latest)? else {
408 trace!("skipping dependency `{name}` because the version requirement didn't change");
409 return Ok(dependency);
410 };
411
412 let upgrade_message = format!("{name} {current} -> {new_req_string}");
413 trace!(upgrade_message);
414
415 if upgrade_messages.insert(upgrade_message.clone()) {
416 gctx.shell()
417 .status_with_color("Upgrading", &upgrade_message, &style::GOOD)?;
418 }
419
420 upgrades.insert((name.to_string(), dependency.source_id()), latest.clone());
421
422 remaining_specs
424 .retain(|spec| !(spec.name() == name.as_str() && dependency.source_id().is_registry()));
425
426 let req = OptVersionReq::Req(VersionReq::parse(&latest.to_string())?);
427 let mut dep = dependency.clone();
428 dep.set_version_req(req);
429 Ok(dep)
430}
431
432pub fn write_manifest_upgrades(
444 ws: &Workspace<'_>,
445 upgrades: &UpgradeMap,
446 dry_run: bool,
447) -> CargoResult<bool> {
448 if upgrades.is_empty() {
449 return Ok(false);
450 }
451
452 let mut any_file_has_changed = false;
453
454 let items = std::iter::once((ws.root_manifest(), ws.unstable_features()))
455 .chain(ws.members().map(|member| {
456 (
457 member.manifest_path(),
458 member.manifest().unstable_features(),
459 )
460 }))
461 .collect::<Vec<_>>();
462
463 for (manifest_path, unstable_features) in items {
464 trace!("updating TOML manifest at `{manifest_path:?}` with upgraded dependencies");
465
466 let crate_root = manifest_path
467 .parent()
468 .expect("manifest path is absolute")
469 .to_owned();
470
471 let mut local_manifest = LocalManifest::try_new(&manifest_path)?;
472 let mut manifest_has_changed = false;
473
474 for dep_table in local_manifest.get_dependency_tables_mut() {
475 for (mut dep_key, dep_item) in dep_table.iter_mut() {
476 let dep_key_str = dep_key.get();
477 let dependency = crate::util::toml_mut::dependency::Dependency::from_toml(
478 ws.gctx(),
479 ws.root(),
480 &manifest_path,
481 unstable_features,
482 dep_key_str,
483 dep_item,
484 )?;
485 let name = &dependency.name;
486
487 if let Some(renamed_to) = dependency.rename {
488 trace!("skipping dependency renamed from `{name}` to `{renamed_to}`");
489 continue;
490 }
491
492 let Some(current) = dependency.version() else {
493 trace!("skipping dependency without a version: {name}");
494 continue;
495 };
496
497 let (MaybeWorkspace::Other(source_id), Some(Source::Registry(source))) =
498 (dependency.source_id(ws.gctx())?, dependency.source())
499 else {
500 trace!("skipping non-registry dependency: {name}");
501 continue;
502 };
503
504 let Some(latest) = upgrades.get(&(name.to_owned(), source_id)) else {
505 trace!("skipping dependency without an upgrade: {name}");
506 continue;
507 };
508
509 let Some((new_req_string, new_req)) = upgrade_requirement(current, latest)? else {
510 trace!(
511 "skipping dependency `{name}` because the version requirement didn't change"
512 );
513 continue;
514 };
515
516 let [comparator] = &new_req.comparators[..] else {
517 trace!(
518 "skipping dependency `{}` with multiple version comparators: {:?}",
519 name, new_req.comparators
520 );
521 continue;
522 };
523
524 if comparator.op != Op::Caret {
525 trace!("skipping non-caret dependency `{}`: {}", name, comparator);
526 continue;
527 }
528
529 let mut dep = dependency.clone();
530 let mut source = source.clone();
531 source.version = new_req_string;
532 dep.source = Some(Source::Registry(source));
533
534 trace!("upgrading dependency {name}");
535 dep.update_toml(
536 ws.gctx(),
537 ws.root(),
538 &crate_root,
539 unstable_features,
540 &mut dep_key,
541 dep_item,
542 )?;
543 manifest_has_changed = true;
544 any_file_has_changed = true;
545 }
546 }
547
548 if manifest_has_changed && !dry_run {
549 debug!("writing upgraded manifest to {}", manifest_path.display());
550 local_manifest.write()?;
551 }
552 }
553
554 Ok(any_file_has_changed)
555}
556
557fn print_lockfile_generation(
558 ws: &Workspace<'_>,
559 resolve: &Resolve,
560 registry: &mut PackageRegistry<'_>,
561) -> CargoResult<()> {
562 let mut changes = PackageChange::new(ws, resolve);
563 let num_pkgs: usize = changes
564 .values()
565 .filter(|change| change.kind.is_new() && !change.is_member.unwrap_or(false))
566 .count();
567 if num_pkgs == 0 {
568 return Ok(());
570 }
571 annotate_required_rust_version(ws, resolve, &mut changes);
572 let publish_age = publish_age_policy_for_report(ws);
573
574 status_locking(ws, num_pkgs)?;
575 for change in changes.values() {
576 if change.is_member.unwrap_or(false) {
577 continue;
578 };
579 match change.kind {
580 PackageChangeKind::Added => {
581 let possibilities = if let Some(query) = change.alternatives_query() {
582 crate::util::block_on(registry.query_vec(&query, QueryKind::Exact))?
583 } else {
584 vec![]
585 };
586
587 let required_rust_version = report_required_rust_version(resolve, change);
588 let too_new = report_too_new(resolve, change, publish_age.as_ref());
589 let latest = report_latest(&possibilities, change, publish_age.as_ref());
590 let note = required_rust_version.or(too_new).or(latest);
591
592 if let Some(note) = note {
593 ws.gctx().shell().status_with_color(
594 change.kind.status(),
595 format!("{change}{note}"),
596 &change.kind.style(),
597 )?;
598 }
599 }
600 PackageChangeKind::Upgraded
601 | PackageChangeKind::Downgraded
602 | PackageChangeKind::Removed
603 | PackageChangeKind::Unchanged => {
604 unreachable!("without a previous resolve, everything should be added")
605 }
606 }
607 }
608
609 Ok(())
610}
611
612fn print_lockfile_sync(
613 ws: &Workspace<'_>,
614 previous_resolve: &Resolve,
615 resolve: &Resolve,
616 registry: &mut PackageRegistry<'_>,
617) -> CargoResult<()> {
618 let mut changes = PackageChange::diff(ws, previous_resolve, resolve);
619 let num_pkgs: usize = changes
620 .values()
621 .filter(|change| change.kind.is_new() && !change.is_member.unwrap_or(false))
622 .count();
623 if num_pkgs == 0 {
624 return Ok(());
626 }
627 annotate_required_rust_version(ws, resolve, &mut changes);
628 let publish_age = publish_age_policy_for_report(ws);
629
630 status_locking(ws, num_pkgs)?;
631 for change in changes.values() {
632 if change.is_member.unwrap_or(false) {
633 continue;
634 };
635 match change.kind {
636 PackageChangeKind::Added
637 | PackageChangeKind::Upgraded
638 | PackageChangeKind::Downgraded => {
639 let possibilities = if let Some(query) = change.alternatives_query() {
640 crate::util::block_on(registry.query_vec(&query, QueryKind::Exact))?
641 } else {
642 vec![]
643 };
644
645 let required_rust_version = report_required_rust_version(resolve, change);
646 let too_new = report_too_new(resolve, change, publish_age.as_ref());
647 let latest = report_latest(&possibilities, change, publish_age.as_ref());
648 let note = required_rust_version
649 .or(too_new)
650 .or(latest)
651 .unwrap_or_default();
652
653 ws.gctx().shell().status_with_color(
654 change.kind.status(),
655 format!("{change}{note}"),
656 &change.kind.style(),
657 )?;
658 }
659 PackageChangeKind::Removed | PackageChangeKind::Unchanged => {}
660 }
661 }
662
663 Ok(())
664}
665
666fn print_lockfile_updates(
667 ws: &Workspace<'_>,
668 previous_resolve: &Resolve,
669 resolve: &Resolve,
670 precise: bool,
671 registry: &mut PackageRegistry<'_>,
672) -> CargoResult<()> {
673 let mut changes = PackageChange::diff(ws, previous_resolve, resolve);
674 let num_pkgs: usize = changes
675 .values()
676 .filter(|change| change.kind.is_new())
677 .count();
678 annotate_required_rust_version(ws, resolve, &mut changes);
679 let publish_age = publish_age_policy_for_report(ws);
680
681 if !precise {
682 status_locking(ws, num_pkgs)?;
683 }
684 let mut unchanged_behind = 0;
685 for change in changes.values() {
686 let possibilities = if let Some(query) = change.alternatives_query() {
687 crate::util::block_on(registry.query_vec(&query, QueryKind::Exact))?
688 } else {
689 vec![]
690 };
691
692 match change.kind {
693 PackageChangeKind::Added
694 | PackageChangeKind::Upgraded
695 | PackageChangeKind::Downgraded => {
696 let required_rust_version = report_required_rust_version(resolve, change);
697 let too_new = report_too_new(resolve, change, publish_age.as_ref());
698 let latest = report_latest(&possibilities, change, publish_age.as_ref());
699 let note = required_rust_version
700 .or(too_new)
701 .or(latest)
702 .unwrap_or_default();
703
704 ws.gctx().shell().status_with_color(
705 change.kind.status(),
706 format!("{change}{note}"),
707 &change.kind.style(),
708 )?;
709 }
710 PackageChangeKind::Removed => {
711 ws.gctx().shell().status_with_color(
712 change.kind.status(),
713 format!("{change}"),
714 &change.kind.style(),
715 )?;
716 }
717 PackageChangeKind::Unchanged => {
718 let required_rust_version = report_required_rust_version(resolve, change);
719 let too_new = report_too_new(resolve, change, publish_age.as_ref());
720 let latest = report_latest(&possibilities, change, publish_age.as_ref());
721 let note = required_rust_version
722 .as_deref()
723 .or(too_new.as_deref())
724 .or(latest.as_deref());
725
726 if let Some(note) = note {
727 if latest.is_some() {
728 unchanged_behind += 1;
729 }
730 if ws.gctx().shell().verbosity() == Verbosity::Verbose {
731 ws.gctx().shell().status_with_color(
732 change.kind.status(),
733 format!("{change}{note}"),
734 &change.kind.style(),
735 )?;
736 }
737 }
738 }
739 }
740 }
741
742 if ws.gctx().shell().verbosity() == Verbosity::Verbose {
743 ws.gctx()
744 .shell()
745 .note("to see how you depend on a package, run `cargo tree --invert <dep>@<ver>`")?;
746 } else {
747 if 0 < unchanged_behind {
748 ws.gctx().shell().note(format!(
749 "pass `--verbose` to see {unchanged_behind} unchanged dependencies behind latest"
750 ))?;
751 }
752 }
753
754 Ok(())
755}
756
757fn status_locking(ws: &Workspace<'_>, num_pkgs: usize) -> CargoResult<()> {
758 use std::fmt::Write as _;
759
760 let plural = if num_pkgs == 1 { "" } else { "s" };
761
762 let mut cfg = String::new();
763 if !ws.gctx().cli_unstable().direct_minimal_versions {
765 write!(&mut cfg, " to")?;
766 if ws.gctx().cli_unstable().minimal_versions {
767 write!(&mut cfg, " earliest")?;
768 } else {
769 write!(&mut cfg, " latest")?;
770 }
771
772 if let Some(rust_version) = required_rust_version(ws) {
773 write!(&mut cfg, " Rust {rust_version}")?;
774 }
775 write!(&mut cfg, " compatible version{plural}")?;
776 if let Some(publish_time) = ws.resolve_publish_time() {
777 write!(&mut cfg, " as of {publish_time}")?;
778 }
779 }
780
781 ws.gctx()
782 .shell()
783 .status("Locking", format!("{num_pkgs} package{plural}{cfg}"))?;
784 Ok(())
785}
786
787fn required_rust_version(ws: &Workspace<'_>) -> Option<PartialVersion> {
788 if !ws.resolve_honors_rust_version() {
789 return None;
790 }
791
792 if let Some(ver) = ws.lowest_rust_version() {
793 Some(ver.to_partial())
794 } else {
795 let rustc = ws.gctx().load_global_rustc(Some(ws)).ok()?;
796 let rustc_version = rustc.version.clone().into();
797 Some(rustc_version)
798 }
799}
800
801fn publish_age_policy_for_report(ws: &Workspace<'_>) -> Option<PublishAgePolicy> {
802 if !ws.resolve_honors_publish_age() {
803 return None;
804 }
805 PublishAgePolicy::for_report(ws.gctx()).ok().flatten()
806}
807
808fn report_required_rust_version(resolve: &Resolve, change: &PackageChange) -> Option<String> {
809 if change.package_id.source_id().is_path() {
810 return None;
811 }
812 let summary = resolve.summary(change.package_id);
813 let package_rust_version = summary.rust_version()?;
814 let required_rust_version = change.required_rust_version.as_ref()?;
815 if package_rust_version.is_compatible_with(required_rust_version) {
816 return None;
817 }
818
819 let error = style::ERROR;
820 Some(format!(
821 " {error}(requires Rust {package_rust_version}){error:#}"
822 ))
823}
824
825fn report_too_new(
827 resolve: &Resolve,
828 change: &PackageChange,
829 publish_age: Option<&PublishAgePolicy>,
830) -> Option<String> {
831 let summary = resolve.summary(change.package_id);
832 let note = publish_age?.too_new(summary)?.note();
833
834 let warn = style::WARN;
835 Some(format!(" {warn}({note}){warn:#}"))
836}
837
838fn report_latest(
839 possibilities: &[IndexSummary],
840 change: &PackageChange,
841 publish_age: Option<&PublishAgePolicy>,
842) -> Option<String> {
843 let package_id = change.package_id;
844 if !package_id.source_id().is_registry() {
845 return None;
846 }
847
848 let version_req = package_id.version().to_caret_req();
849 let required_rust_version = change.required_rust_version.as_ref();
850
851 let publish_note = |summary| {
852 let age = publish_age?.too_new(summary)?.age_label();
853 Some(format!(", published {age}"))
854 };
855
856 let compat_ver_compat_msrv_summary = possibilities
857 .iter()
858 .filter_map(|s| match s {
859 IndexSummary::Candidate(s) => Some(s),
860 _ => None,
861 })
862 .filter(|s| {
863 if let (Some(summary_rust_version), Some(required_rust_version)) =
864 (s.rust_version(), required_rust_version)
865 {
866 summary_rust_version.is_compatible_with(required_rust_version)
867 } else {
868 true
869 }
870 })
871 .filter(|s| package_id.version() != s.version() && version_req.matches(s.version()))
872 .max_by_key(|s| s.version());
873 if let Some(summary) = compat_ver_compat_msrv_summary {
874 let warn = style::WARN;
875 let version = summary.version();
876 let publish_note = publish_note(summary).unwrap_or_default();
877 let report = format!(" {warn}(available: v{version}{publish_note}){warn:#}");
878 return Some(report);
879 }
880
881 if !change.is_transitive.unwrap_or(true) {
882 let incompat_ver_compat_msrv_summary = possibilities
883 .iter()
884 .filter_map(|s| match s {
885 IndexSummary::Candidate(s) => Some(s),
886 _ => None,
887 })
888 .filter(|s| {
889 if let (Some(summary_rust_version), Some(required_rust_version)) =
890 (s.rust_version(), required_rust_version)
891 {
892 summary_rust_version.is_compatible_with(required_rust_version)
893 } else {
894 true
895 }
896 })
897 .filter(|s| is_latest(s.version(), package_id.version()))
898 .max_by_key(|s| s.version());
899 if let Some(summary) = incompat_ver_compat_msrv_summary {
900 let warn = style::WARN;
901 let version = summary.version();
902 let publish_note = publish_note(summary).unwrap_or_default();
903 let report = format!(" {warn}(available: v{version}{publish_note}){warn:#}");
904 return Some(report);
905 }
906 }
907
908 let compat_ver_summary = possibilities
909 .iter()
910 .filter_map(|s| match s {
911 IndexSummary::Candidate(s) => Some(s),
912 _ => None,
913 })
914 .filter(|s| package_id.version() != s.version() && version_req.matches(s.version()))
915 .max_by_key(|s| s.version());
916 if let Some(summary) = compat_ver_summary {
917 let msrv_note = summary
918 .rust_version()
919 .map(|rv| format!(", requires Rust {rv}"))
920 .unwrap_or_default();
921 let warn = style::NOP;
922 let version = summary.version();
923 let publish_note = publish_note(summary).unwrap_or_default();
924 let report = format!(" {warn}(available: v{version}{msrv_note}{publish_note}){warn:#}");
925 return Some(report);
926 }
927
928 if !change.is_transitive.unwrap_or(true) {
929 let incompat_ver_summary = possibilities
930 .iter()
931 .filter_map(|s| match s {
932 IndexSummary::Candidate(s) => Some(s),
933 _ => None,
934 })
935 .filter(|s| is_latest(s.version(), package_id.version()))
936 .max_by_key(|s| s.version());
937 if let Some(summary) = incompat_ver_summary {
938 let msrv_note = summary
939 .rust_version()
940 .map(|rv| format!(", requires Rust {rv}"))
941 .unwrap_or_default();
942 let warn = style::NOP;
943 let version = summary.version();
944 let publish_note = publish_note(summary).unwrap_or_default();
945 let report = format!(" {warn}(available: v{version}{msrv_note}{publish_note}){warn:#}");
946 return Some(report);
947 }
948 }
949
950 None
951}
952
953fn is_latest(candidate: &semver::Version, current: &semver::Version) -> bool {
954 current < candidate
955 && (candidate.pre.is_empty()
957 || (candidate.major == current.major
958 && candidate.minor == current.minor
959 && candidate.patch == current.patch))
960}
961
962fn fill_with_deps<'a>(
963 resolve: &'a Resolve,
964 dep: PackageId,
965 set: &mut HashSet<PackageId>,
966 visited: &mut HashSet<PackageId>,
967) {
968 if !visited.insert(dep) {
969 return;
970 }
971 set.insert(dep);
972 for (dep, _) in resolve.deps_not_replaced(dep) {
973 fill_with_deps(resolve, dep, set, visited);
974 }
975}
976
977#[derive(Clone, Debug)]
978struct PackageChange {
979 package_id: PackageId,
980 previous_id: Option<PackageId>,
981 kind: PackageChangeKind,
982 is_member: Option<bool>,
983 is_transitive: Option<bool>,
984 required_rust_version: Option<PartialVersion>,
985}
986
987impl PackageChange {
988 pub fn new(ws: &Workspace<'_>, resolve: &Resolve) -> IndexMap<PackageId, Self> {
989 let diff = PackageDiff::new(resolve);
990 Self::with_diff(diff, ws, resolve)
991 }
992
993 pub fn diff(
994 ws: &Workspace<'_>,
995 previous_resolve: &Resolve,
996 resolve: &Resolve,
997 ) -> IndexMap<PackageId, Self> {
998 let diff = PackageDiff::diff(previous_resolve, resolve);
999 Self::with_diff(diff, ws, resolve)
1000 }
1001
1002 fn with_diff(
1003 diff: impl Iterator<Item = PackageDiff>,
1004 ws: &Workspace<'_>,
1005 resolve: &Resolve,
1006 ) -> IndexMap<PackageId, Self> {
1007 let member_ids: HashSet<_> = ws.members().map(|p| p.package_id()).collect();
1008
1009 let mut changes = IndexMap::default();
1010 for diff in diff {
1011 if let Some((previous_id, package_id)) = diff.change() {
1012 let kind = if previous_id.version().cmp_precedence(package_id.version())
1017 == Ordering::Greater
1018 {
1019 PackageChangeKind::Downgraded
1020 } else {
1021 PackageChangeKind::Upgraded
1022 };
1023 let is_member = Some(member_ids.contains(&package_id));
1024 let is_transitive = Some(true);
1025 let change = Self {
1026 package_id,
1027 previous_id: Some(previous_id),
1028 kind,
1029 is_member,
1030 is_transitive,
1031 required_rust_version: None,
1032 };
1033 changes.insert(change.package_id, change);
1034 } else {
1035 for package_id in diff.removed {
1036 let kind = PackageChangeKind::Removed;
1037 let is_member = None;
1038 let is_transitive = None;
1039 let change = Self {
1040 package_id,
1041 previous_id: None,
1042 kind,
1043 is_member,
1044 is_transitive,
1045 required_rust_version: None,
1046 };
1047 changes.insert(change.package_id, change);
1048 }
1049 for package_id in diff.added {
1050 let kind = PackageChangeKind::Added;
1051 let is_member = Some(member_ids.contains(&package_id));
1052 let is_transitive = Some(true);
1053 let change = Self {
1054 package_id,
1055 previous_id: None,
1056 kind,
1057 is_member,
1058 is_transitive,
1059 required_rust_version: None,
1060 };
1061 changes.insert(change.package_id, change);
1062 }
1063 }
1064 for package_id in diff.unchanged {
1065 let kind = PackageChangeKind::Unchanged;
1066 let is_member = Some(member_ids.contains(&package_id));
1067 let is_transitive = Some(true);
1068 let change = Self {
1069 package_id,
1070 previous_id: None,
1071 kind,
1072 is_member,
1073 is_transitive,
1074 required_rust_version: None,
1075 };
1076 changes.insert(change.package_id, change);
1077 }
1078 }
1079
1080 for member_id in &member_ids {
1081 let Some(change) = changes.get_mut(member_id) else {
1082 continue;
1083 };
1084 change.is_transitive = Some(false);
1085 for (direct_dep_id, _) in resolve.deps(*member_id) {
1086 let Some(change) = changes.get_mut(&direct_dep_id) else {
1087 continue;
1088 };
1089 change.is_transitive = Some(false);
1090 }
1091 }
1092
1093 changes
1094 }
1095
1096 fn alternatives_query(&self) -> Option<crate::core::dependency::Dependency> {
1098 if !self.package_id.source_id().is_registry() {
1099 return None;
1100 }
1101
1102 let query = crate::core::dependency::Dependency::parse(
1103 self.package_id.name(),
1104 None,
1105 self.package_id.source_id(),
1106 )
1107 .expect("already a valid dependency");
1108 Some(query)
1109 }
1110}
1111
1112impl std::fmt::Display for PackageChange {
1113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1114 let package_id = self.package_id;
1115 if let Some(previous_id) = self.previous_id {
1116 if package_id.source_id().is_git() {
1117 write!(
1118 f,
1119 "{previous_id} -> #{}",
1120 &package_id.source_id().precise_git_fragment().unwrap()[..8],
1121 )
1122 } else {
1123 write!(f, "{previous_id} -> v{}", package_id.version())
1124 }
1125 } else {
1126 write!(f, "{package_id}")
1127 }
1128 }
1129}
1130
1131#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1132enum PackageChangeKind {
1133 Added,
1134 Removed,
1135 Upgraded,
1136 Downgraded,
1137 Unchanged,
1138}
1139
1140impl PackageChangeKind {
1141 pub fn is_new(&self) -> bool {
1142 match self {
1143 Self::Added | Self::Upgraded | Self::Downgraded => true,
1144 Self::Removed | Self::Unchanged => false,
1145 }
1146 }
1147
1148 pub fn status(&self) -> &'static str {
1149 match self {
1150 Self::Added => "Adding",
1151 Self::Removed => "Removing",
1152 Self::Upgraded => "Updating",
1153 Self::Downgraded => "Downgrading",
1154 Self::Unchanged => "Unchanged",
1155 }
1156 }
1157
1158 pub fn style(&self) -> anstyle::Style {
1159 match self {
1160 Self::Added => style::UPDATE_ADDED,
1161 Self::Removed => style::UPDATE_REMOVED,
1162 Self::Upgraded => style::UPDATE_UPGRADED,
1163 Self::Downgraded => style::UPDATE_DOWNGRADED,
1164 Self::Unchanged => style::UPDATE_UNCHANGED,
1165 }
1166 }
1167}
1168
1169#[derive(Default, Clone, Debug)]
1171pub struct PackageDiff {
1172 removed: Vec<PackageId>,
1173 added: Vec<PackageId>,
1174 unchanged: Vec<PackageId>,
1175}
1176
1177impl PackageDiff {
1178 pub fn new(resolve: &Resolve) -> impl Iterator<Item = Self> {
1179 let mut changes = BTreeMap::new();
1180 let empty = Self::default();
1181 for dep in resolve.iter() {
1182 changes
1183 .entry(Self::key(dep))
1184 .or_insert_with(|| empty.clone())
1185 .added
1186 .push(dep);
1187 }
1188
1189 changes.into_iter().map(|(_, v)| v)
1190 }
1191
1192 pub fn diff(previous_resolve: &Resolve, resolve: &Resolve) -> impl Iterator<Item = Self> {
1193 fn vec_subset(a: &[PackageId], b: &[PackageId]) -> Vec<PackageId> {
1194 a.iter().filter(|a| !contains_id(b, a)).cloned().collect()
1195 }
1196
1197 fn vec_intersection(a: &[PackageId], b: &[PackageId]) -> Vec<PackageId> {
1198 a.iter().filter(|a| contains_id(b, a)).cloned().collect()
1199 }
1200
1201 fn contains_id(haystack: &[PackageId], needle: &PackageId) -> bool {
1207 let Ok(i) = haystack.binary_search(needle) else {
1208 return false;
1209 };
1210
1211 if needle.source_id().is_registry() {
1220 return true;
1221 }
1222 haystack[i..]
1223 .iter()
1224 .take_while(|b| &needle == b)
1225 .any(|b| needle.source_id().has_same_precise_as(b.source_id()))
1226 }
1227
1228 let mut changes = BTreeMap::new();
1230 let empty = Self::default();
1231 for dep in previous_resolve.iter() {
1232 changes
1233 .entry(Self::key(dep))
1234 .or_insert_with(|| empty.clone())
1235 .removed
1236 .push(dep);
1237 }
1238 for dep in resolve.iter() {
1239 changes
1240 .entry(Self::key(dep))
1241 .or_insert_with(|| empty.clone())
1242 .added
1243 .push(dep);
1244 }
1245
1246 for v in changes.values_mut() {
1247 let Self {
1248 removed: ref mut old,
1249 added: ref mut new,
1250 unchanged: ref mut other,
1251 } = *v;
1252 old.sort();
1253 new.sort();
1254 let removed = vec_subset(old, new);
1255 let added = vec_subset(new, old);
1256 let unchanged = vec_intersection(new, old);
1257 *old = removed;
1258 *new = added;
1259 *other = unchanged;
1260 }
1261 debug!("{:#?}", changes);
1262
1263 changes.into_iter().map(|(_, v)| v)
1264 }
1265
1266 fn key(dep: PackageId) -> (&'static str, SourceId) {
1267 (dep.name().as_str(), dep.source_id())
1268 }
1269
1270 pub fn change(&self) -> Option<(PackageId, PackageId)> {
1276 if self.removed.len() == 1 && self.added.len() == 1 {
1277 Some((self.removed[0], self.added[0]))
1278 } else {
1279 None
1280 }
1281 }
1282}
1283
1284fn annotate_required_rust_version(
1285 ws: &Workspace<'_>,
1286 resolve: &Resolve,
1287 changes: &mut IndexMap<PackageId, PackageChange>,
1288) {
1289 let rustc = ws.gctx().load_global_rustc(Some(ws)).ok();
1290 let rustc_version: Option<PartialVersion> =
1291 rustc.as_ref().map(|rustc| rustc.version.clone().into());
1292
1293 if ws.resolve_honors_rust_version() {
1294 let mut queue: std::collections::VecDeque<_> = ws
1295 .members()
1296 .map(|p| {
1297 (
1298 p.rust_version()
1299 .map(|r| r.to_partial())
1300 .or_else(|| rustc_version.clone()),
1301 p.package_id(),
1302 )
1303 })
1304 .collect();
1305 while let Some((required_rust_version, current_id)) = queue.pop_front() {
1306 let Some(required_rust_version) = required_rust_version else {
1307 continue;
1308 };
1309 if let Some(change) = changes.get_mut(¤t_id) {
1310 if let Some(existing) = change.required_rust_version.as_ref() {
1311 if *existing <= required_rust_version {
1312 continue;
1314 }
1315 }
1316 change.required_rust_version = Some(required_rust_version.clone());
1317 }
1318 queue.extend(
1319 resolve
1320 .deps(current_id)
1321 .map(|(dep, _)| (Some(required_rust_version.clone()), dep)),
1322 );
1323 }
1324 } else {
1325 for change in changes.values_mut() {
1326 change.required_rust_version = rustc_version.clone();
1327 }
1328 }
1329}