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