1use 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#[derive(Debug, Clone, Hash, PartialEq, Eq)]
31pub struct Options {
32 pub to_update: Vec<(String, Option<Semver>, Cow<'static, str>)>,
34 pub all: bool,
36 pub update: bool,
38 pub install: bool,
40 pub force: bool,
42 pub downdate: bool,
44 pub update_git: bool,
46 pub quiet: bool,
48 pub locked: bool,
51 pub filter: Vec<PackageFilterElement>,
53 pub cargo_dir: (PathBuf, PathBuf),
56 pub temp_dir: PathBuf,
58 pub cargo_install_args: Vec<OsString>,
60 pub install_cargo: Option<OsString>,
62 pub jobs: Option<OsString>,
64}
65
66#[derive(Debug, Clone, Hash, PartialEq, Eq)]
68pub struct ConfigOptions {
69 pub cargo_dir: PathBuf,
71 pub package: String,
73 pub ops: Vec<ConfigOperation>,
75}
76
77
78impl Options {
79 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 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}