cargo_update/
options.rs

1//! Option parsing and management.
2//!
3//! Use the `Options::parse()` function to get the program's configuration,
4//! as parsed from the commandline.
5//!
6//! # Examples
7//!
8//! ```no_run
9//! # use cargo_update::Options;
10//! let opts = Options::parse();
11//! println!("{:#?}", opts);
12//! ```
13
14
15use self::super::ops::{PackageFilterElement, ConfigOperation};
16use semver::{VersionReq as SemverReq, Version as Semver};
17use clap::{AppSettings, SubCommand, App, Arg};
18use std::ffi::{OsString, OsStr};
19use std::path::{PathBuf, Path};
20use std::fmt::Arguments;
21use whoami::username_os;
22use std::process::exit;
23use std::str::FromStr;
24use std::borrow::Cow;
25use std::{env, fs};
26use home;
27
28
29/// Representation of the application's all configurable values.
30#[derive(Debug, Clone, Hash, PartialEq, Eq)]
31pub struct Options {
32    /// (Additional) packages to update. Default: `[]`
33    pub to_update: Vec<(String, Option<Semver>, Cow<'static, str>)>,
34    /// Whether to update all packages. Default: `false`
35    pub all: bool,
36    /// Whether to update packages or just list them. Default: `true`
37    pub update: bool,
38    /// Whether to allow for just installing packages. Default: `false`
39    pub install: bool,
40    /// Update all packages. Default: `false`
41    pub force: bool,
42    /// Downdate packages to match newest unyanked registry version.
43    pub downdate: bool,
44    /// Update git packages too (it's expensive). Default: `false`
45    pub update_git: bool,
46    /// Don't output messages and pass --quiet to `cargo` subprocesses. Default: `false`
47    pub quiet: bool,
48    /// Enforce packages' embedded `Cargo.lock`. Exactly like `CARGO_INSTALL_OPTS=--locked` (or `--enforce-lock` per package)
49    /// except doesn't disable cargo-binstall. Default: `false`
50    pub locked: bool,
51    /// Update all packages. Default: empty
52    pub filter: Vec<PackageFilterElement>,
53    /// The `cargo` home directory; (original, canonicalised). Default: `"$CARGO_INSTALL_ROOT"`, then `"$CARGO_HOME"`,
54    /// then `"$HOME/.cargo"`
55    pub cargo_dir: (PathBuf, PathBuf),
56    /// The temporary directory to clone git repositories to. Default: `"$TEMP/cargo-update"`
57    pub temp_dir: PathBuf,
58    /// Arbitrary arguments to forward to `cargo install`, acquired from `$CARGO_INSTALL_OPTS`. Default: `[]`
59    pub cargo_install_args: Vec<OsString>,
60    /// The cargo to run for installations. Default: `None` (use "cargo")
61    pub install_cargo: Option<OsString>,
62    /// Limit of concurrent jobs. Default: `None`
63    pub jobs: Option<OsString>,
64}
65
66/// Representation of the config application's all configurable values.
67#[derive(Debug, Clone, Hash, PartialEq, Eq)]
68pub struct ConfigOptions {
69    /// The `cargo` home directory. Default: `"$CARGO_INSTALL_ROOT"`, then `"$CARGO_HOME"`, then `"$HOME/.cargo"`
70    pub cargo_dir: PathBuf,
71    /// Crate to modify config for
72    pub package: String,
73    /// What to do to the config, or display with empty
74    pub ops: Vec<ConfigOperation>,
75}
76
77
78impl Options {
79    /// Parse `env`-wide command-line arguments into an `Options` instance
80    pub fn parse() -> Options {
81        let matches = App::new("cargo")
82            .bin_name("cargo")
83            .version(crate_version!())
84            .settings(&[AppSettings::ColoredHelp, AppSettings::ArgRequiredElseHelp, AppSettings::GlobalVersion, AppSettings::SubcommandRequired])
85            .subcommand(SubCommand::with_name("install-update")
86                .version(crate_version!())
87                .author("https://github.com/nabijaczleweli/cargo-update")
88                .about("A cargo subcommand for checking and applying updates to installed executables")
89                .args(&[Arg::from_usage("-c --cargo-dir=[CARGO_DIR] 'The cargo home directory. Default: $CARGO_HOME or $HOME/.cargo'")
90                            .visible_alias("root")
91                            .allow_invalid_utf8(true)
92                            .validator(|s| existing_dir_validator("Cargo", &s)),
93                        Arg::from_usage("-t --temp-dir=[TEMP_DIR] 'The temporary directory. Default: $TEMP/cargo-update'")
94                            .validator(|s| existing_dir_validator("Temporary", &s)),
95                        Arg::from_usage("-a --all 'Update all packages'"),
96                        Arg::from_usage("-l --list 'Don't update packages, only list and check if they need an update (all packages by default)'"),
97                        Arg::from_usage("-f --force 'Update all packages regardless if they need updating'"),
98                        Arg::from_usage("-d --downdate 'Downdate packages to match latest unyanked registry version'"),
99                        Arg::from_usage("-i --allow-no-update 'Allow for fresh-installing packages'"),
100                        Arg::from_usage("-g --git 'Also update git packages'"),
101                        Arg::from_usage("-q --quiet 'No output printed to stdout'"),
102                        Arg::from_usage("--locked 'Enforce packages' embedded Cargo.lock'"),
103                        Arg::from_usage("-s --filter=[PACKAGE_FILTER]... 'Specify a filter a package must match to be considered'")
104                            .number_of_values(1)
105                            .validator(|s| PackageFilterElement::parse(&s).map(|_| ())),
106                        Arg::from_usage("-r --install-cargo=[EXECUTABLE] 'Specify an alternative cargo to run for installations'").allow_invalid_utf8(true),
107                        Arg::from_usage("-j --jobs=[JOBS] 'Limit number of parallel jobs.'").allow_invalid_utf8(true),
108                        Arg::with_name("cargo_install_opts")
109                            .long("__cargo_install_opts")
110                            .env("CARGO_INSTALL_OPTS")
111                            .allow_invalid_utf8(true)
112                            .empty_values(false)
113                            .multiple(true)
114                            .value_delimiter(' ')
115                            .hidden(true),
116                        Arg::from_usage("[PACKAGE]... 'Packages to update'")
117                            .empty_values(false)
118                            .min_values(1)
119                            .validator(|s| package_parse(s).map(|_| ()))]))
120            .get_matches();
121        let matches = matches.subcommand_matches("install-update").unwrap();
122
123        let all = matches.is_present("all");
124        let update = !matches.is_present("list");
125        Options {
126            to_update: match (all || !update, matches.values_of("PACKAGE")) {
127                (_, Some(pkgs)) => {
128                    let mut packages: Vec<_> = pkgs.map(package_parse)
129                        .map(Result::unwrap)
130                        .map(|(package, version, registry)| {
131                            (package.to_string(),
132                             version,
133                             registry.map(str::to_string).map(Cow::from).unwrap_or("https://github.com/rust-lang/crates.io-index".into()))
134                        })
135                        .collect();
136                    packages.sort_by(|l, r| l.0.cmp(&r.0));
137                    packages.dedup_by(|l, r| l.0 == r.0);
138                    packages
139                }
140                (true, None) => vec![],
141                (false, None) => clerror(format_args!("Need at least one PACKAGE without --all")),
142            },
143            all: all,
144            update: update,
145            install: matches.is_present("allow-no-update"),
146            force: matches.is_present("force"),
147            downdate: matches.is_present("downdate"),
148            update_git: matches.is_present("git"),
149            quiet: matches.is_present("quiet"),
150            locked: matches.is_present("locked"),
151            filter: matches.values_of("filter").map(|pfs| pfs.flat_map(PackageFilterElement::parse).collect()).unwrap_or_default(),
152            cargo_dir: cargo_dir(matches.value_of_os("cargo-dir")),
153            temp_dir: {
154                if let Some(tmpdir) = matches.value_of("temp-dir") {
155                        fs::canonicalize(tmpdir).unwrap()
156                    } else {
157                        env::temp_dir()
158                    }
159                    .join(Path::new("cargo-update").with_extension(username_os()))
160            },
161            cargo_install_args: matches.values_of_os("cargo_install_opts").into_iter().flat_map(|cio| cio.map(OsStr::to_os_string)).collect(),
162            install_cargo: matches.value_of_os("install-cargo").map(OsStr::to_os_string),
163            jobs: matches.value_of_os("jobs").map(OsStr::to_os_string),
164        }
165    }
166}
167
168impl ConfigOptions {
169    /// Parse `env`-wide command-line arguments into a `ConfigOptions` instance
170    pub fn parse() -> ConfigOptions {
171        let matches = App::new("cargo")
172            .bin_name("cargo")
173            .version(crate_version!())
174            .settings(&[AppSettings::ColoredHelp, AppSettings::ArgRequiredElseHelp, AppSettings::GlobalVersion, AppSettings::SubcommandRequired])
175            .subcommand(SubCommand::with_name("install-update-config")
176                .version(crate_version!())
177                .author("https://github.com/nabijaczleweli/cargo-update")
178                .about("A cargo subcommand for checking and applying updates to installed executables -- configuration")
179                .args(&[Arg::from_usage("-c --cargo-dir=[CARGO_DIR] 'The cargo home directory. Default: $CARGO_HOME or $HOME/.cargo'")
180                            .validator(|s| existing_dir_validator("Cargo", &s)),
181                        Arg::from_usage("-t --toolchain=[TOOLCHAIN] 'Toolchain to use or empty for default'"),
182                        Arg::from_usage("-f --feature=[FEATURE]... 'Feature to enable'").number_of_values(1),
183                        Arg::from_usage("-n --no-feature=[DISABLED_FEATURE]... 'Feature to disable'").number_of_values(1),
184                        Arg::from_usage("-d --default-features=[DEFAULT_FEATURES] 'Whether to allow default features'")
185                            .possible_values(&["1", "yes", "true", "0", "no", "false"])
186                            .hide_possible_values(true),
187                        Arg::from_usage("--debug 'Compile the package in debug (\"dev\") mode'").conflicts_with("release").conflicts_with("build-profile"),
188                        Arg::from_usage("--release 'Compile the package in release mode'").conflicts_with("debug").conflicts_with("build-profile"),
189                        Arg::from_usage("--build-profile=[PROFILE] 'Compile the package in the given profile'")
190                            .conflicts_with("debug")
191                            .conflicts_with("release"),
192                        Arg::from_usage("--install-prereleases 'Install prerelease versions'").conflicts_with("no-install-prereleases"),
193                        Arg::from_usage("--no-install-prereleases 'Filter out prerelease versions'").conflicts_with("install-prereleases"),
194                        Arg::from_usage("--enforce-lock 'Require Cargo.lock to be up to date'").conflicts_with("no-enforce-lock"),
195                        Arg::from_usage("--no-enforce-lock 'Don't enforce Cargo.lock'").conflicts_with("enforce-lock"),
196                        Arg::from_usage("--respect-binaries 'Only install already installed binaries'").conflicts_with("no-respect-binaries"),
197                        Arg::from_usage("--no-respect-binaries 'Install all binaries'").conflicts_with("respect-binaries"),
198                        Arg::from_usage("-v --version=[VERSION_REQ] 'Require a cargo-compatible version range'")
199                            .validator(|s| SemverReq::from_str(&s).map(|_| ()).map_err(|e| e.to_string()))
200                            .conflicts_with("any-version"),
201                        Arg::from_usage("-a --any-version 'Allow any version'").conflicts_with("version"),
202                        Arg::from_usage("-e --environment=[VARIABLE=VALUE]... 'Environment variable to set'")
203                            .number_of_values(1)
204                            .validator(|s| if s.contains('=') {
205                                Ok(())
206                            } else {
207                                Err("Missing VALUE")
208                            }),
209                        Arg::from_usage("-E --clear-environment=[VARIABLE]... 'Environment variable to clear'")
210                            .number_of_values(1)
211                            .validator(|s| if s.contains('=') {
212                                Err("VARIABLE can't contain a =")
213                            } else {
214                                Ok(())
215                            }),
216                        Arg::from_usage("--inherit-environment=[VARIABLE]... 'Environment variable to use from the environment'")
217                            .number_of_values(1)
218                            .validator(|s| if s.contains('=') {
219                                Err("VARIABLE can't contain a =")
220                            } else {
221                                Ok(())
222                            }),
223                        Arg::from_usage("-r --reset 'Roll back the configuration to the defaults.'"),
224                        Arg::from_usage("<PACKAGE> 'Package to configure'").empty_values(false)]))
225            .get_matches();
226        let matches = matches.subcommand_matches("install-update-config").unwrap();
227
228        ConfigOptions {
229            cargo_dir: cargo_dir(matches.value_of_os("cargo-dir")).1,
230            package: matches.value_of("PACKAGE").unwrap().to_string(),
231            ops: matches.value_of("toolchain")
232                .map(|t| if t.is_empty() {
233                    ConfigOperation::RemoveToolchain
234                } else {
235                    ConfigOperation::SetToolchain(t.to_string())
236                })
237                .into_iter()
238                .chain(matches.values_of("feature").into_iter().flatten().map(str::to_string).map(ConfigOperation::AddFeature))
239                .chain(matches.values_of("no-feature").into_iter().flatten().map(str::to_string).map(ConfigOperation::RemoveFeature))
240                .chain(matches.value_of("default-features").map(|d| ["1", "yes", "true"].contains(&d)).map(ConfigOperation::DefaultFeatures).into_iter())
241                .chain(match (matches.is_present("debug"), matches.is_present("release"), matches.value_of("build-profile")) {
242                    (true, _, _) => Some(ConfigOperation::SetBuildProfile("dev".into())),
243                    (_, true, _) => Some(ConfigOperation::SetBuildProfile("release".into())),
244                    (_, _, Some(prof)) => Some(ConfigOperation::SetBuildProfile(prof.to_string().into())),
245                    _ => None,
246                })
247                .chain(match (matches.is_present("install-prereleases"), matches.is_present("no-install-prereleases")) {
248                    (true, _) => Some(ConfigOperation::SetInstallPrereleases(true)),
249                    (_, true) => Some(ConfigOperation::SetInstallPrereleases(false)),
250                    _ => None,
251                })
252                .chain(match (matches.is_present("enforce-lock"), matches.is_present("no-enforce-lock")) {
253                    (true, _) => Some(ConfigOperation::SetEnforceLock(true)),
254                    (_, true) => Some(ConfigOperation::SetEnforceLock(false)),
255                    _ => None,
256                })
257                .chain(match (matches.is_present("respect-binaries"), matches.is_present("no-respect-binaries")) {
258                    (true, _) => Some(ConfigOperation::SetRespectBinaries(true)),
259                    (_, true) => Some(ConfigOperation::SetRespectBinaries(false)),
260                    _ => None,
261                })
262                .chain(match (matches.is_present("any-version"), matches.value_of("version")) {
263                    (true, _) => Some(ConfigOperation::RemoveTargetVersion),
264                    (false, Some(vr)) => Some(ConfigOperation::SetTargetVersion(SemverReq::from_str(vr).unwrap())),
265                    _ => None,
266                })
267                .chain(matches.values_of("environment")
268                    .into_iter()
269                    .flatten()
270                    .map(|s| s.split_once('=').unwrap())
271                    .map(|(k, v)| ConfigOperation::SetEnvironment(k.to_string(), v.to_string())))
272                .chain(matches.values_of("clear-environment").into_iter().flatten().map(str::to_string).map(ConfigOperation::ClearEnvironment))
273                .chain(matches.values_of("inherit-environment").into_iter().flatten().map(str::to_string).map(ConfigOperation::InheritEnvironment))
274                .chain(matches.index_of("reset").map(|_| ConfigOperation::ResetConfig))
275                .collect(),
276        }
277    }
278}
279
280fn cargo_dir(opt_cargo_dir: Option<&OsStr>) -> (PathBuf, PathBuf) {
281    if let Some(dir) = opt_cargo_dir {
282        match fs::canonicalize(&dir) {
283            Ok(cdir) => (dir.into(), cdir),
284            Err(_) => clerror(format_args!("--cargo-dir={:?} doesn't exist", dir)),
285        }
286    } else {
287        match env::var_os("CARGO_INSTALL_ROOT")
288            .map(PathBuf::from)
289            .or_else(|| home::cargo_home().ok())
290            .and_then(|ch| fs::canonicalize(&ch).map(|can| (ch, can)).ok()) {
291            Some(cd) => cd,
292            None => {
293                clerror(format_args!("$CARGO_INSTALL_ROOT, $CARGO_HOME, and home directory invalid, please specify the cargo home directory with the -c \
294                                      option"))
295            }
296        }
297    }
298}
299
300fn existing_dir_validator(label: &str, s: &str) -> Result<(), String> {
301    fs::canonicalize(s).map(|_| ()).map_err(|_| format!("{} directory \"{}\" not found", label, s))
302}
303
304fn package_parse(mut s: &str) -> Result<(&str, Option<Semver>, Option<&str>), String> {
305    let mut registry_url = None;
306    if s.starts_with('(') {
307        if let Some(idx) = s.find("):") {
308            registry_url = Some(&s[1..idx]);
309            s = &s[idx + 2..];
310        }
311    }
312
313    if let Some(idx) = s.find(':') {
314        Ok((&s[0..idx],
315            Some(Semver::parse(&s[idx + 1..]).map_err(|e| format!("Version {} provided for package {} invalid: {}", &s[idx + 1..], &s[0..idx], e))?),
316            registry_url))
317    } else {
318        Ok((s, None, registry_url))
319    }
320}
321
322
323fn clerror(f: Arguments) -> ! {
324    eprintln!("{}", f);
325    exit(1)
326}