#
cargo/ops/
cargo_update.rs

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