diff --git a/src/bin/cargo.rs b/src/bin/cargo.rs index bfa763852e0..f21ffb1d5c5 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); @@ -81,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 new file mode 100644 index 00000000000..7ae2a130aff --- /dev/null +++ b/src/bin/install.rs @@ -0,0 +1,130 @@ +use std::path::Path; + +use cargo::ops; +use cargo::core::{SourceId, GitReference}; +use cargo::util::{CliResult, Config, ToUrl, human}; + +#[derive(RustcDecodable)] +struct Options { + flag_jobs: Option, + flag_features: Vec, + flag_no_default_features: bool, + flag_debug: bool, + flag_bin: Vec, + flag_example: 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 Rust binary + +Usage: + cargo install [options] [] + cargo install [options] --list + +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 +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 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 +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> { + 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, + jobs: options.flag_jobs, + target: None, + features: &options.flag_features, + no_default_features: options.flag_no_default_features, + spec: &[], + exec_engine: None, + mode: ops::CompileMode::Build, + release: !options.flag_debug, + filter: ops::CompileFilter::new(false, &options.flag_bin, &[], + &options.flag_example, &[]), + target_rustc_args: None, + }; + + 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[..]); + + 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 new file mode 100644 index 00000000000..4d1f8370a91 --- /dev/null +++ b/src/cargo/ops/cargo_install.rs @@ -0,0 +1,338 @@ +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}; + +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 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))); + + #[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 f408b093aa9..92141afb439 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, 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}; @@ -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/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 38e4348705d..b345f245033 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 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;