1use 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#[derive(Debug, Clone, Hash, PartialEq, Eq)]
33pub struct Options {
34 pub to_update: Vec<(String, Option<Semver>, Cow<'static, str>)>,
36 pub all: bool,
38 pub update: bool,
40 pub install: bool,
42 pub force: bool,
44 pub downdate: bool,
46 pub update_git: bool,
48 pub quiet: bool,
50 pub locked: bool,
53 pub released_after: Option<DateTime<Utc>>,
55 pub filter: Vec<PackageFilterElement>,
57 pub cargo_dir: (PathBuf, PathBuf),
60 pub temp_dir: PathBuf,
62 pub cargo_install_args: Vec<OsString>,
64 pub install_cargo: Option<OsString>,
66 pub jobs: Option<NonZero<usize>>,
68 pub recursive_jobs: Option<NonZero<usize>>,
70 pub concurrent_cargos: Option<NonZero<usize>>,
72}
73
74#[derive(Debug, Clone, Hash, PartialEq, Eq)]
76pub struct ConfigOptions {
77 pub cargo_dir: PathBuf,
79 pub package: String,
81 pub ops: Vec<ConfigOperation>,
83}
84
85
86impl Options {
87 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 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}