Skip to content

Improved consistency between breaking and non-breaking updates #14259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 26 additions & 13 deletions src/bin/cargo/commands/update.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use std::collections::HashMap;

use crate::command_prelude::*;

use anyhow::anyhow;
use cargo::ops::{self, UpdateOptions};
use cargo::util::print_available_packages;
use tracing::trace;

pub fn cli() -> Command {
subcommand("update")
Expand Down Expand Up @@ -92,28 +95,38 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
let update_opts = UpdateOptions {
recursive: args.flag("recursive"),
precise: args.get_one::<String>("precise").map(String::as_str),
breaking: args.flag("breaking"),
to_update,
dry_run: args.dry_run(),
workspace: args.flag("workspace"),
gctx,
};

if args.flag("breaking") {
gctx.cli_unstable()
.fail_if_stable_opt("--breaking", 12425)?;

let upgrades = ops::upgrade_manifests(&mut ws, &update_opts.to_update)?;
ops::resolve_ws(&ws, update_opts.dry_run)?;
ops::write_manifest_upgrades(&ws, &upgrades, update_opts.dry_run)?;
let breaking_update = update_opts.breaking; // or if doing a breaking precise update, coming in #14140.

if update_opts.dry_run {
update_opts
.gctx
.shell()
.warn("aborting update due to dry run")?;
// We are using the term "upgrade" here, which is the typical case, but it
// can also be a downgrade (in the case of a precise update). In general, it
// is a change to a version req matching a precise version.
let upgrades = if breaking_update {
if update_opts.breaking {
gctx.cli_unstable()
.fail_if_stable_opt("--breaking", 12425)?;
}

trace!("allowing breaking updates");
ops::upgrade_manifests(&mut ws, &update_opts.to_update)?
} else {
ops::update_lockfile(&ws, &update_opts)?;
HashMap::new()
};

ops::update_lockfile(&ws, &update_opts, &upgrades)?;
ops::write_manifest_upgrades(&ws, &upgrades, update_opts.dry_run)?;

if update_opts.dry_run {
update_opts
.gctx
.shell()
.warn("aborting update due to dry run")?;
}

Ok(())
Expand Down
68 changes: 58 additions & 10 deletions src/cargo/ops/cargo_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::ops;
use crate::sources::source::QueryKind;
use crate::util::cache_lock::CacheLockMode;
use crate::util::context::GlobalContext;
use crate::util::interning::InternedString;
use crate::util::toml_mut::dependency::{MaybeWorkspace, Source};
use crate::util::toml_mut::manifest::LocalManifest;
use crate::util::toml_mut::upgrade::upgrade_requirement;
Expand All @@ -20,12 +21,25 @@ use std::cmp::Ordering;
use std::collections::{BTreeMap, HashMap, HashSet};
use tracing::{debug, trace};

pub type UpgradeMap = HashMap<(String, SourceId), Version>;
/// A map of all breaking upgrades which is filled in by
/// upgrade_manifests/upgrade_dependency when going through workspace member
/// manifests, and later used by write_manifest_upgrades in order to know which
/// upgrades to write to manifest files on disk. Also used by update_lockfile to
/// know which dependencies to keep unchanged if any have been upgraded (we will
/// do either breaking or non-breaking updates, but not both).
pub type UpgradeMap = HashMap<
// The key is a package identifier consisting of the name and the source id.
(InternedString, SourceId),
// The value is the original version requirement before upgrade, and the
// upgraded version.
(VersionReq, Version),
>;

pub struct UpdateOptions<'a> {
pub gctx: &'a GlobalContext,
pub to_update: Vec<String>,
pub precise: Option<&'a str>,
pub breaking: bool,
pub recursive: bool,
pub dry_run: bool,
pub workspace: bool,
Expand All @@ -49,7 +63,11 @@ pub fn generate_lockfile(ws: &Workspace<'_>) -> CargoResult<()> {
Ok(())
}

pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoResult<()> {
pub fn update_lockfile(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ensuring more consistent output messages between breaking and non-breaking updates, as well as better error messages, as we are now running the same ops::update_lockfile in either case.

Reusing ops::update_lockfile is one way of improving these. Are there alternative designs we could use?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could duplicate update_lockfile and modify it to fit our needs, but the approach in this PR seems better to me. A significant chunk of the function is now conditional on breaking updates:

if opts.breaking {} else {}

ws: &Workspace<'_>,
opts: &UpdateOptions<'_>,
upgrades: &UpgradeMap,
) -> CargoResult<()> {
if opts.recursive && opts.precise.is_some() {
anyhow::bail!("cannot specify both recursive and precise simultaneously")
}
Expand Down Expand Up @@ -91,7 +109,38 @@ pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoRes
let mut registry = ws.package_registry()?;
let mut to_avoid = HashSet::new();

if opts.to_update.is_empty() {
if opts.breaking {
// We don't necessarily want to update all specified packages. If we are
// doing a breaking update (or precise upgrades, coming in #14140), we
// don't want to touch any packages that have no breaking updates. So we
// want to only avoid all packages that got upgraded.

for name in opts.to_update.iter() {
// We still want to query any specified package, for the sake of
// outputting errors if they don't exist.
previous_resolve.query(name)?;
}

for ((name, source_id), (version_req, _)) in upgrades.iter() {
if let Some(matching_dep) = previous_resolve.iter().find(|dep| {
dep.name() == *name
&& dep.source_id() == *source_id
&& version_req.matches(dep.version())
}) {
let spec = PackageIdSpec::new(name.to_string())
.with_url(source_id.url().clone())
.with_version(matching_dep.version().clone().into())
.to_string();
let pid = previous_resolve.query(&spec)?;
to_avoid.insert(pid);
} else {
// Should never happen
anyhow::bail!(
"no package named `{name}` with source `{source_id}` and version matching `{version_req}` in the previous lockfile",
)
}
}
} else if opts.to_update.is_empty() {
if !opts.workspace {
to_avoid.extend(previous_resolve.iter());
to_avoid.extend(previous_resolve.unused_patches());
Expand Down Expand Up @@ -185,11 +234,7 @@ pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoRes
opts.precise.is_some(),
&mut registry,
)?;
if opts.dry_run {
opts.gctx
.shell()
.warn("not updating lockfile due to dry run")?;
} else {
if !opts.dry_run {
ops::write_pkg_lockfile(ws, &mut resolve)?;
}
Ok(())
Expand Down Expand Up @@ -361,7 +406,10 @@ fn upgrade_dependency(
.status_with_color("Upgrading", &upgrade_message, &style::GOOD)?;
}

upgrades.insert((name.to_string(), dependency.source_id()), latest.clone());
upgrades.insert(
(name, dependency.source_id()),
(current.clone(), latest.clone()),
);

let req = OptVersionReq::Req(VersionReq::parse(&latest.to_string())?);
let mut dep = dependency.clone();
Expand Down Expand Up @@ -433,7 +481,7 @@ pub fn write_manifest_upgrades(
continue;
};

let Some(latest) = upgrades.get(&(name.to_owned(), source_id)) else {
let Some((_, latest)) = upgrades.get(&(name.into(), source_id)) else {
trace!("skipping dependency without an upgrade: {name}");
continue;
};
Expand Down
Loading