Skip to content
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
74 changes: 55 additions & 19 deletions src/cargo/ops/cargo_add/crate_spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ use crate::CargoResult;
use crate::util::toml_mut::dependency::RegistrySource;
use cargo_util_schemas::manifest::PackageName;

/// A user-provided version selector from `<name>@<value>`.
#[derive(Debug)]
pub(super) enum VersionSpec {
/// A semver requirement that can be written to the manifest.
Requirement(String),
/// The special `@latest` selector, used for diagnostics only.
Latest,
}

impl std::fmt::Display for VersionSpec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Requirement(req) => req.fmt(f),
Self::Latest => "latest".fmt(f),
}
}
}

/// User-specified crate
///
/// This can be a
Expand All @@ -16,8 +34,8 @@ use cargo_util_schemas::manifest::PackageName;
pub struct CrateSpec {
/// Crate name
name: String,
/// Optional version requirement
version_req: Option<String>,
/// Optional version selector
version: Option<VersionSpec>,
}

impl CrateSpec {
Expand Down Expand Up @@ -46,22 +64,34 @@ impl CrateSpec {

package_name?;

if let Some(version) = version {
semver::VersionReq::parse(version).with_context(|| {
if let Some(stripped) = version.strip_prefix("v") {
return format!(
"the version provided, `{version}` is not a \
valid SemVer requirement\n\n\
help: changing the package to `{name}@{stripped}`",
);
}
format!("invalid version requirement `{version}`")
})?;
}
let version = if let Some(version) = version {
// `latest` is the only supported special version selector. It is
// not a SemVer requirement.
//
// We intentionally keep it case-sensitive to match other package
// managers we may be helping users transition from.
if version == "latest" {
Comment thread
0xPoe marked this conversation as resolved.
Some(VersionSpec::Latest)
} else {
semver::VersionReq::parse(version).with_context(|| {
if let Some(stripped) = version.strip_prefix("v") {
return format!(
"the version provided, `{version}` is not a \
valid SemVer requirement\n\n\
help: changing the package to `{name}@{stripped}`",
);
}
format!("invalid version requirement `{version}`")
})?;
Some(VersionSpec::Requirement(version.to_owned()))
}
} else {
None
};

let id = Self {
name: name.to_owned(),
version_req: version.map(|s| s.to_owned()),
version,
};

Ok(id)
Expand All @@ -70,8 +100,14 @@ impl CrateSpec {
/// Generate a dependency entry for this crate specifier
pub fn to_dependency(&self) -> CargoResult<Dependency> {
let mut dep = Dependency::new(self.name());
if let Some(version_req) = self.version_req() {
dep = dep.set_source(RegistrySource::new(version_req));
match self.version.as_ref() {
Some(VersionSpec::Latest) => {
anyhow::bail!("`latest` is not a valid dependency requirement")
}
Some(VersionSpec::Requirement(req)) => {
dep = dep.set_source(RegistrySource::new(req));
}
None => {}
}
Ok(dep)
}
Expand All @@ -80,7 +116,7 @@ impl CrateSpec {
&self.name
}

pub fn version_req(&self) -> Option<&str> {
self.version_req.as_deref()
pub(crate) fn version(&self) -> Option<&VersionSpec> {
self.version.as_ref()
}
}
65 changes: 60 additions & 5 deletions src/cargo/ops/cargo_add/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ use crate::util::toml_mut::dependency::WorkspaceSource;
use crate::util::toml_mut::manifest::DepTable;
use crate::util::toml_mut::manifest::LocalManifest;
use crate_spec::CrateSpec;
use crate_spec::VersionSpec;

const MAX_FEATURE_PRINTS: usize = 30;

Expand Down Expand Up @@ -349,6 +350,10 @@ fn resolve_dependency(
.as_deref()
.map(CrateSpec::resolve)
.transpose()?;
let request_latest = crate_spec
.as_ref()
.is_some_and(|crate_spec| matches!(crate_spec.version(), Some(VersionSpec::Latest)));
Comment on lines +353 to +355

@epage epage Apr 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

how come you pulled this out up here?

View changes since the review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It is used in the following if-else statement.


let mut selected_dep = if let Some(url) = &arg.git {
let mut src = GitSource::new(url);
if let Some(branch) = &arg.branch {
Expand All @@ -362,9 +367,9 @@ fn resolve_dependency(
}

let selected = if let Some(crate_spec) = &crate_spec {
if let Some(v) = crate_spec.version_req() {
if let Some(version) = crate_spec.version() {
// crate specifier includes a version (e.g. `docopt@0.8`)
anyhow::bail!("cannot specify a git URL (`{url}`) with a version (`{v}`).");
anyhow::bail!("cannot specify a git URL (`{url}`) with a version (`{version}`).");
}
let dependency = crate_spec.to_dependency()?.set_source(src);
let selected = select_package(&dependency, gctx, registry)?;
Expand Down Expand Up @@ -399,9 +404,9 @@ fn resolve_dependency(
}

let selected = if let Some(crate_spec) = &crate_spec {
if let Some(v) = crate_spec.version_req() {
if let Some(version) = crate_spec.version() {
// crate specifier includes a version (e.g. `docopt@0.8`)
anyhow::bail!("cannot specify a path (`{raw_path}`) with a version (`{v}`).");
anyhow::bail!("cannot specify a path (`{raw_path}`) with a version (`{version}`).");
}
let dependency = crate_spec.to_dependency()?.set_source(src);
let selected = select_package(&dependency, gctx, registry)?;
Expand All @@ -423,7 +428,14 @@ fn resolve_dependency(
};
selected
} else if let Some(crate_spec) = &crate_spec {
crate_spec.to_dependency()?
if request_latest {
// `latest` is not a dependency requirement we can write to the manifest.
// Build an unconstrained dependency and let the dedicated diagnostics below
// explain what the user should do instead.
Dependency::new(crate_spec.name())
} else {
crate_spec.to_dependency()?
}
} else {
anyhow::bail!("dependency name is required");
};
Expand Down Expand Up @@ -513,6 +525,49 @@ fn resolve_dependency(
dependency = dependency.clear_version();
}

// Check if user tried to use @latest and provide helpful error.
if request_latest {
// The diagnostics below compare against the resolved and latest published registry
// versions, so they only apply to registry dependencies.
if !matches!(dependency.source(), Some(Source::Registry(_))) {
anyhow::bail!("invalid version requirement `latest`");
}

// Get the exact version that `cargo add <name>` would resolve to,
// respecting MSRV and existing version constraints.
Comment on lines +536 to +537

@epage epage Apr 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What other constraints are there?

View changes since the review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I believe it includes:

  • any existing version requirements
  • the selected registry or source
  • registry resolution behavior (path, yanked filtering, prefer stable version)

Basically, it's just what we would select by running the command without the latest version selector.

Comment on lines +536 to +537

@epage epage Apr 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why are we reporting the MSRV-compatible version?

View changes since the review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

From #10741 (comment)

$ cargo add clap@latest
error: invalid version requirement `latest`

help: to use X, run `cargo add clap`
help: to use the latest version, run `cargo add clap@4.5.38`

I thought we usually selected X by running cargo add clap?

let resolved =
get_latest_dependency(spec, &dependency, honor_rust_version, gctx, registry)?;
let resolved_version = resolved
.version()
.expect("resolved dependency should have version");
// Get the actual latest non-prerelease, non-yanked version from the registry,

@0xPoe 0xPoe Apr 22, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think this comment is not entirely accurate. I discovered that the behavior of get_latest_dependency is to prefer the stable version. Only if there is no stable version available will it select the pre-release versions.

@epage Form you comment: #10741 (comment)

Yes, I was suggesting we suggest the latest version as that would be the literal meaning of latest and would therefore match the users intent. The user could have done this themselves. We likely should skip pre-release and yanked as we generally don't consider them the latest.

It may also make sense to inherit the same behavior: if there is no stable release, we suggest the pre-release version here?

View changes since the review

// ignoring MSRV and existing version constraints.
// Only name + registry matter; `Dependency::query` ignores other fields.
let mut unconstrained_dep = Dependency::new(&dependency.name);
if let Some(registry_name) = dependency.registry() {
unconstrained_dep = unconstrained_dep.set_registry(registry_name);
}
let latest = get_latest_dependency(spec, &unconstrained_dep, Some(false), gctx, registry)?;
let latest_version = latest
.version()
.expect("latest dependency should have version");
if resolved_version == latest_version {
anyhow::bail!(
"invalid version requirement `latest`\n\n\
help: to add the latest version `{latest_version}`, run `cargo add {}`",
dependency.name,
);
} else {
anyhow::bail!(
"invalid version requirement `latest`\n\n\
help: to use `{resolved_version}`, run `cargo add {}`\n\
help: to use the latest version, run `cargo add {}@{latest_version}`",
dependency.name,
dependency.name,
);
}
}

let query = query_dependency(ws, gctx, &mut dependency)?;
let dependency = populate_available_features(dependency, &query, registry)?;

Expand Down
9 changes: 9 additions & 0 deletions tests/testsuite/cargo_add/add_latest/in/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[workspace]

[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
edition = "2015"

[dependencies]
my-package = "0.4"
Empty file.
38 changes: 38 additions & 0 deletions tests/testsuite/cargo_add/add_latest/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use crate::prelude::*;
use cargo_test_support::Project;
use cargo_test_support::compare::assert_ui;
use cargo_test_support::current_dir;
use cargo_test_support::file;
use cargo_test_support::str;

#[cargo_test]
fn case() {
cargo_test_support::registry::init();
for ver in [
"0.1.1+my-package",
"0.2.0+my-package",
"0.2.3+my-package",
"0.4.1+my-package",
"0.4.2+my-package",
"20.0.0+my-package",
"99999.0.0+my-package",
"99999.0.0-alpha.1+my-package",
] {
cargo_test_support::registry::Package::new("my-package", ver).publish();
}

let project = Project::from_template(current_dir!().join("in"));
let project_root = project.root();
let cwd = &project_root;

snapbox::cmd::Command::cargo_ui()
.arg("add")
.arg_line("my-package@latest")
.current_dir(cwd)
.assert()
.failure()
.stdout_eq(str![""])
.stderr_eq(file!["stderr.term.svg"]);

assert_ui().subset_matches(current_dir!().join("out"), &project_root);
}
9 changes: 9 additions & 0 deletions tests/testsuite/cargo_add/add_latest/out/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[workspace]

[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
edition = "2015"

[dependencies]
my-package = "0.4"
36 changes: 36 additions & 0 deletions tests/testsuite/cargo_add/add_latest/stderr.term.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[workspace]

[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
edition = "2015"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

40 changes: 40 additions & 0 deletions tests/testsuite/cargo_add/add_latest_alt_registry/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use crate::prelude::*;
use cargo_test_support::Project;
use cargo_test_support::compare::assert_ui;
use cargo_test_support::current_dir;
use cargo_test_support::file;
use cargo_test_support::str;

#[cargo_test]
fn case() {
cargo_test_support::registry::alt_init();
for ver in [
"0.1.1+my-package",
"0.2.0+my-package",
"0.2.3+my-package",
"0.4.1+my-package",
"0.4.2+my-package",
"20.0.0+my-package",
"99999.0.0+my-package",
"99999.0.0-alpha.1+my-package",
] {
cargo_test_support::registry::Package::new("my-package", ver)
.alternative(true)
.publish();
}

let project = Project::from_template(current_dir!().join("in"));
let project_root = project.root();
let cwd = &project_root;

snapbox::cmd::Command::cargo_ui()
.arg("add")
.arg_line("my-package@latest --registry alternative")
.current_dir(cwd)
.assert()
.failure()
.stdout_eq(str![""])
.stderr_eq(file!["stderr.term.svg"]);

assert_ui().subset_matches(current_dir!().join("out"), &project_root);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[workspace]

[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
edition = "2015"
Loading
Loading