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