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::shell::Verbosity;
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};
18use anyhow::Context as _;
19use cargo_util_schemas::core::PartialVersion;
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 = {
374        loop {
375            match registry.query_vec(&query, QueryKind::Exact) {
376                std::task::Poll::Ready(res) => {
377                    break res?;
378                }
379                std::task::Poll::Pending => registry.block_until_ready()?,
380            }
381        }
382    };
383
384    let latest = if !possibilities.is_empty() {
385        possibilities
386            .iter()
387            .map(|s| s.as_summary())
388            .map(|s| s.version())
389            .filter(|v| !v.is_prerelease())
390            .max()
391    } else {
392        None
393    };
394
395    let Some(latest) = latest else {
396        trace!("skipping dependency `{name}` without any published versions");
397        return Ok(dependency);
398    };
399
400    if current.matches(&latest) {
401        trace!("skipping dependency `{name}` without a breaking update available");
402        return Ok(dependency);
403    }
404
405    let Some((new_req_string, _)) = upgrade_requirement(&current.to_string(), latest)? else {
406        trace!("skipping dependency `{name}` because the version requirement didn't change");
407        return Ok(dependency);
408    };
409
410    let upgrade_message = format!("{name} {current} -> {new_req_string}");
411    trace!(upgrade_message);
412
413    if upgrade_messages.insert(upgrade_message.clone()) {
414        gctx.shell()
415            .status_with_color("Upgrading", &upgrade_message, &style::GOOD)?;
416    }
417
418    upgrades.insert((name.to_string(), dependency.source_id()), latest.clone());
419
420    // Remove this spec from remaining_specs since we successfully upgraded it
421    remaining_specs
422        .retain(|spec| !(spec.name() == name.as_str() && dependency.source_id().is_registry()));
423
424    let req = OptVersionReq::Req(VersionReq::parse(&latest.to_string())?);
425    let mut dep = dependency.clone();
426    dep.set_version_req(req);
427    Ok(dep)
428}
429
430/// Update manifests with upgraded versions, and write to disk. Based on
431/// cargo-edit. Returns true if any file has changed.
432///
433/// Some of the checks here are duplicating checks already done in
434/// `upgrade_manifests/upgrade_dependency`. Why? Let's say `upgrade_dependency` has
435/// found that dependency foo was eligible for an upgrade. But foo can occur in
436/// multiple manifest files, and even multiple times in the same manifest file,
437/// and may be pinned, renamed, etc. in some of the instances. So we still need
438/// to check here which dependencies to actually modify. So why not drop the
439/// upgrade map and redo all checks here? Because then we'd have to query the
440/// registries again to find the latest versions.
441pub fn write_manifest_upgrades(
442    ws: &Workspace<'_>,
443    upgrades: &UpgradeMap,
444    dry_run: bool,
445) -> CargoResult<bool> {
446    if upgrades.is_empty() {
447        return Ok(false);
448    }
449
450    let mut any_file_has_changed = false;
451
452    let items = std::iter::once((ws.root_manifest(), ws.unstable_features()))
453        .chain(ws.members().map(|member| {
454            (
455                member.manifest_path(),
456                member.manifest().unstable_features(),
457            )
458        }))
459        .collect::<Vec<_>>();
460
461    for (manifest_path, unstable_features) in items {
462        trace!("updating TOML manifest at `{manifest_path:?}` with upgraded dependencies");
463
464        let crate_root = manifest_path
465            .parent()
466            .expect("manifest path is absolute")
467            .to_owned();
468
469        let mut local_manifest = LocalManifest::try_new(&manifest_path)?;
470        let mut manifest_has_changed = false;
471
472        for dep_table in local_manifest.get_dependency_tables_mut() {
473            for (mut dep_key, dep_item) in dep_table.iter_mut() {
474                let dep_key_str = dep_key.get();
475                let dependency = crate::util::toml_mut::dependency::Dependency::from_toml(
476                    ws.gctx(),
477                    ws.root(),
478                    &manifest_path,
479                    unstable_features,
480                    dep_key_str,
481                    dep_item,
482                )?;
483                let name = &dependency.name;
484
485                if let Some(renamed_to) = dependency.rename {
486                    trace!("skipping dependency renamed from `{name}` to `{renamed_to}`");
487                    continue;
488                }
489
490                let Some(current) = dependency.version() else {
491                    trace!("skipping dependency without a version: {name}");
492                    continue;
493                };
494
495                let (MaybeWorkspace::Other(source_id), Some(Source::Registry(source))) =
496                    (dependency.source_id(ws.gctx())?, dependency.source())
497                else {
498                    trace!("skipping non-registry dependency: {name}");
499                    continue;
500                };
501
502                let Some(latest) = upgrades.get(&(name.to_owned(), source_id)) else {
503                    trace!("skipping dependency without an upgrade: {name}");
504                    continue;
505                };
506
507                let Some((new_req_string, new_req)) = upgrade_requirement(current, latest)? else {
508                    trace!(
509                        "skipping dependency `{name}` because the version requirement didn't change"
510                    );
511                    continue;
512                };
513
514                let [comparator] = &new_req.comparators[..] else {
515                    trace!(
516                        "skipping dependency `{}` with multiple version comparators: {:?}",
517                        name, new_req.comparators
518                    );
519                    continue;
520                };
521
522                if comparator.op != Op::Caret {
523                    trace!("skipping non-caret dependency `{}`: {}", name, comparator);
524                    continue;
525                }
526
527                let mut dep = dependency.clone();
528                let mut source = source.clone();
529                source.version = new_req_string;
530                dep.source = Some(Source::Registry(source));
531
532                trace!("upgrading dependency {name}");
533                dep.update_toml(
534                    ws.gctx(),
535                    ws.root(),
536                    &crate_root,
537                    unstable_features,
538                    &mut dep_key,
539                    dep_item,
540                )?;
541                manifest_has_changed = true;
542                any_file_has_changed = true;
543            }
544        }
545
546        if manifest_has_changed && !dry_run {
547            debug!("writing upgraded manifest to {}", manifest_path.display());
548            local_manifest.write()?;
549        }
550    }
551
552    Ok(any_file_has_changed)
553}
554
555fn print_lockfile_generation(
556    ws: &Workspace<'_>,
557    resolve: &Resolve,
558    registry: &mut PackageRegistry<'_>,
559) -> CargoResult<()> {
560    let mut changes = PackageChange::new(ws, resolve);
561    let num_pkgs: usize = changes
562        .values()
563        .filter(|change| change.kind.is_new() && !change.is_member.unwrap_or(false))
564        .count();
565    if num_pkgs == 0 {
566        // nothing worth reporting
567        return Ok(());
568    }
569    annotate_required_rust_version(ws, resolve, &mut changes);
570
571    status_locking(ws, num_pkgs)?;
572    for change in changes.values() {
573        if change.is_member.unwrap_or(false) {
574            continue;
575        };
576        match change.kind {
577            PackageChangeKind::Added => {
578                let possibilities = if let Some(query) = change.alternatives_query() {
579                    loop {
580                        match registry.query_vec(&query, QueryKind::Exact) {
581                            std::task::Poll::Ready(res) => {
582                                break res?;
583                            }
584                            std::task::Poll::Pending => registry.block_until_ready()?,
585                        }
586                    }
587                } else {
588                    vec![]
589                };
590
591                let required_rust_version = report_required_rust_version(resolve, change);
592                let latest = report_latest(&possibilities, change);
593                let note = required_rust_version.or(latest);
594
595                if let Some(note) = note {
596                    ws.gctx().shell().status_with_color(
597                        change.kind.status(),
598                        format!("{change}{note}"),
599                        &change.kind.style(),
600                    )?;
601                }
602            }
603            PackageChangeKind::Upgraded
604            | PackageChangeKind::Downgraded
605            | PackageChangeKind::Removed
606            | PackageChangeKind::Unchanged => {
607                unreachable!("without a previous resolve, everything should be added")
608            }
609        }
610    }
611
612    Ok(())
613}
614
615fn print_lockfile_sync(
616    ws: &Workspace<'_>,
617    previous_resolve: &Resolve,
618    resolve: &Resolve,
619    registry: &mut PackageRegistry<'_>,
620) -> CargoResult<()> {
621    let mut changes = PackageChange::diff(ws, previous_resolve, resolve);
622    let num_pkgs: usize = changes
623        .values()
624        .filter(|change| change.kind.is_new() && !change.is_member.unwrap_or(false))
625        .count();
626    if num_pkgs == 0 {
627        // nothing worth reporting
628        return Ok(());
629    }
630    annotate_required_rust_version(ws, resolve, &mut changes);
631
632    status_locking(ws, num_pkgs)?;
633    for change in changes.values() {
634        if change.is_member.unwrap_or(false) {
635            continue;
636        };
637        match change.kind {
638            PackageChangeKind::Added
639            | PackageChangeKind::Upgraded
640            | PackageChangeKind::Downgraded => {
641                let possibilities = if let Some(query) = change.alternatives_query() {
642                    loop {
643                        match registry.query_vec(&query, QueryKind::Exact) {
644                            std::task::Poll::Ready(res) => {
645                                break res?;
646                            }
647                            std::task::Poll::Pending => registry.block_until_ready()?,
648                        }
649                    }
650                } else {
651                    vec![]
652                };
653
654                let required_rust_version = report_required_rust_version(resolve, change);
655                let latest = report_latest(&possibilities, change);
656                let note = required_rust_version.or(latest).unwrap_or_default();
657
658                ws.gctx().shell().status_with_color(
659                    change.kind.status(),
660                    format!("{change}{note}"),
661                    &change.kind.style(),
662                )?;
663            }
664            PackageChangeKind::Removed | PackageChangeKind::Unchanged => {}
665        }
666    }
667
668    Ok(())
669}
670
671fn print_lockfile_updates(
672    ws: &Workspace<'_>,
673    previous_resolve: &Resolve,
674    resolve: &Resolve,
675    precise: bool,
676    registry: &mut PackageRegistry<'_>,
677) -> CargoResult<()> {
678    let mut changes = PackageChange::diff(ws, previous_resolve, resolve);
679    let num_pkgs: usize = changes
680        .values()
681        .filter(|change| change.kind.is_new())
682        .count();
683    annotate_required_rust_version(ws, resolve, &mut changes);
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            loop {
692                match registry.query_vec(&query, QueryKind::Exact) {
693                    std::task::Poll::Ready(res) => {
694                        break res?;
695                    }
696                    std::task::Poll::Pending => registry.block_until_ready()?,
697                }
698            }
699        } else {
700            vec![]
701        };
702
703        match change.kind {
704            PackageChangeKind::Added
705            | PackageChangeKind::Upgraded
706            | PackageChangeKind::Downgraded => {
707                let required_rust_version = report_required_rust_version(resolve, change);
708                let latest = report_latest(&possibilities, change);
709                let note = required_rust_version.or(latest).unwrap_or_default();
710
711                ws.gctx().shell().status_with_color(
712                    change.kind.status(),
713                    format!("{change}{note}"),
714                    &change.kind.style(),
715                )?;
716            }
717            PackageChangeKind::Removed => {
718                ws.gctx().shell().status_with_color(
719                    change.kind.status(),
720                    format!("{change}"),
721                    &change.kind.style(),
722                )?;
723            }
724            PackageChangeKind::Unchanged => {
725                let required_rust_version = report_required_rust_version(resolve, change);
726                let latest = report_latest(&possibilities, change);
727                let note = required_rust_version.as_deref().or(latest.as_deref());
728
729                if let Some(note) = note {
730                    if latest.is_some() {
731                        unchanged_behind += 1;
732                    }
733                    if ws.gctx().shell().verbosity() == Verbosity::Verbose {
734                        ws.gctx().shell().status_with_color(
735                            change.kind.status(),
736                            format!("{change}{note}"),
737                            &change.kind.style(),
738                        )?;
739                    }
740                }
741            }
742        }
743    }
744
745    if ws.gctx().shell().verbosity() == Verbosity::Verbose {
746        ws.gctx()
747            .shell()
748            .note("to see how you depend on a package, run `cargo tree --invert <dep>@<ver>`")?;
749    } else {
750        if 0 < unchanged_behind {
751            ws.gctx().shell().note(format!(
752                "pass `--verbose` to see {unchanged_behind} unchanged dependencies behind latest"
753            ))?;
754        }
755    }
756
757    Ok(())
758}
759
760fn status_locking(ws: &Workspace<'_>, num_pkgs: usize) -> CargoResult<()> {
761    use std::fmt::Write as _;
762
763    let plural = if num_pkgs == 1 { "" } else { "s" };
764
765    let mut cfg = String::new();
766    // Don't have a good way to describe `direct_minimal_versions` atm
767    if !ws.gctx().cli_unstable().direct_minimal_versions {
768        write!(&mut cfg, " to")?;
769        if ws.gctx().cli_unstable().minimal_versions {
770            write!(&mut cfg, " earliest")?;
771        } else {
772            write!(&mut cfg, " latest")?;
773        }
774
775        if let Some(rust_version) = required_rust_version(ws) {
776            write!(&mut cfg, " Rust {rust_version}")?;
777        }
778        write!(&mut cfg, " compatible version{plural}")?;
779        if let Some(publish_time) = ws.resolve_publish_time() {
780            write!(&mut cfg, " as of {publish_time}")?;
781        }
782    }
783
784    ws.gctx()
785        .shell()
786        .status("Locking", format!("{num_pkgs} package{plural}{cfg}"))?;
787    Ok(())
788}
789
790fn required_rust_version(ws: &Workspace<'_>) -> Option<PartialVersion> {
791    if !ws.resolve_honors_rust_version() {
792        return None;
793    }
794
795    if let Some(ver) = ws.lowest_rust_version() {
796        Some(ver.clone().into_partial())
797    } else {
798        let rustc = ws.gctx().load_global_rustc(Some(ws)).ok()?;
799        let rustc_version = rustc.version.clone().into();
800        Some(rustc_version)
801    }
802}
803
804fn report_required_rust_version(resolve: &Resolve, change: &PackageChange) -> Option<String> {
805    if change.package_id.source_id().is_path() {
806        return None;
807    }
808    let summary = resolve.summary(change.package_id);
809    let package_rust_version = summary.rust_version()?;
810    let required_rust_version = change.required_rust_version.as_ref()?;
811    if package_rust_version.is_compatible_with(required_rust_version) {
812        return None;
813    }
814
815    let error = style::ERROR;
816    Some(format!(
817        " {error}(requires Rust {package_rust_version}){error:#}"
818    ))
819}
820
821fn report_latest(possibilities: &[IndexSummary], change: &PackageChange) -> Option<String> {
822    let package_id = change.package_id;
823    if !package_id.source_id().is_registry() {
824        return None;
825    }
826
827    let version_req = package_id.version().to_caret_req();
828    let required_rust_version = change.required_rust_version.as_ref();
829
830    let compat_ver_compat_msrv_summary = possibilities
831        .iter()
832        .map(|s| s.as_summary())
833        .filter(|s| {
834            if let (Some(summary_rust_version), Some(required_rust_version)) =
835                (s.rust_version(), required_rust_version)
836            {
837                summary_rust_version.is_compatible_with(required_rust_version)
838            } else {
839                true
840            }
841        })
842        .filter(|s| package_id.version() != s.version() && version_req.matches(s.version()))
843        .max_by_key(|s| s.version());
844    if let Some(summary) = compat_ver_compat_msrv_summary {
845        let warn = style::WARN;
846        let version = summary.version();
847        let report = format!(" {warn}(available: v{version}){warn:#}");
848        return Some(report);
849    }
850
851    if !change.is_transitive.unwrap_or(true) {
852        let incompat_ver_compat_msrv_summary = possibilities
853            .iter()
854            .map(|s| s.as_summary())
855            .filter(|s| {
856                if let (Some(summary_rust_version), Some(required_rust_version)) =
857                    (s.rust_version(), required_rust_version)
858                {
859                    summary_rust_version.is_compatible_with(required_rust_version)
860                } else {
861                    true
862                }
863            })
864            .filter(|s| is_latest(s.version(), package_id.version()))
865            .max_by_key(|s| s.version());
866        if let Some(summary) = incompat_ver_compat_msrv_summary {
867            let warn = style::WARN;
868            let version = summary.version();
869            let report = format!(" {warn}(available: v{version}){warn:#}");
870            return Some(report);
871        }
872    }
873
874    let compat_ver_summary = possibilities
875        .iter()
876        .map(|s| s.as_summary())
877        .filter(|s| package_id.version() != s.version() && version_req.matches(s.version()))
878        .max_by_key(|s| s.version());
879    if let Some(summary) = compat_ver_summary {
880        let msrv_note = summary
881            .rust_version()
882            .map(|rv| format!(", requires Rust {rv}"))
883            .unwrap_or_default();
884        let warn = style::NOP;
885        let version = summary.version();
886        let report = format!(" {warn}(available: v{version}{msrv_note}){warn:#}");
887        return Some(report);
888    }
889
890    if !change.is_transitive.unwrap_or(true) {
891        let incompat_ver_summary = possibilities
892            .iter()
893            .map(|s| s.as_summary())
894            .filter(|s| is_latest(s.version(), package_id.version()))
895            .max_by_key(|s| s.version());
896        if let Some(summary) = incompat_ver_summary {
897            let msrv_note = summary
898                .rust_version()
899                .map(|rv| format!(", requires Rust {rv}"))
900                .unwrap_or_default();
901            let warn = style::NOP;
902            let version = summary.version();
903            let report = format!(" {warn}(available: v{version}{msrv_note}){warn:#}");
904            return Some(report);
905        }
906    }
907
908    None
909}
910
911fn is_latest(candidate: &semver::Version, current: &semver::Version) -> bool {
912    current < candidate
913                // Only match pre-release if major.minor.patch are the same
914                && (candidate.pre.is_empty()
915                    || (candidate.major == current.major
916                        && candidate.minor == current.minor
917                        && candidate.patch == current.patch))
918}
919
920fn fill_with_deps<'a>(
921    resolve: &'a Resolve,
922    dep: PackageId,
923    set: &mut HashSet<PackageId>,
924    visited: &mut HashSet<PackageId>,
925) {
926    if !visited.insert(dep) {
927        return;
928    }
929    set.insert(dep);
930    for (dep, _) in resolve.deps_not_replaced(dep) {
931        fill_with_deps(resolve, dep, set, visited);
932    }
933}
934
935#[derive(Clone, Debug)]
936struct PackageChange {
937    package_id: PackageId,
938    previous_id: Option<PackageId>,
939    kind: PackageChangeKind,
940    is_member: Option<bool>,
941    is_transitive: Option<bool>,
942    required_rust_version: Option<PartialVersion>,
943}
944
945impl PackageChange {
946    pub fn new(ws: &Workspace<'_>, resolve: &Resolve) -> IndexMap<PackageId, Self> {
947        let diff = PackageDiff::new(resolve);
948        Self::with_diff(diff, ws, resolve)
949    }
950
951    pub fn diff(
952        ws: &Workspace<'_>,
953        previous_resolve: &Resolve,
954        resolve: &Resolve,
955    ) -> IndexMap<PackageId, Self> {
956        let diff = PackageDiff::diff(previous_resolve, resolve);
957        Self::with_diff(diff, ws, resolve)
958    }
959
960    fn with_diff(
961        diff: impl Iterator<Item = PackageDiff>,
962        ws: &Workspace<'_>,
963        resolve: &Resolve,
964    ) -> IndexMap<PackageId, Self> {
965        let member_ids: HashSet<_> = ws.members().map(|p| p.package_id()).collect();
966
967        let mut changes = IndexMap::new();
968        for diff in diff {
969            if let Some((previous_id, package_id)) = diff.change() {
970                // If versions differ only in build metadata, we call it an "update"
971                // regardless of whether the build metadata has gone up or down.
972                // This metadata is often stuff like git commit hashes, which are
973                // not meaningfully ordered.
974                let kind = if previous_id.version().cmp_precedence(package_id.version())
975                    == Ordering::Greater
976                {
977                    PackageChangeKind::Downgraded
978                } else {
979                    PackageChangeKind::Upgraded
980                };
981                let is_member = Some(member_ids.contains(&package_id));
982                let is_transitive = Some(true);
983                let change = Self {
984                    package_id,
985                    previous_id: Some(previous_id),
986                    kind,
987                    is_member,
988                    is_transitive,
989                    required_rust_version: None,
990                };
991                changes.insert(change.package_id, change);
992            } else {
993                for package_id in diff.removed {
994                    let kind = PackageChangeKind::Removed;
995                    let is_member = None;
996                    let is_transitive = None;
997                    let change = Self {
998                        package_id,
999                        previous_id: None,
1000                        kind,
1001                        is_member,
1002                        is_transitive,
1003                        required_rust_version: None,
1004                    };
1005                    changes.insert(change.package_id, change);
1006                }
1007                for package_id in diff.added {
1008                    let kind = PackageChangeKind::Added;
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            for package_id in diff.unchanged {
1023                let kind = PackageChangeKind::Unchanged;
1024                let is_member = Some(member_ids.contains(&package_id));
1025                let is_transitive = Some(true);
1026                let change = Self {
1027                    package_id,
1028                    previous_id: None,
1029                    kind,
1030                    is_member,
1031                    is_transitive,
1032                    required_rust_version: None,
1033                };
1034                changes.insert(change.package_id, change);
1035            }
1036        }
1037
1038        for member_id in &member_ids {
1039            let Some(change) = changes.get_mut(member_id) else {
1040                continue;
1041            };
1042            change.is_transitive = Some(false);
1043            for (direct_dep_id, _) in resolve.deps(*member_id) {
1044                let Some(change) = changes.get_mut(&direct_dep_id) else {
1045                    continue;
1046                };
1047                change.is_transitive = Some(false);
1048            }
1049        }
1050
1051        changes
1052    }
1053
1054    /// For querying [`PackageRegistry`] for alternative versions to report to the user
1055    fn alternatives_query(&self) -> Option<crate::core::dependency::Dependency> {
1056        if !self.package_id.source_id().is_registry() {
1057            return None;
1058        }
1059
1060        let query = crate::core::dependency::Dependency::parse(
1061            self.package_id.name(),
1062            None,
1063            self.package_id.source_id(),
1064        )
1065        .expect("already a valid dependency");
1066        Some(query)
1067    }
1068}
1069
1070impl std::fmt::Display for PackageChange {
1071    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1072        let package_id = self.package_id;
1073        if let Some(previous_id) = self.previous_id {
1074            if package_id.source_id().is_git() {
1075                write!(
1076                    f,
1077                    "{previous_id} -> #{}",
1078                    &package_id.source_id().precise_git_fragment().unwrap()[..8],
1079                )
1080            } else {
1081                write!(f, "{previous_id} -> v{}", package_id.version())
1082            }
1083        } else {
1084            write!(f, "{package_id}")
1085        }
1086    }
1087}
1088
1089#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1090enum PackageChangeKind {
1091    Added,
1092    Removed,
1093    Upgraded,
1094    Downgraded,
1095    Unchanged,
1096}
1097
1098impl PackageChangeKind {
1099    pub fn is_new(&self) -> bool {
1100        match self {
1101            Self::Added | Self::Upgraded | Self::Downgraded => true,
1102            Self::Removed | Self::Unchanged => false,
1103        }
1104    }
1105
1106    pub fn status(&self) -> &'static str {
1107        match self {
1108            Self::Added => "Adding",
1109            Self::Removed => "Removing",
1110            Self::Upgraded => "Updating",
1111            Self::Downgraded => "Downgrading",
1112            Self::Unchanged => "Unchanged",
1113        }
1114    }
1115
1116    pub fn style(&self) -> anstyle::Style {
1117        match self {
1118            Self::Added => style::UPDATE_ADDED,
1119            Self::Removed => style::UPDATE_REMOVED,
1120            Self::Upgraded => style::UPDATE_UPGRADED,
1121            Self::Downgraded => style::UPDATE_DOWNGRADED,
1122            Self::Unchanged => style::UPDATE_UNCHANGED,
1123        }
1124    }
1125}
1126
1127/// All resolved versions of a package name within a [`SourceId`]
1128#[derive(Default, Clone, Debug)]
1129pub struct PackageDiff {
1130    removed: Vec<PackageId>,
1131    added: Vec<PackageId>,
1132    unchanged: Vec<PackageId>,
1133}
1134
1135impl PackageDiff {
1136    pub fn new(resolve: &Resolve) -> impl Iterator<Item = Self> {
1137        let mut changes = BTreeMap::new();
1138        let empty = Self::default();
1139        for dep in resolve.iter() {
1140            changes
1141                .entry(Self::key(dep))
1142                .or_insert_with(|| empty.clone())
1143                .added
1144                .push(dep);
1145        }
1146
1147        changes.into_iter().map(|(_, v)| v)
1148    }
1149
1150    pub fn diff(previous_resolve: &Resolve, resolve: &Resolve) -> impl Iterator<Item = Self> {
1151        fn vec_subset(a: &[PackageId], b: &[PackageId]) -> Vec<PackageId> {
1152            a.iter().filter(|a| !contains_id(b, a)).cloned().collect()
1153        }
1154
1155        fn vec_intersection(a: &[PackageId], b: &[PackageId]) -> Vec<PackageId> {
1156            a.iter().filter(|a| contains_id(b, a)).cloned().collect()
1157        }
1158
1159        // Check if a PackageId is present `b` from `a`.
1160        //
1161        // Note that this is somewhat more complicated because the equality for source IDs does not
1162        // take precise versions into account (e.g., git shas), but we want to take that into
1163        // account here.
1164        fn contains_id(haystack: &[PackageId], needle: &PackageId) -> bool {
1165            let Ok(i) = haystack.binary_search(needle) else {
1166                return false;
1167            };
1168
1169            // If we've found `a` in `b`, then we iterate over all instances
1170            // (we know `b` is sorted) and see if they all have different
1171            // precise versions. If so, then `a` isn't actually in `b` so
1172            // we'll let it through.
1173            //
1174            // Note that we only check this for non-registry sources,
1175            // however, as registries contain enough version information in
1176            // the package ID to disambiguate.
1177            if needle.source_id().is_registry() {
1178                return true;
1179            }
1180            haystack[i..]
1181                .iter()
1182                .take_while(|b| &needle == b)
1183                .any(|b| needle.source_id().has_same_precise_as(b.source_id()))
1184        }
1185
1186        // Map `(package name, package source)` to `(removed versions, added versions)`.
1187        let mut changes = BTreeMap::new();
1188        let empty = Self::default();
1189        for dep in previous_resolve.iter() {
1190            changes
1191                .entry(Self::key(dep))
1192                .or_insert_with(|| empty.clone())
1193                .removed
1194                .push(dep);
1195        }
1196        for dep in resolve.iter() {
1197            changes
1198                .entry(Self::key(dep))
1199                .or_insert_with(|| empty.clone())
1200                .added
1201                .push(dep);
1202        }
1203
1204        for v in changes.values_mut() {
1205            let Self {
1206                removed: ref mut old,
1207                added: ref mut new,
1208                unchanged: ref mut other,
1209            } = *v;
1210            old.sort();
1211            new.sort();
1212            let removed = vec_subset(old, new);
1213            let added = vec_subset(new, old);
1214            let unchanged = vec_intersection(new, old);
1215            *old = removed;
1216            *new = added;
1217            *other = unchanged;
1218        }
1219        debug!("{:#?}", changes);
1220
1221        changes.into_iter().map(|(_, v)| v)
1222    }
1223
1224    fn key(dep: PackageId) -> (&'static str, SourceId) {
1225        (dep.name().as_str(), dep.source_id())
1226    }
1227
1228    /// Guess if a package upgraded/downgraded
1229    ///
1230    /// All `PackageDiff` knows is that entries were added/removed within [`Resolve`].
1231    /// A package could be added or removed because of dependencies from other packages
1232    /// which makes it hard to definitively say "X was upgrade to N".
1233    pub fn change(&self) -> Option<(PackageId, PackageId)> {
1234        if self.removed.len() == 1 && self.added.len() == 1 {
1235            Some((self.removed[0], self.added[0]))
1236        } else {
1237            None
1238        }
1239    }
1240}
1241
1242fn annotate_required_rust_version(
1243    ws: &Workspace<'_>,
1244    resolve: &Resolve,
1245    changes: &mut IndexMap<PackageId, PackageChange>,
1246) {
1247    let rustc = ws.gctx().load_global_rustc(Some(ws)).ok();
1248    let rustc_version: Option<PartialVersion> =
1249        rustc.as_ref().map(|rustc| rustc.version.clone().into());
1250
1251    if ws.resolve_honors_rust_version() {
1252        let mut queue: std::collections::VecDeque<_> = ws
1253            .members()
1254            .map(|p| {
1255                (
1256                    p.rust_version()
1257                        .map(|r| r.clone().into_partial())
1258                        .or_else(|| rustc_version.clone()),
1259                    p.package_id(),
1260                )
1261            })
1262            .collect();
1263        while let Some((required_rust_version, current_id)) = queue.pop_front() {
1264            let Some(required_rust_version) = required_rust_version else {
1265                continue;
1266            };
1267            if let Some(change) = changes.get_mut(&current_id) {
1268                if let Some(existing) = change.required_rust_version.as_ref() {
1269                    if *existing <= required_rust_version {
1270                        // Stop early; we already walked down this path with a better match
1271                        continue;
1272                    }
1273                }
1274                change.required_rust_version = Some(required_rust_version.clone());
1275            }
1276            queue.extend(
1277                resolve
1278                    .deps(current_id)
1279                    .map(|(dep, _)| (Some(required_rust_version.clone()), dep)),
1280            );
1281        }
1282    } else {
1283        for change in changes.values_mut() {
1284            change.required_rust_version = rustc_version.clone();
1285        }
1286    }
1287}