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