1#![forbid(unsafe_code)]
2
3#[macro_use] mod macros;
4#[cfg( feature = "manifest") ] mod manifest;
5#[cfg(not(feature = "manifest"))] mod manifest { pub(super) fn find_cwd_installs(_maybe_dst_bin: Option<std::path::PathBuf>) -> Result<Vec<crate::InstallSet>, crate::Error> { Ok(Vec::new()) } }
6
7use std::env::ArgsOs;
8use std::fmt::{self, Display, Debug, Formatter, Write as _};
9use std::ffi::{OsStr, OsString};
10use std::hash::*;
11use std::io::{self, BufRead, BufReader};
12use std::path::*;
13use std::process::{Command, Stdio};
14
15
16
17pub struct Error(String, Option<Inner>);
19impl Display for Error { fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { write!(fmt, "{}", self.0) } }
20impl Debug for Error { fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { write!(fmt, "Error({:?})", self.0) } }
21impl std::error::Error for Error {}
22
23enum Inner { Io(io::Error) }
24impl From<io::Error> for Inner { fn from(err: io::Error) -> Self { Inner::Io(err) } }
25
26
27
28#[derive(Clone, Copy, PartialEq, Eq)]
29enum LogMode {
30 Quiet,
31 Normal,
32 Verbose,
33}
34
35#[derive(Debug)]
36struct InstallSet {
37 bin: PathBuf,
38 src: Option<PathBuf>,
39 installs: Vec<Install>,
40}
41
42impl InstallSet {
43 fn any_local(&self) -> bool { self.installs.iter().any(|i| i.is_local()) }
44 fn any_remote(&self) -> bool { self.installs.iter().any(|i| i.is_remote()) }
45}
46
47#[derive(Debug)]
48struct Install {
49 name: OsString,
50 flags: Vec<InstallFlag>,
51}
52
53impl Install {
54 fn is_local(&self) -> bool { self.flags.iter().any(|flag| flag.flag == "--path") }
55 fn is_remote(&self) -> bool { !self.is_local() }
56}
57
58#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)]
59struct InstallFlag {
60 flag: OsString,
61 args: Vec<OsString>,
62}
63impl InstallFlag {
64 fn new(flag: impl Into<OsString>, args: Vec<OsString>) -> Self { Self { flag: flag.into(), args } }
65}
66
67pub fn exec_from_args_os_after_exe(args: ArgsOs) -> ! {
80 run_from_args_os_after_exe(args).unwrap_or_else(|err| fatal!("{}", err));
81 std::process::exit(0);
82}
83
84pub fn run_from_args_os_after_exe(args: ArgsOs) -> Result<(), Error> {
96 run_from_strs(args)
97}
98
99pub fn run_from_strs<Args: Iterator<Item = Arg>, Arg: Into<OsString> + AsRef<OsStr>>(args: Args) -> Result<(), Error> {
131 let start = std::time::Instant::now();
132
133 let (maj, min, pat, stable) = Command::new("cargo").arg("--version").stderr(std::process::Stdio::null()).stdout(std::process::Stdio::piped()).output().map_or((0, 0, 0, false), |o|{
134 fn split_once<'a>(a: &'a str, sep: &str) -> Option<(&'a str, &'a str)> {
135 let i = a.find(sep);
136 i.map(|i| {
137 let (a,b) = a.split_at(i);
138 (a, &b[sep.len()..])
139 })
140 }
141
142 let o = &*String::from_utf8_lossy(&o.stdout);
143 let ver = o.split(' ').nth(1).unwrap_or("");
144 let (maj, ver) = split_once(ver, ".").unwrap_or((ver, ""));
145 let (min, ver) = split_once(ver, ".").unwrap_or((ver, ""));
146 let (pat, pre) = split_once(ver, "-").unwrap_or((ver, ""));
147 let maj = maj.parse().unwrap_or(0u32);
148 let min = min.parse().unwrap_or(0u32);
149 let pat = pat.parse().unwrap_or(0u32);
150 let stable = pre.is_empty();
151 (maj, min, pat, stable)
152 });
153
154 let z_no_index_update_hack = ((1, 26, 0, true) ..= (1, 47, 0, true)).contains(&(maj,min,pat,stable));
158
159 let mut args = args.peekable();
161
162 let mut dry_run = false;
163 let mut path_warning= true;
164 let mut log_mode = LogMode::Normal;
165 let mut locked = None;
166 let mut maybe_dst_bin = None;
167 let mut target_dir = None;
168 let mut path = None;
169
170 let mut options = Vec::<InstallFlag>::new(); let mut crates = Vec::<OsString>::new();
172
173 while let Some(arg) = args.next() {
174 let arg = arg.into();
175 let lossy = arg.to_string_lossy();
176 match &*lossy {
177 "--help" => return help(),
178 "--locked" => locked = Some(true ),
182 "--unlocked" => locked = Some(false), "--root" => maybe_dst_bin = Some(PathBuf::from(args.next().ok_or_else(|| error!(None, "--root must specify a directory"))?.into()).join("bin")),
186 "--out-bin" => maybe_dst_bin = Some(PathBuf::from(args.next().ok_or_else(|| error!(None, "--out-bin must specify a directory"))?.into())), "--target-dir" => target_dir = Some(canonicalize(PathBuf::from(args.next().ok_or_else(|| error!(None, "--target-dir must specify a directory"))?.into()))?),
188 "--path" => path = Some(canonicalize(PathBuf::from(args.next().ok_or_else(|| error!(None, "--path must specify a directory"))?.into()))?),
189 "--list" => return Err(error!(None, "not yet implemented: --list (should this list global cache or local bins?)")),
190 "--no-track" => return Err(error!(None, "not yet implemented: --no-track (the entire point of this crate is tracking...)")),
191 "-Z" => return Err(error!(None, "not yet implemented: -Z flags")),
192 "--frozen" => return Err(error!(None, "not yet implemented: --frozen (last I checked this never worked in cargo install anyways?)")), "--offline" => return Err(error!(None, "not yet implemented: --offline")),
194 "--dry-run" => dry_run = true, "--no-path-warning" => path_warning = false, "-q" | "--quiet" => {
199 log_mode = LogMode::Quiet;
200 options.push(InstallFlag::new(arg, Vec::new()));
201 },
202 "-v" | "--verbose" => {
203 log_mode = LogMode::Verbose;
204 options.push(InstallFlag::new(arg, Vec::new()));
205 },
206 "-j" | "--jobs" |
207 "-f" | "--force" |
208 "--all-features" | "--no-default-features" |
209 "--debug" | "--bins" | "--examples"
210 => {
211 options.push(InstallFlag::new(arg, Vec::new()));
212 },
213
214 "--version" |
216 "--git" | "--branch" | "--tag" | "--rev" |
217 "--profile" | "--target" |
218 "--index" | "--registry" |
219 "--color"
220 => {
221 let arg2 = args.next().ok_or_else(|| error!(None, "{} requires an argument", lossy))?.into();
222 options.push(InstallFlag::new(arg, vec![arg2]));
223 },
224
225 "--features" => return Err(error!(None, "not yet implemented: {}", lossy)),
227 "--bin" => return Err(error!(None, "not yet implemented: {}", lossy)),
228 "--example" => return Err(error!(None, "not yet implemented: {}", lossy)),
229
230 "--" => {
231 crates.extend(args.map(|a| a.into()));
232 break;
233 },
234
235 flag if flag.starts_with("-") => return Err(error!(None, "unrecognized flag: {}", flag)),
236 _krate => crates.push(arg),
237 }
238 }
239 let quiet = log_mode == LogMode::Quiet;
240 let verbose = log_mode == LogMode::Verbose;
241
242 let locked = locked.unwrap_or_else(|| {
243 if !crates.is_empty() { warnln!("either specify --locked to use the same dependencies the crate was built with, or --unlocked to get rid of this warning"); }
244 false
245 });
246 if locked {
247 options.push(InstallFlag::new("--locked", Vec::new()));
248 }
249
250 let mut installs = if crates.is_empty() {
251 manifest::find_cwd_installs(maybe_dst_bin.clone()).map_err(|err| error!(None, "error enumerating Cargo.tomls: {}", err))?
252 } else {
253 vec![InstallSet {
254 bin: maybe_dst_bin.clone().unwrap_or_else(|| PathBuf::from("bin")),
255 src: None,
256 installs: crates.into_iter().map(|c| Install { name: c, flags: vec![] }).collect(),
257 }]
258 };
259
260 if installs.is_empty() {
261 return Err(error!(None, "no crates specified"))
262 }
263
264 let global_dir = {
265 let var = if cfg!(windows) { "USERPROFILE" } else { "HOME" };
266 let mut d = PathBuf::from(std::env::var_os(var).ok_or_else(|| error!(None, "couldn't determine target dir, {} not set", var))?);
267 d.push(".cargo");
268 d.push("local-install");
269 d
270 };
271 let crates_cache_dir = global_dir.join("crates");
272
273 let target_dir = target_dir.map_or_else(|| Ok(global_dir.join("target")), |td| canonicalize(td))?;
274 options.push(InstallFlag::new("--target-dir", vec![target_dir.into()]));
275 if let Some(path) = path { options.push(InstallFlag::new("--path", vec![canonicalize(path)?.into()])); }
276 options.sort();
277
278 for set in installs.iter_mut() {
279 for install in set.installs.iter_mut() {
280 install.flags.extend(options.clone());
281 install.flags.sort();
282 }
283 }
284
285 for set in installs.into_iter() {
286 let any_local = set.any_local();
287 let any_remote = set.any_remote();
288 if set.installs.is_empty() { continue }
289 assert!(any_local || any_remote);
290
291 let built = set.bin.join(".built");
292
293 let up_to_date = if !any_remote {
294 false
295 } else if let Some(src) = set.src.as_ref() {
296 let src_mod = src.metadata().ok().and_then(|m| m.modified().ok());
297 let built_mod = built.metadata().ok().and_then(|m| m.modified().ok());
298
299 let up_to_date = match (src_mod, built_mod) {
300 (Some(src), Some(built)) => src < built,
301 _other => false,
302 };
303
304 if up_to_date && !any_local {
305 if verbose { statusln!("Skipping", "`{}`: up to date", src.display()); }
306 continue
307 }
308
309 up_to_date
310 } else {
311 false
312 };
313
314 let mut first_install = true;
315 for install in set.installs.into_iter() {
316 if install.is_remote() {
317 if up_to_date { continue }
318 }
319 let context = Context {
320 dry_run, quiet, verbose,
321 z_no_index_update_hack: z_no_index_update_hack && !first_install,
322 crates_cache_dir: crates_cache_dir.as_path(),
323 dst_bin: set.bin.as_path()
324 };
325 install.install(context)?;
326 first_install = false;
327 }
328 if any_remote && set.src.is_some() {
329 std::fs::write(&built, "").map_err(|err| error!(err, "unable to create {}: {}", built.display(), err))?;
330 }
331 }
332
333 let stop = std::time::Instant::now();
334 if !quiet { statusln!("Finished", "installing crate(s) in {:.2}s", (stop-start).as_secs_f32()); }
335 if path_warning {
336 if let Some(dst_bin) = maybe_dst_bin {
337 warnln!("be sure to add `{}` to your PATH to be able to run the installed binaries", dst_bin.display());
338 } else {
339 warnln!("be sure to add `$crate\\bin` path(s) to your PATH to be able to run the installed binaries");
340 }
341 }
342 Ok(())
343}
344
345struct Context<'a> {
346 pub dry_run: bool,
347 pub quiet: bool,
348 pub verbose: bool,
349 pub z_no_index_update_hack: bool,
350 pub crates_cache_dir: &'a Path,
351 pub dst_bin: &'a Path,
352}
353
354impl Install {
355 fn install(self, context: Context) -> Result<(), Error> {
356 let Context { dry_run, quiet, verbose, z_no_index_update_hack, crates_cache_dir, dst_bin } = context;
357
358 let mut trace = format!("cargo install");
359 let mut cmd = Command::new("cargo");
360 cmd.arg("install");
361 for InstallFlag { flag, args } in self.flags {
362 write!(&mut trace, " {}", flag.to_str().unwrap()).unwrap();
363 cmd.arg(flag);
364 for arg in args.into_iter() {
365 write!(&mut trace, " {:?}", arg).unwrap();
366 cmd.arg(arg);
367 }
368 }
369
370 let hash = {
371 let trace_for_hash = format!("{} -- {}", trace, self.name.to_string_lossy());
373 #[allow(deprecated)] let mut hasher = std::hash::SipHasher::new();
374 trace_for_hash.hash(&mut hasher);
375 format!("{:016x}", hasher.finish())
376 };
377
378 let krate_build_dir = crates_cache_dir.join(hash);
379 write!(&mut trace, " --root {:?}", krate_build_dir.display()).unwrap();
380 cmd.arg("--root").arg(&krate_build_dir);
381
382 write!(&mut trace, " --color always").unwrap();
383 cmd.arg("--color").arg("always");
384
385 if z_no_index_update_hack {
386 cmd.arg("-Z").arg("no-index-update");
387 cmd.env("__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS", "nightly"); }
389
390 trace.push_str(" -- ");
391 trace.push_str(&self.name.to_string_lossy());
392 cmd.arg("--");
393 cmd.arg(self.name);
394
395 if dry_run {
396 statusln!("Skipping", "`{}` (--dry-run)", trace);
397 return Ok(()); } else if verbose {
399 statusln!("Running", "`{}`", trace);
400 }
401
402 cmd.stderr(Stdio::piped());
403 let mut cmd = cmd.spawn().map_err(|err| error!(err, "failed to spawn {}: {}", trace, err))?;
404 let stderr_thread = cmd.stderr.take().map(|stderr| std::thread::spawn(|| filter_stderr(stderr)));
405 let status = cmd.wait();
406 let _stderr_thread = stderr_thread.map(|t| t.join());
407 let status = status.map_err(|err| error!(err, "failed to execute {}: {}", trace, err))?;
408 match status.code() {
409 Some(0) => { if verbose { statusln!("Succeeded", "`{}`", trace) } },
410 Some(n) => return Err(error!(None, "{} failed (exit code {})", trace, n)),
411 None => return Err(error!(None, "{} failed (signal)", trace)),
412 }
413
414 if let Err(err) = std::fs::create_dir_all(&dst_bin) {
415 if !quiet {
416 warnln!("Unable to create directory `{}`: {}", dst_bin.display(), err);
417 }
418 } else if verbose {
419 statusln!("Created", "`{}\\`", dst_bin.display());
420 }
421
422 let src_bin_path = krate_build_dir.join("bin");
423 let src_bins = src_bin_path.read_dir().map_err(|err| error!(err, "unable to enumerate source bins at {}: {}", src_bin_path.display(), err))?;
424 for src_bin in src_bins {
425 let src_bin = src_bin.map_err(|err| error!(err, "error enumerating source bins at {}: {}", src_bin_path.display(), err))?;
426 let dst_bin = dst_bin.join(src_bin.file_name());
427 let file_type = src_bin.file_type().map_err(|err| error!(err, "error determining file type for {}: {}", src_bin.path().display(), err))?;
428 if !file_type.is_file() { continue }
429 let src_bin = src_bin.path();
430
431 if verbose { statusln!("Replacing", "`{}`", dst_bin.display()) }
432 #[cfg(windows)] {
433 let _ = std::fs::remove_file(&dst_bin);
434 if let Err(err) = std::os::windows::fs::symlink_file(&src_bin, &dst_bin) {
435 if !quiet { warnln!("Unable to link `{}` to `{}`: {}", dst_bin.display(), src_bin.display(), err) }
436 } else {
437 if verbose { statusln!("Linked", "`{}` to `{}`", dst_bin.display(), src_bin.display()) }
438 continue
439 }
440 }
441 #[cfg(unix)] {
442 let _ = std::fs::remove_file(&dst_bin);
443 if let Err(err) = std::os::unix::fs::symlink(&src_bin, &dst_bin) {
444 if !quiet { warnln!("Unable to link `{}` to `{}`: {}", dst_bin.display(), src_bin.display(), err) }
445 } else {
446 if verbose { statusln!("Linked", "`{}` to `{}`", dst_bin.display(), src_bin.display()) }
447 continue
448 }
449 }
450 std::fs::copy(&src_bin, &dst_bin).map_err(|err| error!(err, "error replacing `{}` with `{}`: {}", dst_bin.display(), src_bin.display(), err))?;
451 if !quiet { statusln!("Replaced", "`{}` with `{}`", dst_bin.display(), src_bin.display()) }
452 }
453
454 Ok(())
455 }
456}
457
458struct Ignore {
459 pre: &'static str,
461
462 prec: &'static str,
464
465 post: &'static str,
467}
468
469static IGNORE : &'static [Ignore] = &[
470 Ignore { pre: " Ignored package `", post: "` is already installed, use --force to override", prec: "\u{1b}[0m\u{1b}[0m\u{1b}[1m\u{1b}[32m Ignored\u{1b}[0m package `" },
472
473 Ignore { pre: "warning: be sure to add `", post: "` to your PATH to be able to run the installed binaries", prec: "\x1B[0m\x1B[0m\x1B[1m\x1B[33mwarning\x1B[0m\x1B[1m:\x1B[0m be sure to add `" },
475 Ignore { pre: " Replacing ", post: "", prec: "\u{1b}[0m\u{1b}[0m\u{1b}[1m\u{1b}[32m Replacing\u{1b}[0m " },
476 Ignore { pre: " Replaced ", post: "", prec: "\u{1b}[0m\u{1b}[0m\u{1b}[1m\u{1b}[32m Replaced\u{1b}[0m " },
477
478 Ignore { pre: " Finished ", post: "", prec: "\u{1b}[0m\u{1b}[0m\u{1b}[1m\u{1b}[32m Finished\u{1b}[0m " },
480
481 ];
484
485
486
487fn filter_stderr(input: std::process::ChildStderr) -> io::Result<()> {
490 for line in BufReader::new(input).lines() {
491 let line = line?;
492 if IGNORE.iter().any(|ignore| line.ends_with(ignore.post) && (line.starts_with(ignore.pre) || line.starts_with(ignore.prec))) { continue }
493 eprintln!("{}", line);
494 }
495 Ok(())
496}
497
498fn help() -> Result<(), Error> {
499 print_usage(&mut std::io::stdout().lock()).map_err(|err| error!(err, "unable to write help text to stdout: {}", err))
500}
501
502fn print_usage(mut o: impl io::Write) -> io::Result<()> {
503 let o = &mut o;
504 writeln!(o, "cargo-local-install")?;
505 writeln!(o, "Install a Rust binary. Default installation location is ./bin")?;
506 writeln!(o)?;
507 writeln!(o, "USAGE:")?;
508 writeln!(o, " cargo local-install [OPTIONS] [--] [crate]...")?;
509 writeln!(o, " cargo-local-install [OPTIONS] [--] [crate]...")?;
510 writeln!(o)?;
511 writeln!(o, "OPTIONS:")?;
512 writeln!(o, " -q, --quiet No output printed to stdout")?;
514 writeln!(o, " --version <VERSION> Specify a version to install")?;
515 writeln!(o, " --git <URL> Git URL to install the specified crate from")?;
516 writeln!(o, " --tag <TAG> Tag to use when installing from git")?;
517 writeln!(o, " --rev <SHA> Specific commit to use when installing from git")?;
518 writeln!(o, " --path <PATH> Filesystem path to local crate to install")?;
519 writeln!(o, " -j, --jobs <N> Number of parallel jobs, defaults to # of CPUs")?;
521 writeln!(o, " -f, --force Force overwriting existing crates or binaries")?;
522 writeln!(o, " --all-features Activate all available features")?;
525 writeln!(o, " --no-default-features Do not activate the `default` feature")?;
526 writeln!(o, " --profile <PROFILE-NAME> Install artifacts with the specified profile")?;
527 writeln!(o, " --debug Build in debug mode instead of release mode")?;
528 writeln!(o, " --bins Install all binaries")?;
530 writeln!(o, " --examples Install all examples")?;
532 writeln!(o, " --target <TRIPLE> Build for the target triple")?;
533 writeln!(o, " --target-dir <DIRECTORY> Directory for all generated artifacts")?;
534 writeln!(o, " --root <DIR> Install package bins into <DIR>/bin")?;
535 writeln!(o, " --out-bin <DIR> Install package bins into <DIR>")?;
536 writeln!(o, " --index <INDEX> Registry index to install from")?;
537 writeln!(o, " --registry <REGISTRY> Registry to use")?;
538 writeln!(o, " -v, --verbose Use verbose output (-vv very verbose/build.rs output)")?;
539 writeln!(o, " --color <WHEN> Coloring: auto, always, never")?;
540 writeln!(o, " --locked Require Cargo.lock is up to date")?;
542 writeln!(o, " --unlocked Don't require an up-to-date Cargo.lock")?;
545 writeln!(o, " --dry-run Print `cargo install ...` spam but don't actually install")?;
546 writeln!(o, " --no-path-warning Don't remind the user to add `bin` to their PATH")?;
547 writeln!(o)?;
549 writeln!(o, "ARGS:")?;
550 writeln!(o, " <crate>...")?;
551 writeln!(o)?;
552 writeln!(o, "This command wraps `cargo install` to solve a couple of problems with using")?;
553 writeln!(o, "the basic command directly:")?;
554 writeln!(o)?;
555 writeln!(o, "* The global `~/.cargo/bin` directory can contain only a single installed")?;
556 writeln!(o, " version of a package at a time - if you've got one project relying on")?;
557 writeln!(o, " `cargo web 0.5` and another prjoect relying on `cargo web 0.6`, you're SOL.")?;
558 writeln!(o)?;
559 writeln!(o, "* Forcing local installs with `--root my/project` to avoid global version")?;
560 writeln!(o, " conflicts means you must rebuild the entire dependency for each project,")?;
561 writeln!(o, " even when you use the exact same version for 100 other projects before.")?;
562 writeln!(o)?;
563 writeln!(o, "* When building similar binaries, the lack of target directory caching means")?;
564 writeln!(o, " the entire dependency tree must still be rebuilt from scratch.")?;
565 Ok(())
566}
567
568#[allow(dead_code)]
569fn version() {
570 println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
572}
573
574fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf, Error> {
575 let path = path.as_ref();
576 let path = std::fs::canonicalize(path).map_err(|err| error!(err, "unable to canonicalize {}: {}", path.display(), err))?;
577 let mut o = PathBuf::new();
578 for component in path.components() {
579 if let Component::Prefix(pre) = component {
580 match pre.kind() {
581 Prefix::VerbatimDisk(disk) => o.push(format!("{}:", disk as char)),
582 _other => o.push(component),
583 }
584 } else {
585 o.push(component);
586 }
587 }
588 Ok(o)
589}