Skip to main content

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