1use clap::builder::{ValueParser, TypedValueParser, NonEmptyStringValueParser, PossibleValue};
16use clap::error::{Error as ClapError, ErrorKind as ClapErrorKind};
17use self::super::ops::{PackageFilterElement, ConfigOperation};
18use semver::{VersionReq as SemverReq, Version as Semver};
19use chrono::{TimeDelta, DateTime, Utc};
20use clap::{Command, Arg, ArgAction};
21use std::ffi::{OsString, OsStr};
22use std::path::{PathBuf, Path};
23use std::fmt::Arguments;
24use whoami::username_os;
25use std::process::exit;
26use std::str::FromStr;
27use std::num::NonZero;
28use std::borrow::Cow;
29use std::{env, fs};
30use home;
31
32
33#[derive(Debug, Clone, Hash, PartialEq, Eq)]
35pub struct Options {
36 pub to_update: Vec<(String, Option<Semver>, Cow<'static, str>)>,
38 pub all: bool,
40 pub update: bool,
42 pub install: bool,
44 pub force: bool,
46 pub downdate: bool,
48 pub update_git: bool,
50 pub quiet: bool,
52 pub locked: bool,
55 pub released_after: Option<DateTime<Utc>>,
57 pub filter: Vec<PackageFilterElement>,
59 pub cargo_dir: (PathBuf, PathBuf),
62 pub temp_dir: PathBuf,
64 pub cargo_install_args: Vec<OsString>,
66 pub install_cargo: Option<OsString>,
68 pub jobs: Option<NonZero<usize>>,
70 pub recursive_jobs: Option<NonZero<usize>>,
72 pub concurrent_cargos: Option<NonZero<usize>>,
74}
75
76#[derive(Debug, Clone, Hash, PartialEq, Eq)]
78pub struct ConfigOptions {
79 pub cargo_dir: PathBuf,
81 pub package: String,
83 pub ops: Vec<ConfigOperation>,
85}
86
87
88impl Options {
89 pub fn parse() -> Options {
91 let nproc = std::thread::available_parallelism().unwrap_or(NonZero::new(1).unwrap());
92 let mut matches = Command::new("cargo")
93 .bin_name("cargo")
94 .version(crate_version!())
95 .arg_required_else_help(true)
96 .subcommand_required(true)
97 .args_override_self(true)
98 .subcommand(Command::new("install-update")
99 .version(crate_version!())
100 .author("https://github.com/nabijaczleweli/cargo-update")
101 .about("A cargo subcommand for checking and applying updates to installed executables")
102 .args(&[arg!(-c --"cargo-dir" <CARGO_DIR> "The cargo home directory. Default: $CARGO_HOME or $HOME/.cargo")
103 .required(false)
104 .action(ArgAction::Set)
105 .visible_alias("root")
106 .value_parser(ExistingDirParser("Cargo")),
107 arg!(-t --"temp-dir" <TEMP_DIR> "The temporary directory. Default: $TEMP/cargo-update")
108 .required(false)
109 .action(ArgAction::Set)
110 .value_parser(ExistingDirParser("Temporary")),
111 arg!(-a --"all" "Update all packages").action(ArgAction::SetTrue),
112 arg!(-l --"list" "Don't update packages, only list and check if they need an update (all packages by default)")
113 .action(ArgAction::SetTrue),
114 arg!(-f --"force" "Update all packages regardless if they need updating").action(ArgAction::SetTrue),
115 arg!(-d --"downdate" "Downdate packages to match latest unyanked registry version").action(ArgAction::SetTrue),
116 arg!(-i --"allow-no-update" "Allow for fresh-installing packages").action(ArgAction::SetTrue),
117 arg!(-g --"git" "Also update git packages").action(ArgAction::SetTrue),
118 arg!(-q --"quiet" "No output printed to stdout").action(ArgAction::SetTrue),
119 arg!(--"locked" "Enforce packages' embedded Cargo.lock").action(ArgAction::SetTrue),
120 arg!(--"cooldown" <TIME> "Only consider versions released before (now - TIME). Seconds, [smhdwy] suffix.")
121 .required(false)
122 .action(ArgAction::Set)
123 .num_args(1)
124 .value_parser(duration_parse),
125 arg!(-s --"filter" <PACKAGE_FILTER>... "Specify a filter a package must match to be considered")
126 .required(false)
127 .action(ArgAction::Append)
128 .num_args(1)
129 .value_parser(PackageFilterElement::parse),
130 arg!(-r --"install-cargo" <EXECUTABLE> "Specify an alternative cargo to run for installations")
131 .required(false)
132 .action(ArgAction::Set)
133 .num_args(1)
134 .value_parser(ValueParser::os_string()),
135 arg!(-j --"jobs" <JOBS>)
136 .help(format!("Limit number of parallel jobs or \"default\" for {}", nproc))
137 .required(false)
138 .num_args(1)
139 .value_parser(JobsParser("default", nproc)),
140 arg!(-J --"recursive-jobs" <JOBS>)
141 .help(format!("Build up to JOBS crates at once on up to JOBS CPUs. {} if empty.", nproc))
142 .required(false)
143 .num_args(1)
144 .default_value("")
145 .value_parser(JobsParser("", nproc)),
146 Arg::new("cargo_install_opts")
147 .long("__cargo_install_opts")
148 .env("CARGO_INSTALL_OPTS")
149 .action(ArgAction::Set)
150 .value_parser(ValueParser::os_string())
151 .value_delimiter(' ')
152 .hide(true),
153 arg!(<PACKAGE>... "Packages to update")
154 .action(ArgAction::Append)
155 .required(false)
156 .value_parser(package_parse)]))
157 .get_matches_mut();
158 let (_, mut matches) = matches.remove_subcommand().unwrap();
159
160 let all = matches.remove_one("all").unwrap_or(false);
161 let update = !matches.remove_one("list").unwrap_or(false);
162 let jobs_arg = matches.remove_one("jobs");
163 let recursive_jobs = matches.remove_one("recursive-jobs");
164 Options {
165 to_update: match (all || !update, matches.remove_many::<(String, Option<Semver>, Option<String>)>("PACKAGE")) {
166 (_, Some(pkgs)) => {
167 let mut packages: Vec<_> = pkgs.map(|(package, version, registry)| {
168 (package, version, registry.map(Cow::from).unwrap_or("https://github.com/rust-lang/crates.io-index".into()))
169 })
170 .collect();
171 packages.sort_by(|l, r| l.0.cmp(&r.0));
172 packages.dedup_by(|l, r| l.0 == r.0);
173 packages
174 }
175 (true, None) => vec![],
176 (false, None) => clerror(format_args!("Need at least one PACKAGE without --all")),
177 },
178 all: all,
179 update: update,
180 install: matches.remove_one("allow-no-update").unwrap_or(false),
181 force: matches.remove_one("force").unwrap_or(false),
182 downdate: matches.remove_one("downdate").unwrap_or(false),
183 update_git: matches.remove_one("git").unwrap_or(false),
184 quiet: matches.remove_one("quiet").unwrap_or(false),
185 released_after: matches.get_one::<TimeDelta>("cooldown")
186 .map(|&td| {
187 Utc::now().checked_sub_signed(td).unwrap_or_else(|| {
188 let raw = matches.get_raw("cooldown").unwrap_or_default().last().unwrap();
189 clerror(format_args!("--cooldown {}: (now - {}) out of range", Path::new(raw).display(), td)) })
191 }),
192 locked: matches.remove_one("locked").unwrap_or(false),
193 filter: matches.remove_many("filter").into_iter().flatten().collect(),
194 cargo_dir: cargo_dir(matches.remove_one("cargo-dir")),
195 temp_dir: matches.remove_one("temp-dir").unwrap_or_else(env::temp_dir).join(Path::new("cargo-update").with_extension(username_os())),
196 cargo_install_args: matches.remove_many("cargo_install_opts").into_iter().flatten().filter(|a: &OsString| !a.is_empty()).collect(),
197 install_cargo: matches.remove_one("install-cargo"),
198 jobs: if recursive_jobs.is_some() {
199 None
200 } else {
201 jobs_arg
202 },
203 recursive_jobs: recursive_jobs,
204 concurrent_cargos: match (jobs_arg, recursive_jobs) {
205 (Some(j), Some(rj)) => Some(NonZero::new((rj.get() + (j.get() - 1)) / j).unwrap_or(NonZero::new(1).unwrap())),
206 _ => None,
207 },
208 }
209 }
210}
211
212impl ConfigOptions {
213 pub fn parse() -> ConfigOptions {
215 let mut matches = Command::new("cargo")
216 .bin_name("cargo")
217 .version(crate_version!())
218 .arg_required_else_help(true)
220 .subcommand_required(true)
221 .subcommand(Command::new("install-update-config")
222 .disable_version_flag(true)
223 .version(crate_version!())
224 .author("https://github.com/nabijaczleweli/cargo-update")
225 .about("A cargo subcommand for checking and applying updates to installed executables -- configuration")
226 .args(&[arg!(-c --"cargo-dir" <CARGO_DIR> "The cargo home directory. Default: $CARGO_HOME or $HOME/.cargo").required(false)
227 .value_parser(ExistingDirParser("Cargo")),
228 arg!(-t --"toolchain" <TOOLCHAIN> "Toolchain to use or empty for default")
229 .num_args(1).required(false).value_parser(ValueParser::string()),
230 arg!(-f --"feature" <FEATURE>... "Feature to enable").num_args(1).required(false).value_parser(ValueParser::string()),
231 arg!(-n --"no-feature" <DISABLED_FEATURE>... "Feature to disable").num_args(1).required(false).value_parser(ValueParser::string()),
232 arg!(-d --"default-features" <DEFAULT_FEATURES> "Whether to allow default features").num_args(1).required(false)
233 .value_parser(DefaultFeaturesBoolParser)
234 .hide_possible_values(true),
235 arg!(--"debug" "Compile the package in debug (\"dev\") mode")
236 .action(ArgAction::SetTrue).conflicts_with("release").conflicts_with("build-profile"),
237 arg!(--"release" "Compile the package in release mode")
238 .action(ArgAction::SetTrue).conflicts_with("debug").conflicts_with("build-profile"),
239 arg!(--"build-profile" <PROFILE> "Compile the package in the given profile").num_args(1).required(false)
240 .conflicts_with("debug")
241 .conflicts_with("release").value_parser(ValueParser::string()),
242 arg!(--"install-prereleases" "Install prerelease versions").action(ArgAction::SetTrue).conflicts_with("no-install-prereleases"),
243 arg!(--"no-install-prereleases" "Filter out prerelease versions").action(ArgAction::SetTrue).conflicts_with("install-prereleases"),
244 arg!(--"enforce-lock" "Require Cargo.lock to be up to date").action(ArgAction::SetTrue).conflicts_with("no-enforce-lock"),
245 arg!(--"no-enforce-lock" "Don't enforce Cargo.lock").action(ArgAction::SetTrue).conflicts_with("enforce-lock"),
246 arg!(--"respect-binaries" "Only install already installed binaries").action(ArgAction::SetTrue).conflicts_with("no-respect-binaries"),
247 arg!(--"no-respect-binaries" "Install all binaries").action(ArgAction::SetTrue).conflicts_with("respect-binaries"),
248 arg!(-v --"version" <VERSION_REQ> "Require a cargo-compatible version range").num_args(1).required(false)
249 .value_parser(SemverReq::from_str)
250 .conflicts_with("any-version"),
251 arg!(-a --"any-version" "Allow any version").action(ArgAction::SetTrue).conflicts_with("version"),
252 arg!(-e --"environment" <VARIABLE_EQ_TODO_VALUE>... "Environment variable to set").required(false)
253 .num_args(1)
254 .value_parser(|s: &str| if let Some((k,v)) = s.split_once('=') {
255 Ok((k.to_string(), v.to_string()))
256 } else {
257 Err("Missing VALUE")
258 }),
259 arg!(-E --"clear-environment" <VARIABLE>... "Environment variable to clear").required(false)
260 .num_args(1)
261 .value_parser(|s: &str| if s.contains('=') {
262 Err("VARIABLE can't contain a =")
263 } else {
264 Ok(s.to_string())
265 }),
266 arg!(--"inherit-environment" <VARIABLE>... "Environment variable to use from the environment").required(false)
267 .num_args(1)
268 .value_parser(|s: &str| if s.contains('=') {
269 Err("VARIABLE can't contain a =")
270 } else {
271 Ok(s.to_string())
272 }),
273 arg!(-r --"reset" "Roll back the configuration to the defaults.").action(ArgAction::SetTrue),
274 arg!(<PACKAGE> "Package to configure").value_parser(NonEmptyStringValueParser ::new())
275 ]))
276 .get_matches_mut();
277 let (_, mut matches) = matches.remove_subcommand().unwrap();
278
279 ConfigOptions {
280 cargo_dir: cargo_dir(matches.get_one("cargo-dir")).1,
281 package: matches.remove_one("PACKAGE").unwrap(),
282 ops: matches.remove_one("toolchain")
283 .map(|t: String| if t.is_empty() {
284 ConfigOperation::RemoveToolchain
285 } else {
286 ConfigOperation::SetToolchain(t)
287 })
288 .into_iter()
289 .chain(matches.remove_many("feature").into_iter().flatten().map(ConfigOperation::AddFeature))
290 .chain(matches.remove_many("no-feature").into_iter().flatten().map(ConfigOperation::RemoveFeature))
291 .chain(matches.remove_one("default-features").map(ConfigOperation::DefaultFeatures))
292 .chain(match (matches.remove_one("debug").unwrap_or(false),
293 matches.remove_one("release").unwrap_or(false),
294 matches.remove_one::<String>("build-profile")) {
295 (true, _, _) => Some(ConfigOperation::SetBuildProfile("dev".into())),
296 (_, true, _) => Some(ConfigOperation::SetBuildProfile("release".into())),
297 (_, _, Some(prof)) => Some(ConfigOperation::SetBuildProfile(prof.into())),
298 _ => None,
299 })
300 .chain(match (matches.remove_one("install-prereleases").unwrap_or(false), matches.remove_one("no-install-prereleases").unwrap_or(false)) {
301 (true, _) => Some(ConfigOperation::SetInstallPrereleases(true)),
302 (_, true) => Some(ConfigOperation::SetInstallPrereleases(false)),
303 _ => None,
304 })
305 .chain(match (matches.remove_one("enforce-lock").unwrap_or(false), matches.remove_one("no-enforce-lock").unwrap_or(false)) {
306 (true, _) => Some(ConfigOperation::SetEnforceLock(true)),
307 (_, true) => Some(ConfigOperation::SetEnforceLock(false)),
308 _ => None,
309 })
310 .chain(match (matches.remove_one("respect-binaries").unwrap_or(false), matches.remove_one("no-respect-binaries").unwrap_or(false)) {
311 (true, _) => Some(ConfigOperation::SetRespectBinaries(true)),
312 (_, true) => Some(ConfigOperation::SetRespectBinaries(false)),
313 _ => None,
314 })
315 .chain(match (matches.remove_one("any-version").unwrap_or(false), matches.remove_one("version")) {
316 (true, _) => Some(ConfigOperation::RemoveTargetVersion),
317 (false, Some(vr)) => Some(ConfigOperation::SetTargetVersion(vr)),
318 _ => None,
319 })
320 .chain(matches.remove_many("environment")
321 .into_iter()
322 .flatten()
323 .map(|(k, v)| ConfigOperation::SetEnvironment(k, v)))
324 .chain(matches.remove_many("clear-environment").into_iter().flatten().map(ConfigOperation::ClearEnvironment))
325 .chain(matches.remove_many("inherit-environment").into_iter().flatten().map(ConfigOperation::InheritEnvironment))
326 .chain(if matches.remove_one("reset").unwrap_or(false) {
327 Some(ConfigOperation::ResetConfig)
328 } else {
329 None
330 })
331 .collect(),
332 }
333 }
334}
335
336fn cargo_dir(opt_cargo_dir: Option<&PathBuf>) -> (PathBuf, PathBuf) {
337 if let Some(dir) = opt_cargo_dir {
338 match fs::canonicalize(&dir) {
339 Ok(cdir) => (dir.into(), cdir),
340 Err(_) => clerror(format_args!("--cargo-dir={:?} doesn't exist", dir)),
341 }
342 } else {
343 match env::var_os("CARGO_INSTALL_ROOT")
344 .map(PathBuf::from)
345 .or_else(|| home::cargo_home().ok())
346 .and_then(|ch| fs::canonicalize(&ch).map(|can| (ch, can)).ok()) {
347 Some(cd) => cd,
348 None => {
349 clerror(format_args!("$CARGO_INSTALL_ROOT, $CARGO_HOME, and home directory invalid, please specify the cargo home directory with the -c \
350 option"))
351 }
352 }
353 }
354}
355
356#[derive(Copy, Clone)]
357struct ExistingDirParser(&'static str);
358impl TypedValueParser for ExistingDirParser {
359 type Value = PathBuf;
360
361 fn parse_ref(&self, cmd: &Command, arg: Option<&Arg>, value: &OsStr) -> Result<Self::Value, ClapError> {
362 fs::canonicalize(value).map_err(|_| {
363 ClapError::raw(ClapErrorKind::InvalidValue,
364 format_args!("{}: {} directory \"{}\" not found", arg.unwrap(), self.0, Path::new(value).display())) .with_cmd(cmd)
366 })
367 }
368}
369
370fn package_parse(mut s: &str) -> Result<(String, Option<Semver>, Option<String>), String> {
371 let mut registry_url = None;
372 if s.starts_with('(') {
373 if let Some(idx) = s.find("):") {
374 registry_url = Some(&s[1..idx]);
375 s = &s[idx + 2..];
376 }
377 }
378
379 if let Some(idx) = s.find(':') {
380 Ok((s[0..idx].to_string(),
381 Some(Semver::parse(&s[idx + 1..]).map_err(|e| format!("Version {} provided for package {} invalid: {}", &s[idx + 1..], &s[0..idx], e))?),
382 registry_url.map(str::to_string)))
383 } else {
384 Ok((s.to_string(), None, registry_url.map(str::to_string)))
385 }
386}
387
388fn duration_parse(s: &str) -> Result<TimeDelta, String> {
389 const MULS_S: [char; 6] = ['y', 'w', 'd', 'h', 'm', 's'];
390 const MULS_V: [f64; 6] = [365.25 / 7., 7., 24., 60., 60., 1.];
391 let (base, mul) = s.strip_suffix(MULS_S).map(|stripped| (stripped, *s.as_bytes().last().unwrap() as _)).unwrap_or((s, 's'));
392 let base = f64::from_str(base).map_err(|e| e.to_string())?;
393 let val = MULS_V[MULS_S.iter().position(|&c| c == mul).unwrap()..].iter().fold(base, |a, e| a * e);
394 let (s, ns) = (val.trunc() as i64, (val.fract() * 1_000_000_000.0) as u32);
395 TimeDelta::new(s, ns).ok_or_else(|| format!("{}.{:09} too big", s, ns))
396}
397
398#[derive(Copy, Clone)]
399struct JobsParser(&'static str, NonZero<usize>);
400impl TypedValueParser for JobsParser {
401 type Value = NonZero<usize>;
402 fn parse_ref(&self, cmd: &Command, arg: Option<&Arg>, value: &OsStr) -> Result<Self::Value, ClapError> {
403 let value = value.to_str().ok_or(ClapError::new(ClapErrorKind::InvalidValue).with_cmd(cmd))?;
404 let Self(special, default) = *self;
405
406 if value != special {
407 if value.starts_with("-") {
408 NonZero::from_str(&value[1..]).map(|sub| if sub >= default {
409 NonZero::new(1).unwrap()
410 } else {
411 NonZero::new(default.get() - sub.get()).unwrap()
412 })
413 } else {
414 NonZero::from_str(value)
415 }
416 .map_err(|e| ClapError::raw(ClapErrorKind::InvalidValue, format_args!("{}: {}", arg.unwrap(), e)).with_cmd(cmd))
417 } else {
418 Ok(default)
419 }
420 }
421}
422
423#[derive(Copy, Clone)]
424struct DefaultFeaturesBoolParser;
425impl TypedValueParser for DefaultFeaturesBoolParser {
426 type Value = bool;
427
428 fn parse_ref(&self, cmd: &Command, arg: Option<&Arg>, value: &OsStr) -> Result<Self::Value, ClapError> {
429 match value.to_str().ok_or(ClapError::new(ClapErrorKind::InvalidValue).with_cmd(cmd))? {
430 "1" | "yes" | "true" => Ok(true),
431 "0" | "no" | "false" => Ok(false),
432 value => {
433 Err(ClapError::raw(ClapErrorKind::InvalidValue,
434 format_args!("{}: {} not 1|yes|true or 0|no|false", arg.unwrap(), value))
435 .with_cmd(cmd))
436 }
437 }
438 }
439
440 fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
441 Some(Box::new(["1", "yes", "true", "0", "no", "false"].iter().map(PossibleValue::new)))
442 }
443}
444
445
446fn clerror(f: Arguments) -> ! {
447 eprintln!("{}", f);
448 exit(1)
449}