Skip to main content

cargo/ops/
cargo_update.rs

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