cargo_local_install/
lib.rs

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
17/// An opaque `cargo-local-install` error, currently meant for [Display] only.
18pub 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
67/// Run an install after reading the executable name / subcommand.
68/// Will `exit(...)`.
69///
70/// ## Example
71/// ```no_run
72/// fn main() {
73///     let mut args = std::env::args_os();
74///     let _cargo_exe  = args.next(); // "cargo.exe"
75///     let _subcommand = args.next(); // "local-install"
76///     cargo_local_install::exec_from_args_os_after_exe(args);
77/// }
78/// ```
79pub 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
84/// Run an install after reading the executable name / subcommand.
85///
86/// ## Example
87/// ```no_run
88/// fn main() {
89///     let mut args = std::env::args_os();
90///     let _cargo_exe  = args.next(); // "cargo.exe"
91///     let _subcommand = args.next(); // "local-install"
92///     cargo_local_install::run_from_args_os_after_exe(args).unwrap();
93/// }
94/// ```
95pub fn run_from_args_os_after_exe(args: ArgsOs) -> Result<(), Error> {
96    run_from_strs(args)
97}
98
99/// Run an install based on string arguments.
100///
101/// ## Example
102/// ```no_run
103/// # fn a() -> Result<(), Box<dyn std::error::Error>> {
104/// # use std::ffi::*;
105/// # use cargo_local_install::run_from_strs;
106/// // &str s
107/// run_from_strs(["cargo-web", "--version", "^0.6"].iter())?;
108/// run_from_strs(["cargo-web", "--version", "^0.6"].into_iter())?;
109///
110/// // String s
111/// let s = ["cargo-web", "--version", "^0.6"];
112/// let s = s.iter().copied().map(String::from).collect::<Vec<String>>();
113/// run_from_strs(s.iter())?;
114/// run_from_strs(s.into_iter())?;
115///
116/// // &OsStr s
117/// let os = ["cargo-web", "--version", "^0.6"];
118/// let os = os.iter().map(OsStr::new).collect::<Vec<&OsStr>>();
119/// run_from_strs(os.iter())?;
120/// run_from_strs(os.into_iter())?;
121///
122/// // OsString s
123/// let os = ["cargo-web", "--version", "^0.6"];
124/// let os = os.iter().map(OsString::from).collect::<Vec<OsString>>();
125/// run_from_strs(os.iter())?;
126/// run_from_strs(os.into_iter())?;
127/// # Ok(())
128/// # }
129/// ```
130pub 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    // 1.26.0 flag: https://github.com/rust-lang/cargo/commit/b83ef97efb5d8d2a0d15f3dcd84f3f5d65a98193
155    // 1.26.0 var:  https://github.com/rust-lang/cargo/blob/41480f5cc50863600e05aa17d13264c88070436a/src/cargo/core/features.rs#L315
156    // 1.47.0 var:  https://github.com/rust-lang/cargo/blob/becb4c282b8f37469efb8f5beda45a5501f9d367/src/cargo/core/features.rs#L509
157    let z_no_index_update_hack = ((1, 26, 0, true) ..= (1, 47, 0, true)).contains(&(maj,min,pat,stable));
158
159    // XXX: I'll likely relax either "Into<OsString>" or "AsRef<OsStr>", but I haven't decided which just yet.
160    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(); // will get reordered for improved caching
171    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            //"--version"     => return version(), // XXX: Conflicts with version selection flag
179
180            // We want to warn if `--locked` wasn't passed, since you probably wanted it
181            "--locked"      => locked = Some(true ),
182            "--unlocked"    => locked = Some(false), // new to cargo-local-install
183
184            // Custom-handled flags
185            "--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())), // new to cargo-local-install
187            "--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?)")), // https://github.com/rust-lang/cargo/issues/7169#issuecomment-515195574
193            "--offline"     => return Err(error!(None, "not yet implemented: --offline")),
194            "--dry-run"     => dry_run = true, // new to cargo-local-install
195            "--no-path-warning" => path_warning = false, // new to cargo-local-install
196
197            // pass-through single-arg commands
198            "-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            // pass-through single-arg commands
215            "--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            // pass-through multi-arg commands
226            "--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            // real trace will have "--root ...", but that depends on hash!
372            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"); // *cackles manically*
388        }
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(()); // XXX: Would be nice to log copied bins, but without building them we don't know what they are
398        } 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    /// ASCII prefix
460    pre:    &'static str,
461
462    /// ANSI colored prefix
463    prec:   &'static str,
464
465    /// postfix
466    post:   &'static str,
467}
468
469static IGNORE : &'static [Ignore] = &[
470    // We spam reinstalls for already installed stuff
471    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    // We spam "internal" .cargo\local-install paths
474    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    // Don't spam this per-crate that's silly, roll our own for the final output
479    Ignore { pre: "    Finished ", post: "", prec: "\u{1b}[0m\u{1b}[0m\u{1b}[1m\u{1b}[32m    Finished\u{1b}[0m " },
480
481    // Okay, we'll let *this* spam through...
482    //Ignore { pre: "  Installing ", post: "", prec: "\u{1b}[0m\u{1b}[0m\u{1b}[1m\u{1b}[32m  Installing\u{1b}[0m " },
483];
484
485
486
487/// Filters out bad warnings like:
488/// "\u{1b}[0m\u{1b}[0m\u{1b}[1m\u{1b}[33mwarning\u{1b}[0m\u{1b}[1m:\u{1b}[0m be sure to add `C:\\Users\\Name\\.cargo\\local-install\\crates\\e5ce6d367e4d6f3f\\bin` to your PATH to be able to run the installed binaries"
489fn 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    // pass-through options to `cargo install`
513    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, "        --list                                       list all installed packages and their versions // not supported
520    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, "        --no-track                                   Do not save tracking information")?; // not supported
523    // writeln!(o, "        --features <FEATURES>...                     Space or comma separated list of features to activate")?; // nyi
524    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, "        --bin <NAME>...                              Install only the specified binary")?; // nyi
529    writeln!(o, "        --bins                                       Install all binaries")?;
530    // writeln!(o, "        --example <NAME>...                          Install only the specified example")?; // nyi
531    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, "        --frozen                                     Require Cargo.lock and cache are up to date")?; // not supported
541    writeln!(o, "        --locked                                     Require Cargo.lock is up to date")?;
542    // writeln!(o, "        --offline                                    Run without accessing the network")?; // not supported
543    // CUSTOM FLAGS:
544    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, "    -Z <FLAG>...")?; // nyi
548    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    // TODO: (git hash, mod status, date) via build.rs nonsense?
571    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}