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