diff --git a/src/bin/cargo.rs b/src/bin/cargo.rs index 62556279d59..cc095eb73e6 100644 --- a/src/bin/cargo.rs +++ b/src/bin/cargo.rs @@ -73,6 +73,7 @@ macro_rules! each_subcommand{ $mac!(install); $mac!(locate_project); $mac!(login); + $mac!(metadata); $mac!(new); $mac!(owner); $mac!(package); diff --git a/src/bin/metadata.rs b/src/bin/metadata.rs new file mode 100644 index 00000000000..41a4dbd83a8 --- /dev/null +++ b/src/bin/metadata.rs @@ -0,0 +1,65 @@ +extern crate cargo; +extern crate docopt; +extern crate rustc_serialize; +extern crate toml; + +use std::path::PathBuf; + +use cargo::ops::{output_metadata, OutputTo, OutputMetadataOptions}; +use cargo::util::important_paths::find_root_manifest_for_wd; +use cargo::util::{CliResult, CliError, Config}; + +#[derive(RustcDecodable)] +struct Options { + flag_features: Vec, + flag_manifest_path: Option, + flag_no_default_features: bool, + flag_output_format: String, + flag_output_path: Option, + flag_verbose: bool, + flag_quiet: bool, + flag_color: Option, +} + +pub const USAGE: &'static str = " +Output the resolved dependencies of a project, the concrete used versions +including overrides, in machine-readable format. + +Usage: + cargo metadata [options] + +Options: + -h, --help Print this message + -o, --output-path PATH Path the output is written to, otherwise stdout is used + -f, --output-format FMT Output format [default: toml] + Valid values: toml, json + --features FEATURES Space-separated list of features + --no-default-features Do not include the `default` feature + --manifest-path PATH Path to the manifest + -v, --verbose Use verbose output + -q, --quiet No output printed to stdout + --color WHEN Coloring: auto, always, never +"; + +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 manifest = try!(find_root_manifest_for_wd(options.flag_manifest_path, config.cwd())); + + let output_to = match options.flag_output_path { + Some(path) => OutputTo::File(PathBuf::from(path)), + None => OutputTo::StdOut + }; + + let options = OutputMetadataOptions { + features: options.flag_features, + manifest_path: &manifest, + no_default_features: options.flag_no_default_features, + output_format: options.flag_output_format, + output_to: output_to, + }; + + output_metadata(options, config) + .map(|_| None) + .map_err(|err| CliError::from_boxed(err, 101)) +} diff --git a/src/cargo/ops/cargo_output_metadata.rs b/src/cargo/ops/cargo_output_metadata.rs new file mode 100644 index 00000000000..38f084af294 --- /dev/null +++ b/src/cargo/ops/cargo_output_metadata.rs @@ -0,0 +1,126 @@ +use std::ascii::AsciiExt; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use core::resolver::Resolve; +use core::{Source, Package}; +use ops; +use rustc_serialize::json; +use sources::PathSource; +use toml; +use util::config::Config; +use util::{paths, CargoResult}; + + +/// Where the dependencies should be written to. +pub enum OutputTo { + File(PathBuf), + StdOut, +} + +pub struct OutputMetadataOptions<'a> { + pub features: Vec, + pub output_format: String, + pub output_to: OutputTo, + pub manifest_path: &'a Path, + pub no_default_features: bool, +} + +/// Loads the manifest, resolves the dependencies of the project to the concrete +/// used versions - considering overrides - and writes all dependencies in a TOML +/// format to stdout or the specified file. +/// +/// The TOML format is e.g.: +/// ```toml +/// root = "libA" +/// +/// [packages.libA] +/// dependencies = ["libB"] +/// path = "/home/user/.cargo/registry/src/github.com-1ecc6299db9ec823/libA-0.1" +/// version = "0.1" +/// +/// [packages.libB] +/// dependencies = [] +/// path = "/home/user/.cargo/registry/src/github.com-1ecc6299db9ec823/libB-0.4" +/// version = "0.4" +/// +/// [packages.libB.features] +/// featureA = ["featureB"] +/// featureB = [] +/// ``` +pub fn output_metadata(opt: OutputMetadataOptions, config: &Config) -> CargoResult<()> { + let deps = try!(resolve_dependencies(opt.manifest_path, + config, + opt.features, + opt.no_default_features)); + let (resolved_deps, packages) = deps; + + #[derive(RustcEncodable)] + struct RootPackageInfo<'a> { + name: &'a str, + version: String, + features: Option<&'a HashMap>>, + } + + #[derive(RustcEncodable)] + struct ExportInfo<'a> { + root: RootPackageInfo<'a>, + packages: Vec<&'a Package>, + } + + let mut output = ExportInfo { + root: RootPackageInfo { + name: resolved_deps.root().name(), + version: format!("{}", resolved_deps.root().version()), + features: None, + }, + packages: Vec::new(), + }; + + for package in packages.iter() { + output.packages.push(&package); + if package.package_id() == resolved_deps.root() { + let features = package.manifest().summary().features(); + if !features.is_empty() { + output.root.features = Some(features); + } + } + } + + let serialized_str = match &opt.output_format.to_ascii_uppercase()[..] { + "TOML" => toml::encode_str(&output), + "JSON" => try!(json::encode(&output)), + _ => bail!("unknown format: {}, supported formats are TOML, JSON.", + opt.output_format), + }; + + match opt.output_to { + OutputTo::StdOut => println!("{}", serialized_str), + OutputTo::File(ref path) => try!(paths::write(path, serialized_str.as_bytes())) + } + + Ok(()) +} + +/// Loads the manifest and resolves the dependencies of the project to the +/// concrete used versions. Afterwards available overrides of dependencies are applied. +fn resolve_dependencies(manifest: &Path, + config: &Config, + features: Vec, + no_default_features: bool) + -> CargoResult<(Resolve, Vec)> { + let mut source = try!(PathSource::for_path(manifest.parent().unwrap(), config)); + try!(source.update()); + + let package = try!(source.root_package()); + + let deps = try!(ops::resolve_dependencies(&package, + config, + Some(Box::new(source)), + features, + no_default_features)); + + let (packages, resolve_with_overrides, _) = deps; + + Ok((resolve_with_overrides, packages)) +} diff --git a/src/cargo/ops/mod.rs b/src/cargo/ops/mod.rs index 806d3921a6e..b5c1efec65a 100644 --- a/src/cargo/ops/mod.rs +++ b/src/cargo/ops/mod.rs @@ -23,6 +23,7 @@ pub use self::registry::{modify_owners, yank, OwnersOptions}; pub use self::cargo_fetch::{fetch, get_resolved_packages}; pub use self::cargo_pkgid::pkgid; pub use self::resolve::{resolve_pkg, resolve_with_previous}; +pub use self::cargo_output_metadata::{output_metadata, OutputTo, OutputMetadataOptions}; mod cargo_clean; mod cargo_compile; @@ -31,6 +32,7 @@ mod cargo_fetch; mod cargo_generate_lockfile; mod cargo_install; mod cargo_new; +mod cargo_output_metadata; mod cargo_package; mod cargo_pkgid; mod cargo_read_manifest; diff --git a/tests/test_cargo_metadata.rs b/tests/test_cargo_metadata.rs new file mode 100644 index 00000000000..f491737f842 --- /dev/null +++ b/tests/test_cargo_metadata.rs @@ -0,0 +1,107 @@ +use std::fs::File; +use std::io::prelude::*; + +use hamcrest::{assert_that, existing_file, is, equal_to}; +use support::{project, execs, basic_bin_manifest}; + + +fn setup() { +} + +test!(cargo_metadata_simple { + let p = project("foo") + .file("Cargo.toml", &basic_bin_manifest("foo")); + + assert_that(p.cargo_process("metadata"), execs().with_stdout(r#" +[[packages]] +dependencies = [] +id = "foo 0.5.0 [..]" +manifest_path = "[..]Cargo.toml" +name = "foo" +version = "0.5.0" + +[packages.features] + +[[packages.targets]] +kind = ["bin"] +name = "foo" +src_path = "src[..]foo.rs" + +[root] +name = "foo" +version = "0.5.0" + +"#)); +}); + + +test!(cargo_metadata_simple_json { + let p = project("foo") + .file("Cargo.toml", &basic_bin_manifest("foo")); + + assert_that(p.cargo_process("metadata").arg("-f").arg("json"), execs().with_stdout(r#" + { + "root": { + "name": "foo", + "version": "0.5.0", + "features": null + }, + "packages": [ + { + "name": "foo", + "version": "0.5.0", + "id": "foo[..]", + "source": null, + "dependencies": [], + "targets": [ + { + "kind": [ + "bin" + ], + "name": "foo", + "src_path": "src[..]foo.rs" + } + ], + "features": {}, + "manifest_path": "[..]Cargo.toml" + } + ] + }"#.split_whitespace().collect::())); +}); + +test!(cargo_metadata_with_invalid_manifest { + let p = project("foo") + .file("Cargo.toml", ""); + + assert_that(p.cargo_process("metadata"), execs().with_status(101) + .with_stderr("\ +failed to parse manifest at `[..]` + +Caused by: + no `package` or `project` section found.")) +}); + +test!(cargo_metadata_with_invalid_output_format { + let p = project("foo") + .file("Cargo.toml", &basic_bin_manifest("foo")); + + assert_that(p.cargo_process("metadata").arg("--output-format").arg("XML"), + execs().with_status(101) + .with_stderr("unknown format: XML, supported formats are TOML, JSON.")) +}); + +test!(cargo_metadata_simple_file { + let p = project("foo") + .file("Cargo.toml", &basic_bin_manifest("foo")); + + assert_that(p.cargo_process("metadata").arg("--output-path").arg("metadata.toml"), + execs().with_status(0)); + + let outputfile = p.root().join("metadata.toml"); + assert_that(&outputfile, existing_file()); + + let mut output = String::new(); + File::open(&outputfile).unwrap().read_to_string(&mut output).unwrap(); + + assert_that(output[..].contains(r#"name = "foo""#), is(equal_to(true))); +}); diff --git a/tests/tests.rs b/tests/tests.rs index a435f91b860..43a97fb636b 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -51,6 +51,7 @@ mod test_cargo_freshness; mod test_cargo_generate_lockfile; mod test_cargo_init; mod test_cargo_install; +mod test_cargo_metadata; mod test_cargo_new; mod test_cargo_package; mod test_cargo_profiles;