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