diff --git a/src/bin/cargo.rs b/src/bin/cargo.rs index cb5e55494b8..ab2765da558 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..433ed75d3cc --- /dev/null +++ b/src/bin/metadata.rs @@ -0,0 +1,54 @@ +extern crate cargo; +extern crate docopt; +extern crate rustc_serialize; +extern crate toml; + +use cargo::ops::{output_metadata, OutputMetadataOptions, ExportInfo}; +use cargo::util::important_paths::find_root_manifest_for_wd; +use cargo::util::{CliResult, Config}; + +#[derive(RustcDecodable)] +struct Options { + flag_color: Option, + flag_features: Vec, + flag_format_version: u32, + flag_manifest_path: Option, + flag_no_default_features: bool, + flag_quiet: bool, + flag_verbose: bool, +} + +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 + --features FEATURES Space-separated list of features + --no-default-features Do not include the `default` feature + --manifest-path PATH Path to the manifest + --format-version VERSION Format version [default: 1] + Valid values: 1 + -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 options = OutputMetadataOptions { + features: options.flag_features, + manifest_path: &manifest, + no_default_features: options.flag_no_default_features, + version: options.flag_format_version, + }; + + let result = try!(output_metadata(options, config)); + Ok(Some(result)) +} diff --git a/src/cargo/ops/cargo_output_metadata.rs b/src/cargo/ops/cargo_output_metadata.rs new file mode 100644 index 00000000000..33be1857f6f --- /dev/null +++ b/src/cargo/ops/cargo_output_metadata.rs @@ -0,0 +1,68 @@ +use std::path::Path; + +use core::resolver::Resolve; +use core::{Source, Package}; +use ops; +use sources::PathSource; +use util::config::Config; +use util::CargoResult; + +const VERSION: u32 = 1; + +pub struct OutputMetadataOptions<'a> { + pub features: Vec, + pub manifest_path: &'a Path, + pub no_default_features: bool, + pub version: u32, +} + +/// Loads the manifest, resolves the dependencies of the project to the concrete +/// used versions - considering overrides - and writes all dependencies in a JSON +/// format to stdout. +pub fn output_metadata<'a>(opt: OutputMetadataOptions, + config: &'a Config) + -> CargoResult { + let deps = try!(resolve_dependencies(opt.manifest_path, + config, + opt.features, + opt.no_default_features)); + let (packages, resolve) = deps; + + assert_eq!(opt.version, VERSION); + Ok(ExportInfo { + packages: packages, + resolve: resolve, + version: VERSION, + }) +} + +#[derive(RustcEncodable)] +pub struct ExportInfo { + packages: Vec, + resolve: Resolve, + version: u32, +} + + +/// 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<(Vec, Resolve)> { + 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((packages, resolve_with_overrides)) +} diff --git a/src/cargo/ops/mod.rs b/src/cargo/ops/mod.rs index 806d3921a6e..b9eb78ac3da 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, OutputMetadataOptions, ExportInfo}; 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/support/mod.rs b/tests/support/mod.rs index f57a283a061..7bd68151872 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -10,6 +10,7 @@ use std::process::Output; use std::str; use std::usize; +use rustc_serialize::json::Json; use url::Url; use hamcrest as ham; use cargo::util::ProcessBuilder; @@ -271,6 +272,7 @@ pub struct Execs { expect_exit_code: Option, expect_stdout_contains: Vec, expect_stderr_contains: Vec, + expect_json: Option, } impl Execs { @@ -299,6 +301,11 @@ impl Execs { self } + pub fn with_json(mut self, expected: &str) -> Execs { + self.expect_json = Some(Json::from_str(expected).unwrap()); + self + } + fn match_output(&self, actual: &Output) -> ham::MatchResult { self.match_status(actual) .and(self.match_stdout(actual)) @@ -330,6 +337,10 @@ impl Execs { try!(self.match_std(Some(expect), &actual.stderr, "stderr", &actual.stdout, true)); } + + if let Some(ref expect_json) = self.expect_json { + try!(self.match_json(expect_json, &actual.stdout)); + } Ok(()) } @@ -379,6 +390,27 @@ impl Execs { } + fn match_json(&self, expected: &Json, stdout: &[u8]) -> ham::MatchResult { + let stdout = match str::from_utf8(stdout) { + Err(..) => return Err("stdout was not utf8 encoded".to_owned()), + Ok(stdout) => stdout, + }; + + let actual = match Json::from_str(stdout) { + Err(..) => return Err(format!("Invalid json {}", stdout)), + Ok(actual) => actual, + }; + + match find_mismatch(expected, &actual) { + Some((expected_part, actual_part)) => Err(format!( + "JSON mismatch\nExpected:\n{}\nWas:\n{}\nExpected part:\n{}\nActual part:\n{}\n", + expected.pretty(), actual.pretty(), + expected_part.pretty(), actual_part.pretty() + )), + None => Ok(()), + } + } + fn diff_lines<'a>(&self, actual: str::Lines<'a>, expected: str::Lines<'a>, partial: bool) -> Vec { let actual = actual.take(if partial { @@ -419,6 +451,49 @@ fn lines_match(expected: &str, mut actual: &str) -> bool { actual.is_empty() || expected.ends_with("[..]") } +// Compares JSON object for approximate equality. +// You can use `[..]` wildcard in strings (useful for OS dependent things such as paths). +// Arrays are sorted before comparison. +fn find_mismatch<'a>(expected: &'a Json, actual: &'a Json) -> Option<(&'a Json, &'a Json)> { + use rustc_serialize::json::Json::*; + match (expected, actual) { + (&I64(l), &I64(r)) if l == r => None, + (&F64(l), &F64(r)) if l == r => None, + (&U64(l), &U64(r)) if l == r => None, + (&Boolean(l), &Boolean(r)) if l == r => None, + (&String(ref l), &String(ref r)) if lines_match(l, r) => None, + (&Array(ref l), &Array(ref r)) => { + if l.len() != r.len() { + return Some((expected, actual)); + } + + fn sorted(xs: &Vec) -> Vec<&Json> { + let mut result = xs.iter().collect::>(); + // `unwrap` should be safe because JSON spec does not allow NaNs + result.sort_by(|x, y| x.partial_cmp(y).unwrap()); + result + } + + sorted(l).iter().zip(sorted(r)) + .filter_map(|(l, r)| find_mismatch(l, r)) + .nth(0) + } + (&Object(ref l), &Object(ref r)) => { + let same_keys = l.len() == r.len() && l.keys().all(|k| r.contains_key(k)); + if !same_keys { + return Some((expected, actual)); + } + + l.values().zip(r.values()) + .filter_map(|(l, r)| find_mismatch(l, r)) + .nth(0) + } + (&Null, &Null) => None, + _ => Some((expected, actual)), + } + +} + struct ZipAll { first: I1, second: I2, @@ -486,6 +561,7 @@ pub fn execs() -> Execs { expect_exit_code: None, expect_stdout_contains: Vec::new(), expect_stderr_contains: Vec::new(), + expect_json: None, } } diff --git a/tests/test_cargo_metadata.rs b/tests/test_cargo_metadata.rs new file mode 100644 index 00000000000..09afbb2fc1d --- /dev/null +++ b/tests/test_cargo_metadata.rs @@ -0,0 +1,189 @@ +use hamcrest::{assert_that, existing_file}; +use support::registry::Package; +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_json(r#" + { + "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" + } + ], + "resolve": { + "package": [], + "root": { + "name": "foo", + "version": "0.5.0", + "source": null, + "dependencies" : [] + }, + "metadata": null + }, + "version": 1 + }"#)); +}); + + +test!(cargo_metadata_with_deps { + let p = project("foo") + .file("Cargo.toml", r#" + [project] + name = "foo" + version = "0.5.0" + authors = [] + license = "MIT" + description = "foo" + + [[bin]] + name = "foo" + + [dependencies] + bar = "*" + "#); + Package::new("baz", "0.0.1").publish(); + Package::new("bar", "0.0.1").dep("baz", "0.0.1").publish(); + + assert_that(p.cargo_process("metadata").arg("-q"), execs().with_json(r#" + { + "packages": [ + { + "dependencies": [], + "features": {}, + "id": "baz 0.0.1 (registry+file:[..])", + "manifest_path": "[..]Cargo.toml", + "name": "baz", + "source": "registry+file:[..]", + "targets": [ + { + "kind": [ + "lib" + ], + "name": "baz", + "src_path": "[..]lib.rs" + } + ], + "version": "0.0.1" + }, + { + "dependencies": [ + { + "features": [], + "kind": null, + "name": "baz", + "optional": false, + "req": "^0.0.1", + "source": "registry+file:[..]", + "target": null, + "uses_default_features": true + } + ], + "features": {}, + "id": "bar 0.0.1 (registry+file:[..])", + "manifest_path": "[..]Cargo.toml", + "name": "bar", + "source": "registry+file:[..]", + "targets": [ + { + "kind": [ + "lib" + ], + "name": "bar", + "src_path": "[..]lib.rs" + } + ], + "version": "0.0.1" + }, + { + "dependencies": [ + { + "features": [], + "kind": null, + "name": "bar", + "optional": false, + "req": "*", + "source": "registry+file:[..]", + "target": null, + "uses_default_features": true + } + ], + "features": {}, + "id": "foo 0.5.0 (path+file:[..]foo)", + "manifest_path": "[..]Cargo.toml", + "name": "foo", + "source": null, + "targets": [ + { + "kind": [ + "bin" + ], + "name": "foo", + "src_path": "[..]foo.rs" + } + ], + "version": "0.5.0" + } + ], + "resolve": { + "metadata": null, + "package": [ + { + "dependencies": [ + "baz 0.0.1 (registry+file:[..])" + ], + "name": "bar", + "source": "registry+file:[..]", + "version": "0.0.1" + }, + { + "dependencies": [], + "name": "baz", + "source": "registry+file:[..]", + "version": "0.0.1" + } + ], + "root": { + "dependencies": [ + "bar 0.0.1 (registry+file:[..])" + ], + "name": "foo", + "source": null, + "version": "0.5.0" + } + }, + "version": 1 + }"#)); +}); + +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.")) +}); diff --git a/tests/tests.rs b/tests/tests.rs index d1a02827150..8eb5f5aa753 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;