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