Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce cargo metadata subcommand #2196

Merged
merged 5 commits into from
Jan 25, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/bin/cargo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ macro_rules! each_subcommand{
$mac!(install);
$mac!(locate_project);
$mac!(login);
$mac!(metadata);
$mac!(new);
$mac!(owner);
$mac!(package);
Expand Down
54 changes: 54 additions & 0 deletions src/bin/metadata.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
flag_features: Vec<String>,
flag_format_version: u32,
flag_manifest_path: Option<String>,
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<Option<ExportInfo>> {
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))
}
68 changes: 68 additions & 0 deletions src/cargo/ops/cargo_output_metadata.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
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<ExportInfo> {
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<Package>,
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<String>,
no_default_features: bool)
-> CargoResult<(Vec<Package>, 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))
}
2 changes: 2 additions & 0 deletions src/cargo/ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
76 changes: 76 additions & 0 deletions tests/support/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -271,6 +272,7 @@ pub struct Execs {
expect_exit_code: Option<i32>,
expect_stdout_contains: Vec<String>,
expect_stderr_contains: Vec<String>,
expect_json: Option<Json>,
}

impl Execs {
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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<String> {
let actual = actual.take(if partial {
Expand Down Expand Up @@ -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<Json>) -> Vec<&Json> {
let mut result = xs.iter().collect::<Vec<_>>();
// `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<I1: Iterator, I2: Iterator> {
first: I1,
second: I2,
Expand Down Expand Up @@ -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,
}
}

Expand Down
Loading