#
cargo/ops/
cargo_update.rs

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