From f8308bc54345fe52ef3a24d98c4c38d92180662d Mon Sep 17 00:00:00 2001 From: Steve Klabnik Date: Mon, 23 Feb 2015 00:06:12 +0100 Subject: [PATCH 1/2] Basic work for cargo install --- src/bin/cargo.rs | 1 + src/bin/install.rs | 89 ++++++++++++++++++++++++++++++++++ src/cargo/ops/cargo_install.rs | 17 +++++++ src/cargo/ops/mod.rs | 2 + tests/support/mod.rs | 1 + 5 files changed, 110 insertions(+) create mode 100644 src/bin/install.rs create mode 100644 src/cargo/ops/cargo_install.rs diff --git a/src/bin/cargo.rs b/src/bin/cargo.rs index bfa763852e0..11019d1ead4 100644 --- a/src/bin/cargo.rs +++ b/src/bin/cargo.rs @@ -69,6 +69,7 @@ macro_rules! each_subcommand{ ($mac:ident) => ({ $mac!(generate_lockfile); $mac!(git_checkout); $mac!(help); + $mac!(install); $mac!(locate_project); $mac!(login); $mac!(new); diff --git a/src/bin/install.rs b/src/bin/install.rs new file mode 100644 index 00000000000..8f443341b84 --- /dev/null +++ b/src/bin/install.rs @@ -0,0 +1,89 @@ +use cargo::ops; +use cargo::util::{CliResult, CliError, Config}; +use std::path::Path; + +#[allow(dead_code)] // for now until all options are implemented + +#[derive(RustcDecodable)] +struct Options { + flag_jobs: Option, + flag_features: Vec, + flag_no_default_features: bool, + flag_debug: bool, + flag_bin: Option, + flag_example: Vec, + flag_package: Vec, + flag_verbose: bool, + flag_root: Option, +} + +pub const USAGE: &'static str = " +Install a crate onto the local system + +Installing new crates: + cargo install [options] + cargo install [options] [-p CRATE | --package CRATE] [--vers VERS] + cargo install [options] --git URL [--branch BRANCH | --tag TAG | --rev SHA] + cargo install [options] --path PATH + +Managing installed crates: + cargo install [options] --list + +Options: + -h, --help Print this message + -j N, --jobs N The number of jobs to run in parallel + --features FEATURES Space-separated list of features to activate + --no-default-features Do not build the `default` feature + --debug Build in debug mode instead of release mode + --bin NAME Only install the binary NAME + --example EXAMPLE Install the example EXAMPLE instead of binaries + -p, --package CRATE Install this crate from crates.io or select the + package in a repository/path to install. + -v, --verbose Use verbose output + --root DIR Directory to install packages into + +This command manages Cargo's local set of install binary crates. Only packages +which have [[bin]] targets can be installed, and all binaries are installed into +`$HOME/.cargo/bin` by default (or `$CARGO_HOME/bin` if you change the home +directory). + +There are multiple methods of installing a new crate onto the system. The +`cargo install` command with no arguments will install the current crate (as +specifed by the current directory). Otherwise the `-p`, `--package`, `--git`, +and `--path` options all specify the source from which a crate is being +installed. The `-p` and `--package` options will download crates from crates.io. + +Crates from crates.io can optionally specify the version they wish to install +via the `--vers` flags, and similarly packages from git repositories can +optionally specify the branch, tag, or revision that should be installed. If a +crate has multiple binaries, the `--bin` argument can selectively install only +one of them, and if you'd rather install examples the `--example` argument can +be used as well. + +The `--list` option will list all installed packages (and their versions). +"; + +pub fn execute(options: Options, config: &Config) -> CliResult> { + config.shell().set_verbose(options.flag_verbose); + + let compile_opts = ops::CompileOptions { + config: config, + jobs: options.flag_jobs, + target: None, + features: &options.flag_features, + no_default_features: options.flag_no_default_features, + spec: None, + exec_engine: None, + mode: ops::CompileMode::Build, + release: true, + filter: ops::CompileFilter::Everything, + target_rustc_args: None, + }; + + let root = &Path::new("$HOME/.cargo/bin"); + + ops::install(&root, + &compile_opts).map_err(|err| { + CliError::from_boxed(err, 101) + }).map(|_| None) +} diff --git a/src/cargo/ops/cargo_install.rs b/src/cargo/ops/cargo_install.rs new file mode 100644 index 00000000000..2bd65cc0dd6 --- /dev/null +++ b/src/cargo/ops/cargo_install.rs @@ -0,0 +1,17 @@ +use ops; +use util::CargoResult; +use sources::PathSource; +use std::path::Path; + +pub fn install(manifest_path: &Path, + opts: &ops::CompileOptions) -> CargoResult<()> { + let config = opts.config; + let src = try!(PathSource::for_path(manifest_path.parent().unwrap(), + config)); + let _root = try!(src.root_package()); + + println!("Compiling"); + try!(ops::compile(manifest_path, opts)); + + Ok(()) +} diff --git a/src/cargo/ops/mod.rs b/src/cargo/ops/mod.rs index f408b093aa9..5aa156cd61f 100644 --- a/src/cargo/ops/mod.rs +++ b/src/cargo/ops/mod.rs @@ -7,6 +7,7 @@ pub use self::cargo_rustc::{Context, LayoutProxy}; pub use self::cargo_rustc::{BuildOutput, BuildConfig, TargetConfig}; pub use self::cargo_rustc::{CommandType, CommandPrototype, ExecEngine, ProcessEngine}; pub use self::cargo_run::run; +pub use self::cargo_install::install; pub use self::cargo_new::{new, NewOptions, VersionControl}; pub use self::cargo_doc::{doc, DocOptions}; pub use self::cargo_generate_lockfile::{generate_lockfile}; @@ -28,6 +29,7 @@ mod cargo_compile; mod cargo_doc; mod cargo_fetch; mod cargo_generate_lockfile; +mod cargo_install; mod cargo_new; mod cargo_package; mod cargo_pkgid; diff --git a/tests/support/mod.rs b/tests/support/mod.rs index 38e4348705d..90b1dfac81f 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -563,3 +563,4 @@ pub static DOWNLOADING: &'static str = " Downloading"; pub static UPLOADING: &'static str = " Uploading"; pub static VERIFYING: &'static str = " Verifying"; pub static ARCHIVING: &'static str = " Archiving"; +pub static INSTALLED: &'static str = " Installed"; From bc60f64b13d267dcc3b4c0194944a678182a610e Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 5 Oct 2015 15:29:15 -0700 Subject: [PATCH 2/2] Finish implementing `cargo install` This commit is an implementation of [RFC 1200][rfc] which brings two new subcommands: `cargo install` and `cargo uninstall`. Most of this is a straight implementation of the RFC, but a few tweaks were made: * The `-p` or `--package` arguments are no longer needed as you just pass `crate` as a bare argument to the command, this means `cargo install foo` works and downloads from crates.io by default. * Some logic around selecting which crate in a multi-crate repo is installed has been tweaked slightly, but mostly in the realm of "let's do the thing that makes sense" rather than the literal "let's do what's in the RFC". Specifically, we don't pick a crate with examples if there are multiple crates with binaries (instead an error is generated saying there are multiple binary crates). [rfc]: https://github.com/rust-lang/rfcs/blob/master/text/1200-cargo-install.md --- src/bin/cargo.rs | 1 + src/bin/install.rs | 123 +++++--- src/bin/uninstall.rs | 43 +++ src/cargo/core/package_id_spec.rs | 56 ++++ src/cargo/core/registry.rs | 13 +- src/cargo/core/resolver/mod.rs | 52 +--- src/cargo/core/source.rs | 12 +- src/cargo/ops/cargo_compile.rs | 7 +- src/cargo/ops/cargo_install.rs | 341 +++++++++++++++++++- src/cargo/ops/cargo_package.rs | 2 +- src/cargo/ops/cargo_pkgid.rs | 2 +- src/cargo/ops/mod.rs | 2 +- src/cargo/ops/resolve.rs | 4 +- src/cargo/sources/git/source.rs | 7 + src/cargo/sources/path.rs | 2 +- src/cargo/sources/registry.rs | 11 +- src/cargo/util/config.rs | 14 +- tests/support/mod.rs | 2 +- tests/support/registry.rs | 8 +- tests/test_cargo_install.rs | 500 ++++++++++++++++++++++++++++++ tests/tests.rs | 1 + 21 files changed, 1080 insertions(+), 123 deletions(-) create mode 100644 src/bin/uninstall.rs create mode 100644 tests/test_cargo_install.rs diff --git a/src/bin/cargo.rs b/src/bin/cargo.rs index 11019d1ead4..f21ffb1d5c5 100644 --- a/src/bin/cargo.rs +++ b/src/bin/cargo.rs @@ -82,6 +82,7 @@ macro_rules! each_subcommand{ ($mac:ident) => ({ $mac!(rustc); $mac!(search); $mac!(test); + $mac!(uninstall); $mac!(update); $mac!(verify_project); $mac!(version); diff --git a/src/bin/install.rs b/src/bin/install.rs index 8f443341b84..7ae2a130aff 100644 --- a/src/bin/install.rs +++ b/src/bin/install.rs @@ -1,8 +1,8 @@ -use cargo::ops; -use cargo::util::{CliResult, CliError, Config}; use std::path::Path; -#[allow(dead_code)] // for now until all options are implemented +use cargo::ops; +use cargo::core::{SourceId, GitReference}; +use cargo::util::{CliResult, Config, ToUrl, human}; #[derive(RustcDecodable)] struct Options { @@ -10,48 +10,65 @@ struct Options { flag_features: Vec, flag_no_default_features: bool, flag_debug: bool, - flag_bin: Option, + flag_bin: Vec, flag_example: Vec, - flag_package: Vec, flag_verbose: bool, + flag_quiet: bool, + flag_color: Option, flag_root: Option, + flag_list: bool, + + arg_crate: Option, + flag_vers: Option, + + flag_git: Option, + flag_branch: Option, + flag_tag: Option, + flag_rev: Option, + + flag_path: Option, } pub const USAGE: &'static str = " -Install a crate onto the local system +Install a Rust binary -Installing new crates: - cargo install [options] - cargo install [options] [-p CRATE | --package CRATE] [--vers VERS] - cargo install [options] --git URL [--branch BRANCH | --tag TAG | --rev SHA] - cargo install [options] --path PATH - -Managing installed crates: +Usage: + cargo install [options] [] cargo install [options] --list -Options: - -h, --help Print this message - -j N, --jobs N The number of jobs to run in parallel - --features FEATURES Space-separated list of features to activate - --no-default-features Do not build the `default` feature - --debug Build in debug mode instead of release mode - --bin NAME Only install the binary NAME - --example EXAMPLE Install the example EXAMPLE instead of binaries - -p, --package CRATE Install this crate from crates.io or select the - package in a repository/path to install. - -v, --verbose Use verbose output - --root DIR Directory to install packages into +Specifying what crate to install: + --vers VERS Specify a version to install from crates.io + --git URL Git URL to install the specified crate from + --branch BRANCH Branch to use when installing from git + --tag TAG Tag to use when installing from git + --rev SHA Specific commit to use when installing from git + --path PATH Filesystem path to local crate to install + +Build and install options: + -h, --help Print this message + -j N, --jobs N The number of jobs to run in parallel + --features FEATURES Space-separated list of features to activate + --no-default-features Do not build the `default` feature + --debug Build in debug mode instead of release mode + --bin NAME Only install the binary NAME + --example EXAMPLE Install the example EXAMPLE instead of binaries + --root DIR Directory to install packages into + -v, --verbose Use verbose output + -q, --quiet Less output printed to stdout + --color WHEN Coloring: auto, always, never This command manages Cargo's local set of install binary crates. Only packages which have [[bin]] targets can be installed, and all binaries are installed into -`$HOME/.cargo/bin` by default (or `$CARGO_HOME/bin` if you change the home -directory). +the installation root's `bin` folder. The installation root is determined, in +order of precedence, by `--root`, `$CARGO_INSTALL_ROOT`, the `install.root` +configuration key, and finally the home directory (which is either +`$CARGO_HOME` if set or `$HOME/.cargo` by default). -There are multiple methods of installing a new crate onto the system. The -`cargo install` command with no arguments will install the current crate (as -specifed by the current directory). Otherwise the `-p`, `--package`, `--git`, -and `--path` options all specify the source from which a crate is being -installed. The `-p` and `--package` options will download crates from crates.io. +There are multiple sources from which a crate can be installed. The default +location is crates.io but the `--git` and `--path` flags can change this source. +If the source contains more than one package (such as crates.io or a git +repository with multiple crates) the `` argument is required to indicate +which crate should be installed. Crates from crates.io can optionally specify the version they wish to install via the `--vers` flags, and similarly packages from git repositories can @@ -64,7 +81,8 @@ The `--list` option will list all installed packages (and their versions). "; pub fn execute(options: Options, config: &Config) -> CliResult> { - config.shell().set_verbose(options.flag_verbose); + try!(config.shell().set_verbosity(options.flag_verbose, options.flag_quiet)); + try!(config.shell().set_color_config(options.flag_color.as_ref().map(|s| &s[..]))); let compile_opts = ops::CompileOptions { config: config, @@ -72,18 +90,41 @@ pub fn execute(options: Options, config: &Config) -> CliResult> { target: None, features: &options.flag_features, no_default_features: options.flag_no_default_features, - spec: None, + spec: &[], exec_engine: None, mode: ops::CompileMode::Build, - release: true, - filter: ops::CompileFilter::Everything, + release: !options.flag_debug, + filter: ops::CompileFilter::new(false, &options.flag_bin, &[], + &options.flag_example, &[]), target_rustc_args: None, }; - let root = &Path::new("$HOME/.cargo/bin"); + let source = if let Some(url) = options.flag_git { + let url = try!(url.to_url().map_err(human)); + let gitref = if let Some(branch) = options.flag_branch { + GitReference::Branch(branch) + } else if let Some(tag) = options.flag_tag { + GitReference::Tag(tag) + } else if let Some(rev) = options.flag_rev { + GitReference::Rev(rev) + } else { + GitReference::Branch("master".to_string()) + }; + SourceId::for_git(&url, gitref) + } else if let Some(path) = options.flag_path { + try!(SourceId::for_path(Path::new(&path))) + } else { + try!(SourceId::for_central(config)) + }; + + let krate = options.arg_crate.as_ref().map(|s| &s[..]); + let vers = options.flag_vers.as_ref().map(|s| &s[..]); + let root = options.flag_root.as_ref().map(|s| &s[..]); - ops::install(&root, - &compile_opts).map_err(|err| { - CliError::from_boxed(err, 101) - }).map(|_| None) + if options.flag_list { + try!(ops::install_list(root, config)); + } else { + try!(ops::install(root, krate, &source, vers, &compile_opts)); + } + Ok(None) } diff --git a/src/bin/uninstall.rs b/src/bin/uninstall.rs new file mode 100644 index 00000000000..20d4b579914 --- /dev/null +++ b/src/bin/uninstall.rs @@ -0,0 +1,43 @@ +use cargo::ops; +use cargo::util::{CliResult, Config}; + +#[derive(RustcDecodable)] +struct Options { + flag_bin: Vec, + flag_root: Option, + flag_verbose: bool, + flag_quiet: bool, + flag_color: Option, + + arg_spec: String, +} + +pub const USAGE: &'static str = " +Remove a Rust binary + +Usage: + cargo uninstall [options] + +Options: + -h, --help Print this message + --root DIR Directory to uninstall packages from + --bin NAME Only uninstall the binary NAME + -v, --verbose Use verbose output + -q, --quiet Less output printed to stdout + --color WHEN Coloring: auto, always, never + +The argument SPEC is a package id specification (see `cargo help pkgid`) to +specify which crate should be uninstalled. By default all binaries are +uninstalled for a crate but the `--bin` and `--example` flags can be used to +only uninstall particular binaries. +"; + +pub fn execute(options: Options, config: &Config) -> CliResult> { + try!(config.shell().set_verbosity(options.flag_verbose, options.flag_quiet)); + try!(config.shell().set_color_config(options.flag_color.as_ref().map(|s| &s[..]))); + + let root = options.flag_root.as_ref().map(|s| &s[..]); + try!(ops::uninstall(root, &options.arg_spec, &options.flag_bin, config)); + Ok(None) +} + diff --git a/src/cargo/core/package_id_spec.rs b/src/cargo/core/package_id_spec.rs index a950e6006bc..15b191e9697 100644 --- a/src/cargo/core/package_id_spec.rs +++ b/src/cargo/core/package_id_spec.rs @@ -1,4 +1,6 @@ +use std::collections::HashMap; use std::fmt; + use semver::Version; use url::{self, Url, UrlParser}; @@ -45,6 +47,15 @@ impl PackageIdSpec { }) } + pub fn query_str<'a, I>(spec: &str, i: I) -> CargoResult<&'a PackageId> + where I: IntoIterator + { + let spec = try!(PackageIdSpec::parse(spec).chain_error(|| { + human(format!("invalid package id specification: `{}`", spec)) + })); + spec.query(i) + } + pub fn from_package_id(package_id: &PackageId) -> PackageIdSpec { PackageIdSpec { name: package_id.name().to_string(), @@ -115,6 +126,51 @@ impl PackageIdSpec { None => true } } + + pub fn query<'a, I>(&self, i: I) -> CargoResult<&'a PackageId> + where I: IntoIterator + { + let mut ids = i.into_iter().filter(|p| self.matches(*p)); + let ret = match ids.next() { + Some(id) => id, + None => return Err(human(format!("package id specification `{}` \ + matched no packages", self))), + }; + return match ids.next() { + Some(other) => { + let mut msg = format!("There are multiple `{}` packages in \ + your project, and the specification \ + `{}` is ambiguous.\n\ + Please re-run this command \ + with `-p ` where `` is one \ + of the following:", + self.name(), self); + let mut vec = vec![ret, other]; + vec.extend(ids); + minimize(&mut msg, vec, self); + Err(human(msg)) + } + None => Ok(ret) + }; + + fn minimize(msg: &mut String, + ids: Vec<&PackageId>, + spec: &PackageIdSpec) { + let mut version_cnt = HashMap::new(); + for id in ids.iter() { + *version_cnt.entry(id.version()).or_insert(0) += 1; + } + for id in ids.iter() { + if version_cnt[id.version()] == 1 { + msg.push_str(&format!("\n {}:{}", spec.name(), + id.version())); + } else { + msg.push_str(&format!("\n {}", + PackageIdSpec::from_package_id(*id))); + } + } + } + } } fn url(s: &str) -> url::ParseResult { diff --git a/src/cargo/core/registry.rs b/src/cargo/core/registry.rs index 1c21e0d9607..037c5d4174e 100644 --- a/src/cargo/core/registry.rs +++ b/src/cargo/core/registry.rs @@ -154,6 +154,16 @@ impl<'cfg> PackageRegistry<'cfg> { Ok(()) } + pub fn add_preloaded(&mut self, id: &SourceId, source: Box) { + self.add_source(id, source, Kind::Locked); + } + + fn add_source(&mut self, id: &SourceId, source: Box, + kind: Kind) { + self.sources.insert(id, source); + self.source_ids.insert(id.clone(), (id.clone(), kind)); + } + pub fn add_overrides(&mut self, ids: Vec) -> CargoResult<()> { for id in ids.iter() { try!(self.load(id, Kind::Override)); @@ -183,8 +193,7 @@ impl<'cfg> PackageRegistry<'cfg> { } // Save off the source - self.sources.insert(source_id, source); - self.source_ids.insert(source_id.clone(), (source_id.clone(), kind)); + self.add_source(source_id, source, kind); Ok(()) }).chain_error(|| human(format!("Unable to update {}", source_id))) diff --git a/src/cargo/core/resolver/mod.rs b/src/cargo/core/resolver/mod.rs index bd9ba548ac9..bfade996269 100644 --- a/src/cargo/core/resolver/mod.rs +++ b/src/cargo/core/resolver/mod.rs @@ -55,7 +55,7 @@ use semver; use core::{PackageId, Registry, SourceId, Summary, Dependency}; use core::PackageIdSpec; -use util::{CargoResult, Graph, human, ChainError, CargoError}; +use util::{CargoResult, Graph, human, CargoError}; use util::profile; use util::graph::{Nodes, Edges}; @@ -118,55 +118,13 @@ impl Resolve { self.graph.edges(pkg) } - pub fn query(&self, spec: &str) -> CargoResult<&PackageId> { - let spec = try!(PackageIdSpec::parse(spec).chain_error(|| { - human(format!("invalid package id specification: `{}`", spec)) - })); - let mut ids = self.iter().filter(|p| spec.matches(*p)); - let ret = match ids.next() { - Some(id) => id, - None => return Err(human(format!("package id specification `{}` \ - matched no packages", spec))), - }; - return match ids.next() { - Some(other) => { - let mut msg = format!("There are multiple `{}` packages in \ - your project, and the specification \ - `{}` is ambiguous.\n\ - Please re-run this command \ - with `-p ` where `` is one \ - of the following:", - spec.name(), spec); - let mut vec = vec![ret, other]; - vec.extend(ids); - minimize(&mut msg, vec, &spec); - Err(human(msg)) - } - None => Ok(ret) - }; - - fn minimize(msg: &mut String, - ids: Vec<&PackageId>, - spec: &PackageIdSpec) { - let mut version_cnt = HashMap::new(); - for id in ids.iter() { - *version_cnt.entry(id.version()).or_insert(0) += 1; - } - for id in ids.iter() { - if version_cnt[id.version()] == 1 { - msg.push_str(&format!("\n {}:{}", spec.name(), - id.version())); - } else { - msg.push_str(&format!("\n {}", - PackageIdSpec::from_package_id(*id))); - } - } - } - } - pub fn features(&self, pkg: &PackageId) -> Option<&HashSet> { self.features.get(pkg) } + + pub fn query(&self, spec: &str) -> CargoResult<&PackageId> { + PackageIdSpec::query_str(spec, self.iter()) + } } impl fmt::Debug for Resolve { diff --git a/src/cargo/core/source.rs b/src/cargo/core/source.rs index d18010a36ef..d258b9f160f 100644 --- a/src/cargo/core/source.rs +++ b/src/cargo/core/source.rs @@ -129,17 +129,19 @@ impl SourceId { SourceId::new(Kind::Registry, url) .with_precise(Some("locked".to_string())) } - "path" => SourceId::for_path(Path::new(&url[5..])).unwrap(), + "path" => { + let url = url.to_url().unwrap(); + SourceId::new(Kind::Path, url) + } _ => panic!("Unsupported serialized SourceId") } } pub fn to_url(&self) -> String { match *self.inner { - SourceIdInner { kind: Kind::Path, .. } => { - panic!("Path sources are not included in the lockfile, \ - so this is unimplemented") - }, + SourceIdInner { kind: Kind::Path, ref url, .. } => { + format!("path+{}", url) + } SourceIdInner { kind: Kind::Git(ref reference), ref url, ref precise, .. } => { diff --git a/src/cargo/ops/cargo_compile.rs b/src/cargo/ops/cargo_compile.rs index b52cae00213..e9263a69d1b 100644 --- a/src/cargo/ops/cargo_compile.rs +++ b/src/cargo/ops/cargo_compile.rs @@ -92,11 +92,12 @@ pub fn compile<'a>(manifest_path: &Path, for key in package.manifest().warnings().iter() { try!(options.config.shell().warn(key)) } - compile_pkg(&package, options) + compile_pkg(&package, None, options) } #[allow(deprecated)] // connect => join in 1.3 pub fn compile_pkg<'a>(root_package: &Package, + source: Option>, options: &CompileOptions<'a>) -> CargoResult> { let CompileOptions { config, jobs, target, spec, features, @@ -122,6 +123,10 @@ pub fn compile_pkg<'a>(root_package: &Package, let (packages, resolve_with_overrides, sources) = { let mut registry = PackageRegistry::new(options.config); + if let Some(source) = source { + registry.add_preloaded(root_package.package_id().source_id(), source); + } + // First, resolve the root_package's *listed* dependencies, as well as // downloading and updating all remotes and such. let resolve = try!(ops::resolve_pkg(&mut registry, root_package)); diff --git a/src/cargo/ops/cargo_install.rs b/src/cargo/ops/cargo_install.rs index 2bd65cc0dd6..4d1f8370a91 100644 --- a/src/cargo/ops/cargo_install.rs +++ b/src/cargo/ops/cargo_install.rs @@ -1,17 +1,338 @@ -use ops; -use util::CargoResult; -use sources::PathSource; -use std::path::Path; +use std::collections::btree_map::Entry; +use std::collections::{BTreeMap, BTreeSet}; +use std::env; +use std::ffi::OsString; +use std::fs::{self, File}; +use std::io::prelude::*; +use std::path::{Path, PathBuf}; -pub fn install(manifest_path: &Path, +use toml; + +use core::{SourceId, Source, Package, Registry, Dependency, PackageIdSpec}; +use core::PackageId; +use ops::{self, CompileFilter}; +use sources::{GitSource, PathSource, RegistrySource}; +use util::{CargoResult, ChainError, Config, human, internal}; + +#[derive(RustcDecodable, RustcEncodable)] +enum CrateListing { + V1(CrateListingV1), +} + +#[derive(RustcDecodable, RustcEncodable)] +struct CrateListingV1 { + v1: BTreeMap>, +} + +struct Transaction { + bins: Vec, +} + +impl Drop for Transaction { + fn drop(&mut self) { + for bin in self.bins.iter() { + let _ = fs::remove_file(bin); + } + } +} + +pub fn install(root: Option<&str>, + krate: Option<&str>, + source_id: &SourceId, + vers: Option<&str>, opts: &ops::CompileOptions) -> CargoResult<()> { let config = opts.config; - let src = try!(PathSource::for_path(manifest_path.parent().unwrap(), - config)); - let _root = try!(src.root_package()); + let root = try!(resolve_root(root, config)); + let (pkg, source) = if source_id.is_git() { + try!(select_pkg(GitSource::new(source_id, config), source_id, + krate, vers, &mut |git| git.read_packages())) + } else if source_id.is_path() { + let path = source_id.url().to_file_path().ok() + .expect("path sources must have a valid path"); + try!(select_pkg(PathSource::new(&path, source_id, config), + source_id, krate, vers, + &mut |path| path.read_packages())) + } else { + try!(select_pkg(RegistrySource::new(source_id, config), + source_id, krate, vers, + &mut |_| Err(human("must specify a crate to install from \ + crates.io")))) + }; + + let mut list = try!(read_crate_list(&root)); + let dst = root.join("bin"); + try!(check_overwrites(&dst, &pkg, &opts.filter, &list)); + + let target_dir = config.cwd().join("target-install"); + config.set_target_dir(&target_dir); + let compile = try!(ops::compile_pkg(&pkg, Some(source), opts).chain_error(|| { + human(format!("failed to compile `{}`, intermediate artifacts can be \ + found at `{}`", pkg, target_dir.display())) + })); + + let mut t = Transaction { bins: Vec::new() }; + try!(fs::create_dir_all(&dst)); + for bin in compile.binaries.iter() { + let dst = dst.join(bin.file_name().unwrap()); + try!(config.shell().status("Installing", dst.display())); + try!(fs::copy(&bin, &dst).chain_error(|| { + human(format!("failed to copy `{}` to `{}`", bin.display(), + dst.display())) + })); + t.bins.push(dst); + } + try!(fs::remove_dir_all(&target_dir)); + + list.v1.entry(pkg.package_id().clone()).or_insert_with(|| { + BTreeSet::new() + }).extend(t.bins.iter().map(|t| { + t.file_name().unwrap().to_string_lossy().into_owned() + })); + try!(write_crate_list(&root, list)); + + t.bins.truncate(0); + + // Print a warning that if this directory isn't in PATH that they won't be + // able to run these commands. + let path = env::var_os("PATH").unwrap_or(OsString::new()); + for path in env::split_paths(&path) { + if path == dst { + return Ok(()) + } + } + + try!(config.shell().warn(&format!("be sure to add `{}` to your PATH to be \ + able to run the installed binaries", + dst.display()))); + Ok(()) +} + +fn select_pkg<'a, T>(mut source: T, + source_id: &SourceId, + name: Option<&str>, + vers: Option<&str>, + list_all: &mut FnMut(&mut T) -> CargoResult>) + -> CargoResult<(Package, Box)> + where T: Source + 'a +{ + try!(source.update()); + match name { + Some(name) => { + let dep = try!(Dependency::parse(name, vers, source_id)); + let deps = try!(source.query(&dep)); + match deps.iter().map(|p| p.package_id()).max() { + Some(pkgid) => { + try!(source.download(&[pkgid.clone()])); + Ok((try!(source.get(&[pkgid.clone()])).remove(0), + Box::new(source))) + } + None => { + let vers_info = vers.map(|v| format!(" with version `{}`", v)) + .unwrap_or(String::new()); + Err(human(format!("could not find `{}` in `{}`{}", name, + source_id, vers_info))) + } + } + } + None => { + let candidates = try!(list_all(&mut source)); + let binaries = candidates.iter().filter(|cand| { + cand.targets().iter().filter(|t| t.is_bin()).count() > 0 + }); + let examples = candidates.iter().filter(|cand| { + cand.targets().iter().filter(|t| t.is_example()).count() > 0 + }); + let pkg = match try!(one(binaries, |v| multi_err("binaries", v))) { + Some(p) => p, + None => { + match try!(one(examples, |v| multi_err("examples", v))) { + Some(p) => p, + None => return Err(human("no packages found with \ + binaries or examples")), + } + } + }; + return Ok((pkg.clone(), Box::new(source))); - println!("Compiling"); - try!(ops::compile(manifest_path, opts)); + #[allow(deprecated)] // connect => join in 1.3 + fn multi_err(kind: &str, mut pkgs: Vec<&Package>) -> String { + pkgs.sort_by(|a, b| a.name().cmp(b.name())); + format!("multiple packages with {} found: {}", kind, + pkgs.iter().map(|p| p.name()).collect::>() + .connect(", ")) + } + } + } +} + +fn one(mut i: I, f: F) -> CargoResult> + where I: Iterator, + F: FnOnce(Vec) -> String +{ + match (i.next(), i.next()) { + (Some(i1), Some(i2)) => { + let mut v = vec![i1, i2]; + v.extend(i); + Err(human(f(v))) + } + (Some(i), None) => Ok(Some(i)), + (None, _) => Ok(None) + } +} + +fn check_overwrites(dst: &Path, + pkg: &Package, + filter: &ops::CompileFilter, + prev: &CrateListingV1) -> CargoResult<()> { + let check = |name| { + let name = format!("{}{}", name, env::consts::EXE_SUFFIX); + if fs::metadata(dst.join(&name)).is_err() { + return Ok(()) + } + let mut msg = format!("binary `{}` already exists in destination", name); + if let Some((p, _)) = prev.v1.iter().find(|&(_, v)| v.contains(&name)) { + msg.push_str(&format!(" as part of `{}`", p)); + } + Err(human(msg)) + }; + match *filter { + CompileFilter::Everything => { + // If explicit --bin or --example flags were passed then those'll + // get checked during cargo_compile, we only care about the "build + // everything" case here + if pkg.targets().iter().filter(|t| t.is_bin()).next().is_none() { + return Err(human("specified package has no binaries")) + } + + for target in pkg.targets().iter().filter(|t| t.is_bin()) { + try!(check(target.name())); + } + } + CompileFilter::Only { bins, examples, .. } => { + for bin in bins.iter().chain(examples) { + try!(check(bin)); + } + } + } + Ok(()) +} + +fn read_crate_list(path: &Path) -> CargoResult { + let metadata = path.join(".crates.toml"); + let mut f = match File::open(&metadata) { + Ok(f) => f, + Err(..) => return Ok(CrateListingV1 { v1: BTreeMap::new() }), + }; + (|| -> CargoResult<_> { + let mut contents = String::new(); + try!(f.read_to_string(&mut contents)); + let listing = try!(toml::decode_str(&contents).chain_error(|| { + internal("invalid TOML found for metadata") + })); + match listing { + CrateListing::V1(v1) => Ok(v1), + } + }).chain_error(|| { + human(format!("failed to parse crate metadata at `{}`", + metadata.display())) + }) +} + +fn write_crate_list(path: &Path, listing: CrateListingV1) -> CargoResult<()> { + let metadata = path.join(".crates.toml"); + (|| -> CargoResult<_> { + let mut f = try!(File::create(&metadata)); + let data = toml::encode_str::(&CrateListing::V1(listing)); + try!(f.write_all(data.as_bytes())); + Ok(()) + }).chain_error(|| { + human(format!("failed to write crate metadata at `{}`", + metadata.display())) + }) +} + +pub fn install_list(dst: Option<&str>, config: &Config) -> CargoResult<()> { + let dst = try!(resolve_root(dst, config)); + let list = try!(read_crate_list(&dst)); + let mut shell = config.shell(); + let out = shell.out(); + for (k, v) in list.v1.iter() { + try!(writeln!(out, "{}:", k)); + for bin in v { + try!(writeln!(out, " {}", bin)); + } + } + Ok(()) +} + +pub fn uninstall(root: Option<&str>, + spec: &str, + bins: &[String], + config: &Config) -> CargoResult<()> { + let root = try!(resolve_root(root, config)); + let mut metadata = try!(read_crate_list(&root)); + let mut to_remove = Vec::new(); + { + let result = try!(PackageIdSpec::query_str(spec, metadata.v1.keys())) + .clone(); + let mut installed = match metadata.v1.entry(result.clone()) { + Entry::Occupied(e) => e, + Entry::Vacant(..) => panic!("entry not found: {}", result), + }; + let dst = root.join("bin"); + for bin in installed.get() { + let bin = dst.join(bin); + if fs::metadata(&bin).is_err() { + return Err(human(format!("corrupt metadata, `{}` does not \ + exist when it should", + bin.display()))) + } + } + + let bins = bins.iter().map(|s| { + if s.ends_with(env::consts::EXE_SUFFIX) { + s.to_string() + } else { + format!("{}{}", s, env::consts::EXE_SUFFIX) + } + }).collect::>(); + + for bin in bins.iter() { + if !installed.get().contains(bin) { + return Err(human(format!("binary `{}` not installed as part \ + of `{}`", bin, result))) + } + } + + if bins.len() == 0 { + to_remove.extend(installed.get().iter().map(|b| dst.join(b))); + installed.get_mut().clear(); + } else { + for bin in bins.iter() { + to_remove.push(dst.join(bin)); + installed.get_mut().remove(bin); + } + } + if installed.get().len() == 0 { + installed.remove(); + } + } + try!(write_crate_list(&root, metadata)); + for bin in to_remove { + try!(config.shell().status("Removing", bin.display())); + try!(fs::remove_file(bin)); + } Ok(()) } + +fn resolve_root(flag: Option<&str>, config: &Config) -> CargoResult { + let config_root = try!(config.get_string("install.root")); + Ok(flag.map(PathBuf::from).or_else(|| { + env::var_os("CARGO_INSTALL_ROOT").map(PathBuf::from) + }).or_else(|| { + config_root.clone().map(|(v, _)| PathBuf::from(v)) + }).unwrap_or_else(|| { + config.home().to_owned() + })) +} diff --git a/src/cargo/ops/cargo_package.rs b/src/cargo/ops/cargo_package.rs index a1b3cd5afd5..e08281814b1 100644 --- a/src/cargo/ops/cargo_package.rs +++ b/src/cargo/ops/cargo_package.rs @@ -208,7 +208,7 @@ fn run_verify(config: &Config, pkg: &Package, tar: &Path) let new_pkg = Package::new(new_manifest, &manifest_path); // Now that we've rewritten all our path dependencies, compile it! - try!(ops::compile_pkg(&new_pkg, &ops::CompileOptions { + try!(ops::compile_pkg(&new_pkg, None, &ops::CompileOptions { config: config, jobs: None, target: None, diff --git a/src/cargo/ops/cargo_pkgid.rs b/src/cargo/ops/cargo_pkgid.rs index ab5cfe6911d..48fad309ec0 100644 --- a/src/cargo/ops/cargo_pkgid.rs +++ b/src/cargo/ops/cargo_pkgid.rs @@ -17,7 +17,7 @@ pub fn pkgid(manifest_path: &Path, }; let pkgid = match spec { - Some(spec) => try!(resolve.query(spec)), + Some(spec) => try!(PackageIdSpec::query_str(spec, resolve.iter())), None => package.package_id(), }; Ok(PackageIdSpec::from_package_id(pkgid)) diff --git a/src/cargo/ops/mod.rs b/src/cargo/ops/mod.rs index 5aa156cd61f..92141afb439 100644 --- a/src/cargo/ops/mod.rs +++ b/src/cargo/ops/mod.rs @@ -7,7 +7,7 @@ pub use self::cargo_rustc::{Context, LayoutProxy}; pub use self::cargo_rustc::{BuildOutput, BuildConfig, TargetConfig}; pub use self::cargo_rustc::{CommandType, CommandPrototype, ExecEngine, ProcessEngine}; pub use self::cargo_run::run; -pub use self::cargo_install::install; +pub use self::cargo_install::{install, install_list, uninstall}; pub use self::cargo_new::{new, NewOptions, VersionControl}; pub use self::cargo_doc::{doc, DocOptions}; pub use self::cargo_generate_lockfile::{generate_lockfile}; diff --git a/src/cargo/ops/resolve.rs b/src/cargo/ops/resolve.rs index e7afbcbca07..f1dbb0f8154 100644 --- a/src/cargo/ops/resolve.rs +++ b/src/cargo/ops/resolve.rs @@ -17,7 +17,9 @@ pub fn resolve_pkg(registry: &mut PackageRegistry, package: &Package) let resolve = try!(resolve_with_previous(registry, package, Method::Everything, prev.as_ref(), None)); - try!(ops::write_pkg_lockfile(package, &resolve)); + if package.package_id().source_id().is_path() { + try!(ops::write_pkg_lockfile(package, &resolve)); + } Ok(resolve) } diff --git a/src/cargo/sources/git/source.rs b/src/cargo/sources/git/source.rs index 1863e8fd237..eaf66adccfb 100644 --- a/src/cargo/sources/git/source.rs +++ b/src/cargo/sources/git/source.rs @@ -67,6 +67,13 @@ impl<'cfg> GitSource<'cfg> { } pub fn url(&self) -> &Url { self.remote.url() } + + pub fn read_packages(&mut self) -> CargoResult> { + if self.path_source.is_none() { + try!(self.update()); + } + self.path_source.as_mut().unwrap().read_packages() + } } fn ident(url: &Url) -> String { diff --git a/src/cargo/sources/path.rs b/src/cargo/sources/path.rs index 1da040f1a1b..eedb88303bf 100644 --- a/src/cargo/sources/path.rs +++ b/src/cargo/sources/path.rs @@ -56,7 +56,7 @@ impl<'cfg> PathSource<'cfg> { } } - fn read_packages(&self) -> CargoResult> { + pub fn read_packages(&self) -> CargoResult> { if self.updated { Ok(self.packages.clone()) } else if self.id.is_path() && self.id.precise().is_some() { diff --git a/src/cargo/sources/registry.rs b/src/cargo/sources/registry.rs index 3d0c67dca7f..49d9cf20796 100644 --- a/src/cargo/sources/registry.rs +++ b/src/cargo/sources/registry.rs @@ -187,7 +187,7 @@ pub struct RegistrySource<'cfg> { src_path: PathBuf, config: &'cfg Config, handle: Option, - sources: Vec>, + sources: HashMap>, hashes: HashMap<(String, String), String>, // (name, vers) => cksum cache: HashMap>, updated: bool, @@ -239,7 +239,7 @@ impl<'cfg> RegistrySource<'cfg> { config: config, source_id: source_id.clone(), handle: None, - sources: Vec::new(), + sources: HashMap::new(), hashes: HashMap::new(), cache: HashMap::new(), updated: false, @@ -366,7 +366,7 @@ impl<'cfg> RegistrySource<'cfg> { } /// Parse the on-disk metadata for the package provided - fn summaries(&mut self, name: &str) -> CargoResult<&Vec<(Summary, bool)>> { + pub fn summaries(&mut self, name: &str) -> CargoResult<&Vec<(Summary, bool)>> { if self.cache.contains_key(name) { return Ok(self.cache.get(name).unwrap()); } @@ -537,6 +537,7 @@ impl<'cfg> Source for RegistrySource<'cfg> { let url = try!(config.dl.to_url().map_err(internal)); for package in packages.iter() { if self.source_id != *package.source_id() { continue } + if self.sources.contains_key(package) { continue } let mut url = url.clone(); url.path_mut().unwrap().push(package.name().to_string()); @@ -551,14 +552,14 @@ impl<'cfg> Source for RegistrySource<'cfg> { })); let mut src = PathSource::new(&path, &self.source_id, self.config); try!(src.update()); - self.sources.push(src); + self.sources.insert(package.clone(), src); } Ok(()) } fn get(&self, packages: &[PackageId]) -> CargoResult> { let mut ret = Vec::new(); - for src in self.sources.iter() { + for src in self.sources.values() { ret.extend(try!(src.get(packages)).into_iter()); } return Ok(ret); diff --git a/src/cargo/util/config.rs b/src/cargo/util/config.rs index 21aa840b7eb..de5302f6fdd 100644 --- a/src/cargo/util/config.rs +++ b/src/cargo/util/config.rs @@ -27,7 +27,7 @@ pub struct Config { cwd: PathBuf, rustc: PathBuf, rustdoc: PathBuf, - target_dir: Option, + target_dir: RefCell>, } impl Config { @@ -48,7 +48,7 @@ impl Config { values_loaded: Cell::new(false), rustc: PathBuf::from("rustc"), rustdoc: PathBuf::from("rustdoc"), - target_dir: None, + target_dir: RefCell::new(None), }; try!(cfg.scrape_tool_config()); @@ -101,11 +101,15 @@ impl Config { pub fn cwd(&self) -> &Path { &self.cwd } pub fn target_dir(&self, pkg: &Package) -> PathBuf { - self.target_dir.clone().unwrap_or_else(|| { + self.target_dir.borrow().clone().unwrap_or_else(|| { pkg.root().join("target") }) } + pub fn set_target_dir(&self, path: &Path) { + *self.target_dir.borrow_mut() = Some(path.to_owned()); + } + pub fn get(&self, key: &str) -> CargoResult> { let vals = try!(self.values()); let mut parts = key.split('.').enumerate(); @@ -237,9 +241,9 @@ impl Config { path.pop(); path.pop(); path.push(dir); - self.target_dir = Some(path); + *self.target_dir.borrow_mut() = Some(path); } else if let Some(dir) = env::var_os("CARGO_TARGET_DIR") { - self.target_dir = Some(self.cwd.join(dir)); + *self.target_dir.borrow_mut() = Some(self.cwd.join(dir)); } Ok(()) } diff --git a/tests/support/mod.rs b/tests/support/mod.rs index 90b1dfac81f..b345f245033 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -563,4 +563,4 @@ pub static DOWNLOADING: &'static str = " Downloading"; pub static UPLOADING: &'static str = " Uploading"; pub static VERIFYING: &'static str = " Verifying"; pub static ARCHIVING: &'static str = " Archiving"; -pub static INSTALLED: &'static str = " Installed"; +pub static INSTALLING: &'static str = " Installing"; diff --git a/tests/support/registry.rs b/tests/support/registry.rs index 861bc5a11dc..be48cb7845e 100644 --- a/tests/support/registry.rs +++ b/tests/support/registry.rs @@ -55,7 +55,11 @@ pub fn mock_archive(name: &str, version: &str, deps: &[(&str, &str, &str)]) { } let p = project(name) .file("Cargo.toml", &manifest) - .file("src/lib.rs", ""); + .file("src/lib.rs", "") + .file("src/main.rs", &format!("\ + extern crate {}; + fn main() {{}} + ", name)); p.build(); let dst = mock_archive_dst(name, version); @@ -66,6 +70,8 @@ pub fn mock_archive(name: &str, version: &str, deps: &[(&str, &str, &str)]) { &mut File::open(&p.root().join("Cargo.toml")).unwrap()).unwrap(); a.append_file(&format!("{}-{}/src/lib.rs", name, version), &mut File::open(&p.root().join("src/lib.rs")).unwrap()).unwrap(); + a.append_file(&format!("{}-{}/src/main.rs", name, version), + &mut File::open(&p.root().join("src/main.rs")).unwrap()).unwrap(); a.finish().unwrap(); } diff --git a/tests/test_cargo_install.rs b/tests/test_cargo_install.rs new file mode 100644 index 00000000000..c2835792ce7 --- /dev/null +++ b/tests/test_cargo_install.rs @@ -0,0 +1,500 @@ +use std::fmt; +use std::fs::{self, File}; +use std::io::prelude::*; +use std::path::{Path, PathBuf}; + +use cargo::util::{process, ProcessBuilder}; +use hamcrest::{assert_that, existing_file, is_not, Matcher, MatchResult}; + +use support::{project, execs, cargo_dir}; +use support::{UPDATING, DOWNLOADING, COMPILING, INSTALLING, REMOVING}; +use support::paths; +use support::registry as r; +use support::git; + +use self::InstalledExe as has_installed_exe; + +fn setup() { + r::init(); +} + +fn cargo_process(s: &str) -> ProcessBuilder { + let mut p = process(&cargo_dir().join("cargo")).unwrap(); + p.arg(s).cwd(&paths::root()) + .env("HOME", &paths::home()) + .env_remove("CARGO_HOME"); + return p; +} + +fn exe(name: &str) -> String { + if cfg!(windows) {format!("{}.exe", name)} else {name.to_string()} +} + +fn cargo_home() -> PathBuf { + paths::home().join(".cargo") +} + +struct InstalledExe(&'static str); + +impl> Matcher

for InstalledExe { + fn matches(&self, path: P) -> MatchResult { + let path = path.as_ref().join("bin").join(exe(self.0)); + existing_file().matches(&path) + } +} + +impl fmt::Display for InstalledExe { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "installed exe `{}`", self.0) + } +} + +test!(simple { + r::mock_pkg("foo", "0.0.1", &[]); + + assert_that(cargo_process("install").arg("foo"), + execs().with_status(0).with_stdout(&format!("\ +{updating} registry `[..]` +{downloading} foo v0.0.1 (registry file://[..]) +{compiling} foo v0.0.1 (registry file://[..]) +{installing} {home}[..]bin[..]foo[..] +", + updating = UPDATING, + downloading = DOWNLOADING, + compiling = COMPILING, + installing = INSTALLING, + home = cargo_home().display()))); + assert_that(cargo_home(), has_installed_exe("foo")); + + assert_that(cargo_process("uninstall").arg("foo"), + execs().with_status(0).with_stdout(&format!("\ +{removing} {home}[..]bin[..]foo[..] +", + removing = REMOVING, + home = cargo_home().display()))); + assert_that(cargo_home(), is_not(has_installed_exe("foo"))); +}); + +test!(pick_max_version { + r::mock_pkg("foo", "0.0.1", &[]); + r::mock_pkg("foo", "0.0.2", &[]); + + assert_that(cargo_process("install").arg("foo"), + execs().with_status(0).with_stdout(&format!("\ +{updating} registry `[..]` +{downloading} foo v0.0.2 (registry file://[..]) +{compiling} foo v0.0.2 (registry file://[..]) +{installing} {home}[..]bin[..]foo[..] +", + updating = UPDATING, + downloading = DOWNLOADING, + compiling = COMPILING, + installing = INSTALLING, + home = cargo_home().display()))); + assert_that(cargo_home(), has_installed_exe("foo")); +}); + +test!(missing { + r::mock_pkg("foo", "0.0.1", &[]); + assert_that(cargo_process("install").arg("bar"), + execs().with_status(101).with_stderr("\ +could not find `bar` in `registry file://[..]` +")); +}); + +test!(bad_version { + r::mock_pkg("foo", "0.0.1", &[]); + assert_that(cargo_process("install").arg("foo").arg("--vers=0.2.0"), + execs().with_status(101).with_stderr("\ +could not find `foo` in `registry file://[..]` with version `0.2.0` +")); +}); + +test!(no_crate { + assert_that(cargo_process("install"), + execs().with_status(101).with_stderr("\ +must specify a crate to install from crates.io +")); +}); + +test!(install_location_precedence { + r::mock_pkg("foo", "0.0.1", &[]); + + let root = paths::root(); + let t1 = root.join("t1"); + let t2 = root.join("t2"); + let t3 = root.join("t3"); + let t4 = cargo_home(); + + fs::create_dir(root.join(".cargo")).unwrap(); + File::create(root.join(".cargo/config")).unwrap().write_all(format!("\ + [install] + root = '{}' + ", t3.display()).as_bytes()).unwrap(); + + println!("install --root"); + + assert_that(cargo_process("install").arg("foo") + .arg("--root").arg(&t1) + .env("CARGO_INSTALL_ROOT", &t2), + execs().with_status(0)); + assert_that(&t1, has_installed_exe("foo")); + assert_that(&t2, is_not(has_installed_exe("foo"))); + + println!("install CARGO_INSTALL_ROOT"); + + assert_that(cargo_process("install").arg("foo") + .env("CARGO_INSTALL_ROOT", &t2), + execs().with_status(0)); + assert_that(&t2, has_installed_exe("foo")); + assert_that(&t3, is_not(has_installed_exe("foo"))); + + println!("install install.root"); + + assert_that(cargo_process("install").arg("foo"), + execs().with_status(0)); + assert_that(&t3, has_installed_exe("foo")); + assert_that(&t4, is_not(has_installed_exe("foo"))); + + fs::remove_file(root.join(".cargo/config")).unwrap(); + + println!("install cargo home"); + + assert_that(cargo_process("install").arg("foo"), + execs().with_status(0)); + assert_that(&t4, has_installed_exe("foo")); +}); + +test!(install_path { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("src/main.rs", "fn main() {}"); + p.build(); + + assert_that(cargo_process("install").arg("--path").arg(p.root()), + execs().with_status(0)); + assert_that(cargo_home(), has_installed_exe("foo")); +}); + +test!(multiple_crates_error { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("src/main.rs", "fn main() {}") + .file("a/Cargo.toml", r#" + [package] + name = "bar" + version = "0.1.0" + authors = [] + "#) + .file("a/src/main.rs", "fn main() {}"); + p.build(); + + assert_that(cargo_process("install").arg("--path").arg(p.root()), + execs().with_status(101).with_stderr("\ +multiple packages with binaries found: bar, foo +")); +}); + +test!(multiple_crates_select { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("src/main.rs", "fn main() {}") + .file("a/Cargo.toml", r#" + [package] + name = "bar" + version = "0.1.0" + authors = [] + "#) + .file("a/src/main.rs", "fn main() {}"); + p.build(); + + assert_that(cargo_process("install").arg("--path").arg(p.root()).arg("foo"), + execs().with_status(0)); + assert_that(cargo_home(), has_installed_exe("foo")); + assert_that(cargo_home(), is_not(has_installed_exe("bar"))); + + assert_that(cargo_process("install").arg("--path").arg(p.root()).arg("bar"), + execs().with_status(0)); + assert_that(cargo_home(), has_installed_exe("bar")); +}); + +test!(multiple_crates_auto_binaries { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + + [dependencies] + bar = { path = "a" } + "#) + .file("src/main.rs", "extern crate bar; fn main() {}") + .file("a/Cargo.toml", r#" + [package] + name = "bar" + version = "0.1.0" + authors = [] + "#) + .file("a/src/lib.rs", ""); + p.build(); + + assert_that(cargo_process("install").arg("--path").arg(p.root()), + execs().with_status(0)); + assert_that(cargo_home(), has_installed_exe("foo")); +}); + +test!(multiple_crates_auto_examples { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + + [dependencies] + bar = { path = "a" } + "#) + .file("src/lib.rs", "extern crate bar;") + .file("examples/foo.rs", " + extern crate bar; + extern crate foo; + fn main() {} + ") + .file("a/Cargo.toml", r#" + [package] + name = "bar" + version = "0.1.0" + authors = [] + "#) + .file("a/src/lib.rs", ""); + p.build(); + + assert_that(cargo_process("install").arg("--path").arg(p.root()) + .arg("--example=foo"), + execs().with_status(0)); + assert_that(cargo_home(), has_installed_exe("foo")); +}); + +test!(no_binaries_or_examples { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + + [dependencies] + bar = { path = "a" } + "#) + .file("src/lib.rs", "") + .file("a/Cargo.toml", r#" + [package] + name = "bar" + version = "0.1.0" + authors = [] + "#) + .file("a/src/lib.rs", ""); + p.build(); + + assert_that(cargo_process("install").arg("--path").arg(p.root()), + execs().with_status(101).with_stderr("\ +no packages found with binaries or examples +")); +}); + +test!(no_binaries { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("src/lib.rs", "") + .file("examples/foo.rs", "fn main() {}"); + p.build(); + + assert_that(cargo_process("install").arg("--path").arg(p.root()).arg("foo"), + execs().with_status(101).with_stderr("\ +specified package has no binaries +")); +}); + +test!(examples { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("src/lib.rs", "") + .file("examples/foo.rs", "extern crate foo; fn main() {}"); + p.build(); + + assert_that(cargo_process("install").arg("--path").arg(p.root()) + .arg("--example=foo"), + execs().with_status(0)); + assert_that(cargo_home(), has_installed_exe("foo")); +}); + +test!(install_twice { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("src/main.rs", "fn main() {}"); + p.build(); + + assert_that(cargo_process("install").arg("--path").arg(p.root()), + execs().with_status(0)); + assert_that(cargo_process("install").arg("--path").arg(p.root()), + execs().with_status(101).with_stderr("\ +binary `foo[..]` already exists in destination as part of `foo v0.1.0 ([..])` +")); +}); + +test!(compile_failure { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("src/main.rs", ""); + p.build(); + + assert_that(cargo_process("install").arg("--path").arg(p.root()), + execs().with_status(101).with_stderr("\ +error: main function not found +error: aborting due to previous error +failed to compile `foo v0.1.0 (file://[..])`, intermediate artifacts can be \ + found at `[..]target-install` + +Caused by: + Could not compile `foo`. + +To learn more, run the command again with --verbose. +")); +}); + +test!(git_repo { + let p = git::repo(&paths::root().join("foo")) + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("src/main.rs", "fn main() {}"); + p.build(); + + assert_that(cargo_process("install").arg("--git").arg(p.url().to_string()), + execs().with_status(0).with_stdout(&format!("\ +{updating} git repository `[..]` +{compiling} foo v0.1.0 ([..]) +{installing} {home}[..]bin[..]foo[..] +", + updating = UPDATING, + compiling = COMPILING, + installing = INSTALLING, + home = cargo_home().display()))); + assert_that(cargo_home(), has_installed_exe("foo")); + assert_that(cargo_home(), has_installed_exe("foo")); +}); + +test!(list { + r::mock_pkg("foo", "0.0.1", &[]); + r::mock_pkg("bar", "0.2.1", &[]); + r::mock_pkg("bar", "0.2.2", &[]); + + assert_that(cargo_process("install").arg("--list"), + execs().with_status(0).with_stdout("")); + + assert_that(cargo_process("install").arg("bar").arg("--vers").arg("=0.2.1"), + execs().with_status(0)); + assert_that(cargo_process("install").arg("foo"), + execs().with_status(0)); + assert_that(cargo_process("install").arg("--list"), + execs().with_status(0).with_stdout("\ +bar v0.2.1 (registry [..]): + bar[..] +foo v0.0.1 (registry [..]): + foo[..] +")); +}); + +test!(uninstall_pkg_does_not_exist { + assert_that(cargo_process("uninstall").arg("foo"), + execs().with_status(101).with_stderr("\ +package id specification `foo` matched no packages +")); +}); + +test!(uninstall_bin_does_not_exist { + r::mock_pkg("foo", "0.0.1", &[]); + + assert_that(cargo_process("install").arg("foo"), + execs().with_status(0)); + assert_that(cargo_process("uninstall").arg("foo").arg("--bin=bar"), + execs().with_status(101).with_stderr("\ +binary `bar[..]` not installed as part of `foo v0.0.1 ([..])` +")); +}); + +test!(uninstall_piecemeal { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("src/bin/foo.rs", "fn main() {}") + .file("src/bin/bar.rs", "fn main() {}"); + p.build(); + + assert_that(cargo_process("install").arg("--path").arg(p.root()), + execs().with_status(0)); + assert_that(cargo_home(), has_installed_exe("foo")); + assert_that(cargo_home(), has_installed_exe("bar")); + + assert_that(cargo_process("uninstall").arg("foo").arg("--bin=bar"), + execs().with_status(0).with_stdout(&format!("\ +{removing} [..]bar[..] +", removing = REMOVING))); + + assert_that(cargo_home(), has_installed_exe("foo")); + assert_that(cargo_home(), is_not(has_installed_exe("bar"))); + + assert_that(cargo_process("uninstall").arg("foo").arg("--bin=foo"), + execs().with_status(0).with_stdout(&format!("\ +{removing} [..]foo[..] +", removing = REMOVING))); + assert_that(cargo_home(), is_not(has_installed_exe("foo"))); + + assert_that(cargo_process("uninstall").arg("foo"), + execs().with_status(101).with_stderr("\ +package id specification `foo` matched no packages +")); +}); diff --git a/tests/tests.rs b/tests/tests.rs index ad6d9abb883..612d11e0344 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -48,6 +48,7 @@ mod test_cargo_features; mod test_cargo_fetch; mod test_cargo_freshness; mod test_cargo_generate_lockfile; +mod test_cargo_install; mod test_cargo_new; mod test_cargo_package; mod test_cargo_profiles;