Skip to content

Commit 4b9a10d

Browse files
committed
Auto merge of #12547 - epage:install, r=weihanglo
refactor(install): Move value parsing to clap ### What does this PR try to resolve? Originally, I thought this was going to help some of my other work but it won't. Instead I decided to finish and post this PR in case there was interest since `@weihanglo` expressed interest in using `Arg::value_parser` more and this demonstrates that. ### How should we test and review this PR? A commit at a time. There seemed to be existing tests for some of the error changes.
2 parents 2a159aa + 9379325 commit 4b9a10d

File tree

4 files changed

+122
-93
lines changed

4 files changed

+122
-93
lines changed

src/bin/cargo/commands/install.rs

Lines changed: 89 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
use crate::command_prelude::*;
22

33
use anyhow::anyhow;
4+
use anyhow::bail;
5+
use anyhow::format_err;
46
use cargo::core::{GitReference, SourceId, Workspace};
57
use cargo::ops;
68
use cargo::util::IntoUrl;
9+
use cargo::util::ToSemver;
10+
use cargo::util::VersionReqExt;
11+
use cargo::CargoResult;
12+
use semver::VersionReq;
713

814
use cargo_util::paths;
915

1016
pub fn cli() -> Command {
1117
subcommand("install")
1218
.about("Install a Rust binary. Default location is $HOME/.cargo/bin")
13-
.arg(
14-
Arg::new("crate")
15-
.value_parser(clap::builder::NonEmptyStringValueParser::new())
16-
.num_args(0..),
17-
)
19+
.arg(Arg::new("crate").value_parser(parse_crate).num_args(0..))
1820
.arg(
1921
opt("version", "Specify a version to install")
2022
.alias("vers")
2123
.value_name("VERSION")
24+
.value_parser(parse_semver_flag)
2225
.requires("crate"),
2326
)
2427
.arg(
@@ -102,11 +105,12 @@ pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult {
102105
// but not `Config::reload_rooted_at` which is always cwd)
103106
let path = path.map(|p| paths::normalize_path(&p));
104107

105-
let version = args.get_one::<String>("version").map(String::as_str);
108+
let version = args.get_one::<VersionReq>("version");
106109
let krates = args
107-
.get_many::<String>("crate")
110+
.get_many::<CrateVersion>("crate")
108111
.unwrap_or_default()
109-
.map(|k| resolve_crate(k, version))
112+
.cloned()
113+
.map(|(krate, local_version)| resolve_crate(krate, local_version, version))
110114
.collect::<crate::CargoResult<Vec<_>>>()?;
111115

112116
for (crate_name, _) in krates.iter() {
@@ -190,20 +194,86 @@ pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult {
190194
Ok(())
191195
}
192196

193-
fn resolve_crate<'k>(
194-
mut krate: &'k str,
195-
mut version: Option<&'k str>,
196-
) -> crate::CargoResult<(&'k str, Option<&'k str>)> {
197-
if let Some((k, v)) = krate.split_once('@') {
198-
if version.is_some() {
199-
anyhow::bail!("cannot specify both `@{v}` and `--version`");
200-
}
197+
type CrateVersion = (String, Option<VersionReq>);
198+
199+
fn parse_crate(krate: &str) -> crate::CargoResult<CrateVersion> {
200+
let (krate, version) = if let Some((k, v)) = krate.split_once('@') {
201201
if k.is_empty() {
202202
// by convention, arguments starting with `@` are response files
203-
anyhow::bail!("missing crate name for `@{v}`");
203+
anyhow::bail!("missing crate name before '@'");
204204
}
205-
krate = k;
206-
version = Some(v);
205+
let krate = k.to_owned();
206+
let version = Some(parse_semver_flag(v)?);
207+
(krate, version)
208+
} else {
209+
let krate = krate.to_owned();
210+
let version = None;
211+
(krate, version)
212+
};
213+
214+
if krate.is_empty() {
215+
anyhow::bail!("crate name is empty");
207216
}
217+
218+
Ok((krate, version))
219+
}
220+
221+
/// Parses x.y.z as if it were =x.y.z, and gives CLI-specific error messages in the case of invalid
222+
/// values.
223+
fn parse_semver_flag(v: &str) -> CargoResult<VersionReq> {
224+
// If the version begins with character <, >, =, ^, ~ parse it as a
225+
// version range, otherwise parse it as a specific version
226+
let first = v
227+
.chars()
228+
.next()
229+
.ok_or_else(|| format_err!("no version provided for the `--version` flag"))?;
230+
231+
let is_req = "<>=^~".contains(first) || v.contains('*');
232+
if is_req {
233+
match v.parse::<VersionReq>() {
234+
Ok(v) => Ok(v),
235+
Err(_) => bail!(
236+
"the `--version` provided, `{}`, is \
237+
not a valid semver version requirement\n\n\
238+
Please have a look at \
239+
https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html \
240+
for the correct format",
241+
v
242+
),
243+
}
244+
} else {
245+
match v.to_semver() {
246+
Ok(v) => Ok(VersionReq::exact(&v)),
247+
Err(e) => {
248+
let mut msg = e.to_string();
249+
250+
// If it is not a valid version but it is a valid version
251+
// requirement, add a note to the warning
252+
if v.parse::<VersionReq>().is_ok() {
253+
msg.push_str(&format!(
254+
"\n\n tip: if you want to specify semver range, \
255+
add an explicit qualifier, like '^{}'",
256+
v
257+
));
258+
}
259+
bail!(msg);
260+
}
261+
}
262+
}
263+
}
264+
265+
fn resolve_crate(
266+
krate: String,
267+
local_version: Option<VersionReq>,
268+
version: Option<&VersionReq>,
269+
) -> crate::CargoResult<CrateVersion> {
270+
let version = match (local_version, version) {
271+
(Some(_), Some(_)) => {
272+
anyhow::bail!("cannot specify both `@<VERSION>` and `--version <VERSION>`");
273+
}
274+
(Some(l), None) => Some(l),
275+
(None, Some(g)) => Some(g.to_owned()),
276+
(None, None) => None,
277+
};
208278
Ok((krate, version))
209279
}

src/cargo/ops/cargo_install.rs

Lines changed: 19 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ use crate::ops::{common_for_install_and_uninstall::*, FilterRule};
1111
use crate::ops::{CompileFilter, Packages};
1212
use crate::sources::{GitSource, PathSource, SourceConfigMap};
1313
use crate::util::errors::CargoResult;
14-
use crate::util::{Config, Filesystem, Rustc, ToSemver, VersionReqExt};
14+
use crate::util::{Config, Filesystem, Rustc};
1515
use crate::{drop_println, ops};
1616

17-
use anyhow::{bail, format_err, Context as _};
17+
use anyhow::{bail, Context as _};
1818
use cargo_util::paths;
1919
use itertools::Itertools;
2020
use semver::VersionReq;
@@ -38,12 +38,12 @@ impl Drop for Transaction {
3838
}
3939
}
4040

41-
struct InstallablePackage<'cfg, 'a> {
41+
struct InstallablePackage<'cfg> {
4242
config: &'cfg Config,
4343
opts: ops::CompileOptions,
4444
root: Filesystem,
4545
source_id: SourceId,
46-
vers: Option<&'a str>,
46+
vers: Option<VersionReq>,
4747
force: bool,
4848
no_track: bool,
4949

@@ -53,7 +53,7 @@ struct InstallablePackage<'cfg, 'a> {
5353
target: String,
5454
}
5555

56-
impl<'cfg, 'a> InstallablePackage<'cfg, 'a> {
56+
impl<'cfg> InstallablePackage<'cfg> {
5757
// Returns pkg to install. None if pkg is already installed
5858
pub fn new(
5959
config: &'cfg Config,
@@ -62,12 +62,12 @@ impl<'cfg, 'a> InstallablePackage<'cfg, 'a> {
6262
krate: Option<&str>,
6363
source_id: SourceId,
6464
from_cwd: bool,
65-
vers: Option<&'a str>,
66-
original_opts: &'a ops::CompileOptions,
65+
vers: Option<&VersionReq>,
66+
original_opts: &ops::CompileOptions,
6767
force: bool,
6868
no_track: bool,
6969
needs_update_if_source_is_index: bool,
70-
) -> CargoResult<Option<InstallablePackage<'cfg, 'a>>> {
70+
) -> CargoResult<Option<Self>> {
7171
if let Some(name) = krate {
7272
if name == "." {
7373
bail!(
@@ -82,8 +82,8 @@ impl<'cfg, 'a> InstallablePackage<'cfg, 'a> {
8282
let pkg = {
8383
let dep = {
8484
if let Some(krate) = krate {
85-
let vers = if let Some(vers_flag) = vers {
86-
Some(parse_semver_flag(vers_flag)?.to_string())
85+
let vers = if let Some(vers) = vers {
86+
Some(vers.to_string())
8787
} else if source_id.is_registry() {
8888
// Avoid pre-release versions from crate.io
8989
// unless explicitly asked for
@@ -234,7 +234,7 @@ impl<'cfg, 'a> InstallablePackage<'cfg, 'a> {
234234
opts,
235235
root,
236236
source_id,
237-
vers,
237+
vers: vers.cloned(),
238238
force,
239239
no_track,
240240

@@ -604,7 +604,7 @@ Consider enabling some of the needed features by passing, e.g., `--features=\"{e
604604
pub fn install(
605605
config: &Config,
606606
root: Option<&str>,
607-
krates: Vec<(&str, Option<&str>)>,
607+
krates: Vec<(String, Option<VersionReq>)>,
608608
source_id: SourceId,
609609
from_cwd: bool,
610610
opts: &ops::CompileOptions,
@@ -617,9 +617,9 @@ pub fn install(
617617

618618
let (installed_anything, scheduled_error) = if krates.len() <= 1 {
619619
let (krate, vers) = krates
620-
.into_iter()
620+
.iter()
621621
.next()
622-
.map(|(k, v)| (Some(k), v))
622+
.map(|(k, v)| (Some(k.as_str()), v.as_ref()))
623623
.unwrap_or((None, None));
624624
let installable_pkg = InstallablePackage::new(
625625
config, root, map, krate, source_id, from_cwd, vers, opts, force, no_track, true,
@@ -637,18 +637,18 @@ pub fn install(
637637
let mut did_update = false;
638638

639639
let pkgs_to_install: Vec<_> = krates
640-
.into_iter()
640+
.iter()
641641
.filter_map(|(krate, vers)| {
642642
let root = root.clone();
643643
let map = map.clone();
644644
match InstallablePackage::new(
645645
config,
646646
root,
647647
map,
648-
Some(krate),
648+
Some(krate.as_str()),
649649
source_id,
650650
from_cwd,
651-
vers,
651+
vers.as_ref(),
652652
opts,
653653
force,
654654
no_track,
@@ -660,12 +660,12 @@ pub fn install(
660660
}
661661
Ok(None) => {
662662
// Already installed
663-
succeeded.push(krate);
663+
succeeded.push(krate.as_str());
664664
None
665665
}
666666
Err(e) => {
667667
crate::display_error(&e, &mut config.shell());
668-
failed.push(krate);
668+
failed.push(krate.as_str());
669669
// We assume an update was performed if we got an error.
670670
did_update = true;
671671
None
@@ -805,54 +805,6 @@ fn make_ws_rustc_target<'cfg>(
805805
Ok((ws, rustc, target))
806806
}
807807

808-
/// Parses x.y.z as if it were =x.y.z, and gives CLI-specific error messages in the case of invalid
809-
/// values.
810-
fn parse_semver_flag(v: &str) -> CargoResult<VersionReq> {
811-
// If the version begins with character <, >, =, ^, ~ parse it as a
812-
// version range, otherwise parse it as a specific version
813-
let first = v
814-
.chars()
815-
.next()
816-
.ok_or_else(|| format_err!("no version provided for the `--version` flag"))?;
817-
818-
let is_req = "<>=^~".contains(first) || v.contains('*');
819-
if is_req {
820-
match v.parse::<VersionReq>() {
821-
Ok(v) => Ok(v),
822-
Err(_) => bail!(
823-
"the `--version` provided, `{}`, is \
824-
not a valid semver version requirement\n\n\
825-
Please have a look at \
826-
https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html \
827-
for the correct format",
828-
v
829-
),
830-
}
831-
} else {
832-
match v.to_semver() {
833-
Ok(v) => Ok(VersionReq::exact(&v)),
834-
Err(e) => {
835-
let mut msg = format!(
836-
"the `--version` provided, `{}`, is \
837-
not a valid semver version: {}\n",
838-
v, e
839-
);
840-
841-
// If it is not a valid version but it is a valid version
842-
// requirement, add a note to the warning
843-
if v.parse::<VersionReq>().is_ok() {
844-
msg.push_str(&format!(
845-
"\nif you want to specify semver range, \
846-
add an explicit qualifier, like ^{}",
847-
v
848-
));
849-
}
850-
bail!(msg);
851-
}
852-
}
853-
}
854-
}
855-
856808
/// Display a list of installed binaries.
857809
pub fn install_list(dst: Option<&str>, config: &Config) -> CargoResult<()> {
858810
let root = resolve_root(dst, config)?;

tests/testsuite/install.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1600,8 +1600,13 @@ fn inline_version_without_name() {
16001600
pkg("foo", "0.1.2");
16011601

16021602
cargo_process("install @0.1.1")
1603-
.with_status(101)
1604-
.with_stderr("error: missing crate name for `@0.1.1`")
1603+
.with_status(1)
1604+
.with_stderr(
1605+
"error: invalid value '@0.1.1' for '[crate]...': missing crate name before '@'
1606+
1607+
For more information, try '--help'.
1608+
",
1609+
)
16051610
.run();
16061611
}
16071612

@@ -1612,7 +1617,7 @@ fn inline_and_explicit_version() {
16121617

16131618
cargo_process("install [email protected] --version 0.1.1")
16141619
.with_status(101)
1615-
.with_stderr("error: cannot specify both `@0.1.1` and `--version`")
1620+
.with_stderr("error: cannot specify both `@<VERSION>` and `--version <VERSION>`")
16161621
.run();
16171622
}
16181623

@@ -1827,7 +1832,7 @@ fn install_empty_argument() {
18271832
cargo_process("install")
18281833
.arg("")
18291834
.with_status(1)
1830-
.with_stderr_contains("[ERROR] a value is required for '[crate]...' but none was supplied")
1835+
.with_stderr_contains("[ERROR] invalid value '' for '[crate]...': crate name is empty")
18311836
.run();
18321837
}
18331838

tests/testsuite/install_upgrade.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,12 +230,14 @@ fn ambiguous_version_no_longer_allowed() {
230230
cargo_process("install foo --version=1.0")
231231
.with_stderr(
232232
"\
233-
[ERROR] the `--version` provided, `1.0`, is not a valid semver version: cannot parse '1.0' as a semver
233+
[ERROR] invalid value '1.0' for '--version <VERSION>': cannot parse '1.0' as a semver
234234
235-
if you want to specify semver range, add an explicit qualifier, like ^1.0
235+
tip: if you want to specify semver range, add an explicit qualifier, like '^1.0'
236+
237+
For more information, try '--help'.
236238
",
237239
)
238-
.with_status(101)
240+
.with_status(1)
239241
.run();
240242
}
241243

0 commit comments

Comments
 (0)