From f5d786e05600720ba4671caf5b598624103c8f72 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 25 Jan 2016 16:54:10 -0800 Subject: [PATCH] Implement cfg-based target-specific dependencies This commit is an implementation of [RFC 1361][rfc] which is an extension of Cargo's `target` section in `Cargo.toml` to allow the use of `#[cfg]`-like expressions for target-specific dependencies. Now that the compiler has been extended with `--print cfg` each invocation of Cargo will scrape this output and learn about the relevant `#[cfg]` directives in play for the target being compiled. Cargo will then use these directives to decide whether a dependency should be activated or not. This should allow definition of dependencies along the lines of: [target.'cfg(unix)'.dependencies] [target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(windows)'.dependencies] Which is much more ergonomic and robust than listing all the triples out! --- src/cargo/core/dependency.rs | 96 ++++++-- src/cargo/ops/cargo_rustc/context.rs | 100 ++++---- src/cargo/ops/registry.rs | 2 +- src/cargo/sources/registry.rs | 7 +- src/cargo/util/cfg.rs | 261 +++++++++++++++++++++ src/cargo/util/mod.rs | 4 +- src/cargo/util/toml.rs | 68 +++--- src/doc/manifest.md | 27 ++- tests/support/registry.rs | 40 +++- tests/test_cargo_cfg.rs | 333 +++++++++++++++++++++++++++ tests/tests.rs | 1 + 11 files changed, 823 insertions(+), 116 deletions(-) create mode 100644 src/cargo/util/cfg.rs create mode 100644 tests/test_cargo_cfg.rs diff --git a/src/cargo/core/dependency.rs b/src/cargo/core/dependency.rs index ce538d964a3..a030de626f9 100644 --- a/src/cargo/core/dependency.rs +++ b/src/cargo/core/dependency.rs @@ -1,12 +1,22 @@ +use std::fmt; +use std::rc::Rc; +use std::str::FromStr; + use semver::VersionReq; use rustc_serialize::{Encoder, Encodable}; use core::{SourceId, Summary, PackageId}; -use std::rc::Rc; -use util::CargoResult; +use util::{CargoError, CargoResult, Cfg, CfgExpr, ChainError, human}; + +/// Information about a dependency requested by a Cargo manifest. +/// Cheap to copy. +#[derive(PartialEq, Clone ,Debug)] +pub struct Dependency { + inner: Rc, +} /// The data underlying a Dependency. -#[derive(PartialEq,Clone,Debug)] +#[derive(PartialEq, Clone, Debug)] pub struct DependencyInner { name: String, source_id: SourceId, @@ -21,17 +31,15 @@ pub struct DependencyInner { // This dependency should be used only for this platform. // `None` means *all platforms*. - only_for_platform: Option, + platform: Option, } -/// Information about a dependency requested by a Cargo manifest. -/// Cheap to copy. -#[derive(PartialEq,Clone,Debug)] -pub struct Dependency { - inner: Rc, +#[derive(Clone, Debug, PartialEq)] +pub enum Platform { + Name(String), + Cfg(CfgExpr), } - #[derive(RustcEncodable)] struct SerializedDependency<'a> { name: &'a str, @@ -42,7 +50,7 @@ struct SerializedDependency<'a> { optional: bool, uses_default_features: bool, features: &'a [String], - target: &'a Option<&'a str>, + target: Option<&'a Platform>, } impl Encodable for Dependency { @@ -55,7 +63,7 @@ impl Encodable for Dependency { optional: self.is_optional(), uses_default_features: self.uses_default_features(), features: self.features(), - target: &self.only_for_platform(), + target: self.platform(), }.encode(s) } } @@ -106,7 +114,7 @@ impl DependencyInner { features: Vec::new(), default_features: true, specified_req: None, - only_for_platform: None, + platform: None, } } @@ -118,10 +126,10 @@ impl DependencyInner { self.specified_req.as_ref().map(|s| &s[..]) } - /// If none, this dependencies must be built for all platforms. - /// If some, it must only be built for the specified platform. - pub fn only_for_platform(&self) -> Option<&str> { - self.only_for_platform.as_ref().map(|s| &s[..]) + /// If none, this dependency must be built for all platforms. + /// If some, it must only be built for matching platforms. + pub fn platform(&self) -> Option<&Platform> { + self.platform.as_ref() } pub fn set_kind(mut self, kind: Kind) -> DependencyInner { @@ -159,9 +167,9 @@ impl DependencyInner { self } - pub fn set_only_for_platform(mut self, platform: Option) - -> DependencyInner { - self.only_for_platform = platform; + pub fn set_platform(mut self, platform: Option) + -> DependencyInner { + self.platform = platform; self } @@ -230,8 +238,8 @@ impl Dependency { /// If none, this dependencies must be built for all platforms. /// If some, it must only be built for the specified platform. - pub fn only_for_platform(&self) -> Option<&str> { - self.inner.only_for_platform() + pub fn platform(&self) -> Option<&Platform> { + self.inner.platform() } /// Lock this dependency to depending on the specified package id @@ -258,3 +266,47 @@ impl Dependency { self.inner.matches_id(id) } } + +impl Platform { + pub fn matches(&self, name: &str, cfg: Option<&[Cfg]>) -> bool { + match *self { + Platform::Name(ref p) => p == name, + Platform::Cfg(ref p) => { + match cfg { + Some(cfg) => p.matches(cfg), + None => false, + } + } + } + } +} + +impl Encodable for Platform { + fn encode(&self, s: &mut S) -> Result<(), S::Error> { + self.to_string().encode(s) + } +} + +impl FromStr for Platform { + type Err = Box; + + fn from_str(s: &str) -> CargoResult { + if s.starts_with("cfg(") && s.ends_with(")") { + let s = &s[4..s.len()-1]; + s.parse().map(Platform::Cfg).chain_error(|| { + human(format!("failed to parse `{}` as a cfg expression", s)) + }) + } else { + Ok(Platform::Name(s.to_string())) + } + } +} + +impl fmt::Display for Platform { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Platform::Name(ref n) => n.fmt(f), + Platform::Cfg(ref e) => write!(f, "cfg({})", e), + } + } +} diff --git a/src/cargo/ops/cargo_rustc/context.rs b/src/cargo/ops/cargo_rustc/context.rs index c85de27c4c8..22025b1785b 100644 --- a/src/cargo/ops/cargo_rustc/context.rs +++ b/src/cargo/ops/cargo_rustc/context.rs @@ -1,6 +1,6 @@ use std::collections::{HashSet, HashMap}; use std::path::{Path, PathBuf}; -use std::str; +use std::str::{self, FromStr}; use std::sync::Arc; use regex::Regex; @@ -8,7 +8,7 @@ use regex::Regex; use core::{SourceMap, Package, PackageId, PackageSet, Resolve, Target, Profile}; use core::{TargetKind, LibKind, Profiles, Metadata, Dependency}; use core::dependency::Kind as DepKind; -use util::{self, CargoResult, ChainError, internal, Config, profile}; +use util::{self, CargoResult, ChainError, internal, Config, profile, Cfg, human}; use super::TargetConfig; use super::custom_build::{BuildState, BuildScripts}; @@ -41,16 +41,20 @@ pub struct Context<'a, 'cfg: 'a> { host: Layout, target: Option, target_triple: String, - host_dylib: Option<(String, String)>, - host_staticlib: Option<(String, String)>, - host_exe: String, + target_info: TargetInfo, + host_info: TargetInfo, package_set: &'a PackageSet, - target_dylib: Option<(String, String)>, - target_staticlib: Option<(String, String)>, - target_exe: String, profiles: &'a Profiles, } +#[derive(Clone)] +struct TargetInfo { + dylib: Option<(String, String)>, + staticlib: Option<(String, String)>, + exe: String, + cfg: Option>, +} + impl<'a, 'cfg> Context<'a, 'cfg> { pub fn new(resolve: &'a Resolve, sources: &'a SourceMap<'cfg>, @@ -62,12 +66,11 @@ impl<'a, 'cfg> Context<'a, 'cfg> { profiles: &'a Profiles) -> CargoResult> { let target = build_config.requested_target.clone(); let target = target.as_ref().map(|s| &s[..]); - let (target_dylib, target_staticlib, target_exe) = try!(Context::filename_parts(target, - config)); - let (host_dylib, host_staticlib, host_exe) = if build_config.requested_target.is_none() { - (target_dylib.clone(), target_staticlib.clone(), target_exe.clone()) + let target_info = try!(Context::target_info(target, config)); + let host_info = if build_config.requested_target.is_none() { + target_info.clone() } else { - try!(Context::filename_parts(None, config)) + try!(Context::target_info(None, config)) }; let target_triple = target.unwrap_or_else(|| { &config.rustc_info().host[..] @@ -83,12 +86,8 @@ impl<'a, 'cfg> Context<'a, 'cfg> { sources: sources, package_set: deps, config: config, - target_dylib: target_dylib, - target_staticlib: target_staticlib, - target_exe: target_exe, - host_dylib: host_dylib, - host_staticlib: host_staticlib, - host_exe: host_exe, + target_info: target_info, + host_info: host_info, compilation: Compilation::new(config), build_state: Arc::new(BuildState::new(&build_config, deps)), build_config: build_config, @@ -103,8 +102,8 @@ impl<'a, 'cfg> Context<'a, 'cfg> { /// Run `rustc` to discover the dylib prefix/suffix for the target /// specified as well as the exe suffix - fn filename_parts(target: Option<&str>, cfg: &Config) - -> CargoResult<(Option<(String, String)>, Option<(String, String)>, String)> { + fn target_info(target: Option<&str>, cfg: &Config) + -> CargoResult { let mut process = util::process(cfg.rustc()); process.arg("-") .arg("--crate-name").arg("_") @@ -116,7 +115,18 @@ impl<'a, 'cfg> Context<'a, 'cfg> { if let Some(s) = target { process.arg("--target").arg(s); }; - let output = try!(process.exec_with_output()); + + let mut with_cfg = process.clone(); + with_cfg.arg("--print=cfg"); + + let mut has_cfg = true; + let output = try!(with_cfg.exec_with_output().or_else(|_| { + has_cfg = false; + process.exec_with_output() + }).chain_error(|| { + human(format!("failed to run `rustc` to learn about \ + target-specific information")) + })); let error = str::from_utf8(&output.stderr).unwrap(); let output = str::from_utf8(&output.stdout).unwrap(); @@ -143,13 +153,25 @@ impl<'a, 'cfg> Context<'a, 'cfg> { Some((staticlib_parts[0].to_string(), staticlib_parts[1].to_string())) }; - let exe_suffix = if nobin.is_match(error) { + let exe = if nobin.is_match(error) { String::new() } else { lines.next().unwrap().trim() .split('_').skip(1).next().unwrap().to_string() }; - Ok((dylib, staticlib, exe_suffix)) + + let cfg = if has_cfg { + Some(try!(lines.map(Cfg::from_str).collect())) + } else { + None + }; + + Ok(TargetInfo { + dylib: dylib, + staticlib: staticlib, + exe: exe, + cfg: cfg, + }) } /// Prepare this context, ensuring that all filesystem directories are in @@ -207,9 +229,9 @@ impl<'a, 'cfg> Context<'a, 'cfg> { /// otherwise it corresponds to the target platform. fn dylib(&self, kind: Kind) -> CargoResult<(&str, &str)> { let (triple, pair) = if kind == Kind::Host { - (&self.config.rustc_info().host, &self.host_dylib) + (&self.config.rustc_info().host, &self.host_info.dylib) } else { - (&self.target_triple, &self.target_dylib) + (&self.target_triple, &self.target_info.dylib) }; match *pair { None => bail!("dylib outputs are not supported for {}", triple), @@ -223,9 +245,9 @@ impl<'a, 'cfg> Context<'a, 'cfg> { /// otherwise it corresponds to the target platform. pub fn staticlib(&self, kind: Kind) -> CargoResult<(&str, &str)> { let (triple, pair) = if kind == Kind::Host { - (&self.config.rustc_info().host, &self.host_staticlib) + (&self.config.rustc_info().host, &self.host_info.staticlib) } else { - (&self.target_triple, &self.target_staticlib) + (&self.target_triple, &self.target_info.staticlib) }; match *pair { None => bail!("staticlib outputs are not supported for {}", triple), @@ -284,9 +306,9 @@ impl<'a, 'cfg> Context<'a, 'cfg> { pub fn target_filenames(&self, unit: &Unit) -> CargoResult> { let stem = self.file_stem(unit); let suffix = if unit.target.for_host() { - &self.host_exe + &self.host_info.exe } else { - &self.target_exe + &self.target_info.exe }; let mut ret = Vec::new(); @@ -532,15 +554,15 @@ impl<'a, 'cfg> Context<'a, 'cfg> { fn dep_platform_activated(&self, dep: &Dependency, kind: Kind) -> bool { // If this dependency is only available for certain platforms, // make sure we're only enabling it for that platform. - match (dep.only_for_platform(), kind) { - (Some(ref platform), Kind::Host) => { - *platform == self.config.rustc_info().host - }, - (Some(ref platform), Kind::Target) => { - *platform == self.target_triple - }, - (None, _) => true - } + let platform = match dep.platform() { + Some(p) => p, + None => return true, + }; + let (name, info) = match kind { + Kind::Host => (&self.config.rustc_info().host, &self.host_info), + Kind::Target => (&self.target_triple, &self.target_info), + }; + platform.matches(name, info.cfg.as_ref().map(|cfg| &cfg[..])) } /// Gets a package for the given package id. diff --git a/src/cargo/ops/registry.rs b/src/cargo/ops/registry.rs index 62b287959d2..9bbdf87127a 100644 --- a/src/cargo/ops/registry.rs +++ b/src/cargo/ops/registry.rs @@ -81,7 +81,7 @@ fn transmit(pkg: &Package, tarball: &Path, registry: &mut Registry) name: dep.name().to_string(), features: dep.features().to_vec(), version_req: dep.version_req().to_string(), - target: dep.only_for_platform().map(|s| s.to_string()), + target: dep.platform().map(|s| s.to_string()), kind: match dep.kind() { Kind::Normal => "normal", Kind::Build => "build", diff --git a/src/cargo/sources/registry.rs b/src/cargo/sources/registry.rs index 94bfa7d4ee2..7e0ad0558e3 100644 --- a/src/cargo/sources/registry.rs +++ b/src/cargo/sources/registry.rs @@ -435,6 +435,11 @@ impl<'cfg> RegistrySource<'cfg> { _ => Kind::Normal, }; + let platform = match target { + Some(target) => Some(try!(target.parse())), + None => None, + }; + // Unfortunately older versions of cargo and/or the registry ended up // publishing lots of entries where the features array contained the // empty feature, "", inside. This confuses the resolution process much @@ -445,7 +450,7 @@ impl<'cfg> RegistrySource<'cfg> { Ok(dep.set_optional(optional) .set_default_features(default_features) .set_features(features) - .set_only_for_platform(target) + .set_platform(platform) .set_kind(kind) .into_dependency()) } diff --git a/src/cargo/util/cfg.rs b/src/cargo/util/cfg.rs new file mode 100644 index 00000000000..95b20e508f2 --- /dev/null +++ b/src/cargo/util/cfg.rs @@ -0,0 +1,261 @@ +use std::str::{self, FromStr}; +use std::iter; +use std::fmt; + +use util::{CargoError, CargoResult, human}; + +#[derive(Clone, PartialEq, Debug)] +pub enum Cfg { + Name(String), + KeyPair(String, String), +} + +#[derive(Clone, PartialEq, Debug)] +pub enum CfgExpr { + Not(Box), + All(Vec), + Any(Vec), + Value(Cfg), +} + +#[derive(PartialEq)] +enum Token<'a> { + LeftParen, + RightParen, + Ident(&'a str), + Comma, + Equals, + String(&'a str), +} + +struct Tokenizer<'a> { + s: iter::Peekable>, + orig: &'a str, +} + +struct Parser<'a> { + t: iter::Peekable>, +} + +impl FromStr for Cfg { + type Err = Box; + + fn from_str(s: &str) -> CargoResult { + let mut p = Parser::new(s); + let e = try!(p.cfg()); + if p.t.next().is_some() { + bail!("malformed cfg value or key/value pair") + } + Ok(e) + } +} + +impl fmt::Display for Cfg { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Cfg::Name(ref s) => s.fmt(f), + Cfg::KeyPair(ref k, ref v) => write!(f, "{} = \"{}\"", k, v), + } + } +} + +impl CfgExpr { + pub fn matches(&self, cfg: &[Cfg]) -> bool { + match *self { + CfgExpr::Not(ref e) => !e.matches(cfg), + CfgExpr::All(ref e) => e.iter().all(|e| e.matches(cfg)), + CfgExpr::Any(ref e) => e.iter().any(|e| e.matches(cfg)), + CfgExpr::Value(ref e) => cfg.contains(e), + } + } +} + +impl FromStr for CfgExpr { + type Err = Box; + + fn from_str(s: &str) -> CargoResult { + let mut p = Parser::new(s); + let e = try!(p.expr()); + if p.t.next().is_some() { + bail!("can only have one cfg-expression, consider using all() or \ + any() explicitly") + } + Ok(e) + } +} + +impl fmt::Display for CfgExpr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + CfgExpr::Not(ref e) => write!(f, "not({})", e), + CfgExpr::All(ref e) => write!(f, "all({})", CommaSep(e)), + CfgExpr::Any(ref e) => write!(f, "any({})", CommaSep(e)), + CfgExpr::Value(ref e) => write!(f, "{}", e), + } + } +} + +struct CommaSep<'a, T: 'a>(&'a [T]); + +impl<'a, T: fmt::Display> fmt::Display for CommaSep<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for (i, v) in self.0.iter().enumerate() { + if i > 0 { + try!(write!(f, ", ")); + } + try!(write!(f, "{}", v)); + } + Ok(()) + } +} + +impl<'a> Parser<'a> { + fn new(s: &'a str) -> Parser<'a> { + Parser { + t: Tokenizer { + s: s.char_indices().peekable(), + orig: s, + }.peekable(), + } + } + + fn expr(&mut self) -> CargoResult { + match self.t.peek() { + Some(&Ok(Token::Ident(op @ "all"))) | + Some(&Ok(Token::Ident(op @ "any"))) => { + self.t.next(); + let mut e = Vec::new(); + try!(self.eat(Token::LeftParen)); + while !self.try(Token::RightParen) { + e.push(try!(self.expr())); + if !self.try(Token::Comma) { + try!(self.eat(Token::RightParen)); + break + } + } + if op == "all" { + Ok(CfgExpr::All(e)) + } else { + Ok(CfgExpr::Any(e)) + } + } + Some(&Ok(Token::Ident("not"))) => { + self.t.next(); + try!(self.eat(Token::LeftParen)); + let e = try!(self.expr()); + try!(self.eat(Token::RightParen)); + Ok(CfgExpr::Not(Box::new(e))) + } + Some(&Ok(..)) => self.cfg().map(CfgExpr::Value), + Some(&Err(..)) => { + Err(self.t.next().unwrap().err().unwrap()) + } + None => bail!("expected start of a cfg expression, \ + found nothing"), + } + } + + fn cfg(&mut self) -> CargoResult { + match self.t.next() { + Some(Ok(Token::Ident(name))) => { + let e = if self.try(Token::Equals) { + let val = match self.t.next() { + Some(Ok(Token::String(s))) => s, + Some(Ok(t)) => bail!("expected a string, found {}", + t.classify()), + Some(Err(e)) => return Err(e), + None => bail!("expected a string, found nothing"), + }; + Cfg::KeyPair(name.to_string(), val.to_string()) + } else { + Cfg::Name(name.to_string()) + }; + Ok(e) + } + Some(Ok(t)) => bail!("expected identifier, found {}", t.classify()), + Some(Err(e)) => Err(e), + None => bail!("expected identifier, found nothing"), + } + } + + fn try(&mut self, token: Token<'a>) -> bool { + match self.t.peek() { + Some(&Ok(ref t)) if token == *t => {} + _ => return false, + } + self.t.next(); + true + } + + fn eat(&mut self, token: Token<'a>) -> CargoResult<()> { + match self.t.next() { + Some(Ok(ref t)) if token == *t => Ok(()), + Some(Ok(t)) => bail!("expected {}, found {}", token.classify(), + t.classify()), + Some(Err(e)) => Err(e), + None => bail!("expected {}, but cfg expr ended", token.classify()), + } + } +} + +impl<'a> Iterator for Tokenizer<'a> { + type Item = CargoResult>; + + fn next(&mut self) -> Option>> { + loop { + match self.s.next() { + Some((_, ' ')) => {} + Some((_, '(')) => return Some(Ok(Token::LeftParen)), + Some((_, ')')) => return Some(Ok(Token::RightParen)), + Some((_, ',')) => return Some(Ok(Token::Comma)), + Some((_, '=')) => return Some(Ok(Token::Equals)), + Some((start, '"')) => { + while let Some((end, ch)) = self.s.next() { + if ch == '"' { + return Some(Ok(Token::String(&self.orig[start+1..end]))) + } + } + return Some(Err(human(format!("unterminated string in cfg")))) + } + Some((start, ch)) if is_ident_start(ch) => { + while let Some(&(end, ch)) = self.s.peek() { + if !is_ident_rest(ch) { + return Some(Ok(Token::Ident(&self.orig[start..end]))) + } else { + self.s.next(); + } + } + return Some(Ok(Token::Ident(&self.orig[start..]))) + } + Some((_, ch)) => { + return Some(Err(human(format!("unexpected character in \ + cfg `{}`, expected parens, \ + a comma, an identifier, or \ + a string", ch)))) + } + None => return None + } + } + } +} + +fn is_ident_start(ch: char) -> bool { + ch == '_' || ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') +} + +fn is_ident_rest(ch: char) -> bool { + is_ident_start(ch) || ('0' <= ch && ch <= '9') +} + +impl<'a> Token<'a> { + fn classify(&self) -> &str { + match *self { + Token::LeftParen => "`(`", + Token::RightParen => "`)`", + Token::Ident(..) => "an identifier", + Token::Comma => "`,`", + Token::Equals => "`=`", + Token::String(..) => "a string", + } + } +} diff --git a/src/cargo/util/mod.rs b/src/cargo/util/mod.rs index c83356767ae..e2f2cb971c2 100644 --- a/src/cargo/util/mod.rs +++ b/src/cargo/util/mod.rs @@ -1,3 +1,4 @@ +pub use self::cfg::{Cfg, CfgExpr}; pub use self::config::Config; pub use self::dependency_queue::Dependency; pub use self::dependency_queue::{DependencyQueue, Fresh, Dirty, Freshness}; @@ -30,8 +31,9 @@ pub mod to_url; pub mod toml; pub mod lev_distance; pub mod job; +mod cfg; mod dependency_queue; +mod rustc; mod sha256; mod shell_escape; mod vcs; -mod rustc; diff --git a/src/cargo/util/toml.rs b/src/cargo/util/toml.rs index 47b67aa0c64..7ff7251b2e6 100644 --- a/src/cargo/util/toml.rs +++ b/src/cargo/util/toml.rs @@ -12,7 +12,7 @@ use rustc_serialize::{Decodable, Decoder}; use core::{SourceId, Profiles}; use core::{Summary, Manifest, Target, Dependency, DependencyInner, PackageId, GitReference}; -use core::dependency::Kind; +use core::dependency::{Kind, Platform}; use core::manifest::{LibKind, Profile, ManifestMetadata}; use core::package_id::Metadata; use util::{self, CargoResult, human, ToUrl, ToSemver, ChainError, Config}; @@ -293,6 +293,8 @@ struct Context<'a, 'b> { source_id: &'a SourceId, nested_paths: &'a mut Vec, config: &'b Config, + warnings: &'a mut Vec, + platform: Option, } // These functions produce the equivalent of specific manifest entries. One @@ -512,35 +514,30 @@ impl TomlManifest { source_id: source_id, nested_paths: &mut nested_paths, config: config, + warnings: &mut warnings, + platform: None, }; // Collect the deps try!(process_dependencies(&mut cx, self.dependencies.as_ref(), - |dep| dep, &mut warnings)); + None)); try!(process_dependencies(&mut cx, self.dev_dependencies.as_ref(), - |dep| dep.set_kind(Kind::Development), &mut warnings)); + Some(Kind::Development))); try!(process_dependencies(&mut cx, self.build_dependencies.as_ref(), - |dep| dep.set_kind(Kind::Build), &mut warnings)); + Some(Kind::Build))); if let Some(targets) = self.target.as_ref() { for (name, platform) in targets.iter() { + cx.platform = Some(try!(name.parse())); try!(process_dependencies(&mut cx, platform.dependencies.as_ref(), - |dep| { - dep.set_only_for_platform(Some(name.clone())) - }, &mut warnings)); + None)); try!(process_dependencies(&mut cx, platform.build_dependencies.as_ref(), - |dep| { - dep.set_only_for_platform(Some(name.clone())) - .set_kind(Kind::Build) - }, &mut warnings)); + Some(Kind::Build))); try!(process_dependencies(&mut cx, platform.dev_dependencies.as_ref(), - |dep| { - dep.set_only_for_platform(Some(name.clone())) - .set_kind(Kind::Development) - }, &mut warnings)); + Some(Kind::Development))); } } } @@ -665,12 +662,10 @@ fn validate_bench_name(target: &TomlTarget) -> CargoResult<()> { } } -fn process_dependencies(cx: &mut Context, - new_deps: Option<&HashMap>, - mut f: F, - warnings: &mut Vec) -> CargoResult<()> - where F: FnMut(DependencyInner) -> DependencyInner -{ +fn process_dependencies(cx: &mut Context, + new_deps: Option<&HashMap>, + kind: Option) + -> CargoResult<()> { let dependencies = match new_deps { Some(ref dependencies) => dependencies, None => return Ok(()) @@ -685,10 +680,13 @@ fn process_dependencies(cx: &mut Context, TomlDependency::Detailed(ref details) => details.clone(), }; - if details.version.is_none() && details.path.is_none() && details.git.is_none() { - warnings.push(format!("warning: dependency ({}) specified without providing a local \ - path, Git repository, or version to use. This will be \ - considered an error in future versions", n)); + if details.version.is_none() && details.path.is_none() && + details.git.is_none() { + cx.warnings.push(format!("warning: dependency ({}) specified \ + without providing a local path, Git \ + repository, or version to use. This will \ + be considered an error in future \ + versions", n)); } let reference = details.branch.clone().map(GitReference::Branch) @@ -711,16 +709,16 @@ fn process_dependencies(cx: &mut Context, } }.unwrap_or(try!(SourceId::for_central(cx.config))); - let dep = try!(DependencyInner::parse(&n, - details.version.as_ref() - .map(|v| &v[..]), - &new_source_id)); - let dep = f(dep) - .set_features(details.features.unwrap_or(Vec::new())) - .set_default_features(details.default_features.unwrap_or(true)) - .set_optional(details.optional.unwrap_or(false)) - .into_dependency(); - cx.deps.push(dep); + let version = details.version.as_ref().map(|v| &v[..]); + let mut dep = try!(DependencyInner::parse(&n, version, &new_source_id)); + dep = dep.set_features(details.features.unwrap_or(Vec::new())) + .set_default_features(details.default_features.unwrap_or(true)) + .set_optional(details.optional.unwrap_or(false)) + .set_platform(cx.platform.clone()); + if let Some(kind) = kind { + dep = dep.set_kind(kind); + } + cx.deps.push(dep.into_dependency()); } Ok(()) diff --git a/src/doc/manifest.md b/src/doc/manifest.md index 89e900f177c..864794aad08 100644 --- a/src/doc/manifest.md +++ b/src/doc/manifest.md @@ -166,19 +166,36 @@ The syntax of the requirement strings is described in the [crates.io guide](crates-io.html#using-cratesio-based-crates). Platform-specific dependencies take the same format, but are listed under the -`target.$triple` section: +`target` section. Normally Rust-like `#[cfg]` syntax will be used to define +these sections: ```toml -[target.x86_64-pc-windows-gnu.dependencies] +[target.'cfg(windows)'.dependencies] winhttp = "0.4.0" -[target.i686-unknown-linux-gnu.dependencies] +[target.'cfg(unix)'.dependencies] openssl = "1.0.1" + +[target.'cfg(target_pointer_width = "32")'.dependencies] native = { path = "native/i686" } -[target.x86_64-unknown-linux-gnu.dependencies] +[target.'cfg(target_pointer_width = "64")'.dependencies] +native = { path = "native/i686" } +``` + +Like with Rust, the syntax here supports the `not`, `any`, and `all` operators +to combine various cfg name/value pairs. Note that the `cfg` syntax has only +been available since Cargo 0.9.0 (Rust 1.8.0). + +In addition to `#[cfg]` syntax, Cargo also supports listing out the full target +the dependencies would apply to: + +```toml +[target.x86_64-pc-windows-gnu.dependencies] +winhttp = "0.4.0" + +[target.i686-unknown-linux-gnu.dependencies] openssl = "1.0.1" -native = { path = "native/x86_64" } ``` If you’re using a custom target specification, quote the full path and file diff --git a/tests/support/registry.rs b/tests/support/registry.rs index 63f0532453f..5452fe58373 100644 --- a/tests/support/registry.rs +++ b/tests/support/registry.rs @@ -21,7 +21,7 @@ pub fn dl_url() -> Url { Url::from_file_path(&*dl_path()).ok().unwrap() } pub struct Package { name: String, vers: String, - deps: Vec<(String, String, &'static str)>, + deps: Vec<(String, String, &'static str, String)>, files: Vec<(String, String)>, yanked: bool, } @@ -64,12 +64,23 @@ impl Package { } pub fn dep(&mut self, name: &str, vers: &str) -> &mut Package { - self.deps.push((name.to_string(), vers.to_string(), "normal")); + self.deps.push((name.to_string(), vers.to_string(), "normal", + "null".to_string())); + self + } + + pub fn target_dep(&mut self, + name: &str, + vers: &str, + target: &str) -> &mut Package { + self.deps.push((name.to_string(), vers.to_string(), "normal", + format!("\"{}\"", target))); self } pub fn dev_dep(&mut self, name: &str, vers: &str) -> &mut Package { - self.deps.push((name.to_string(), vers.to_string(), "dev")); + self.deps.push((name.to_string(), vers.to_string(), "dev", + "null".to_string())); self } @@ -83,14 +94,14 @@ impl Package { self.make_archive(); // Figure out what we're going to write into the index - let deps = self.deps.iter().map(|&(ref name, ref req, ref kind)| { + let deps = self.deps.iter().map(|&(ref name, ref req, ref kind, ref target)| { format!("{{\"name\":\"{}\",\ \"req\":\"{}\",\ \"features\":[],\ \"default_features\":false,\ - \"target\":null,\ + \"target\":{},\ \"optional\":false,\ - \"kind\":\"{}\"}}", name, req, kind) + \"kind\":\"{}\"}}", name, req, target, kind) }).collect::>().connect(","); let cksum = { let mut c = Vec::new(); @@ -141,15 +152,20 @@ impl Package { version = "{}" authors = [] "#, self.name, self.vers); - for &(ref dep, ref req, kind) in self.deps.iter() { - manifest.push_str(&format!(r#" - [{}dependencies.{}] - version = "{}" - "#, match kind { + for &(ref dep, ref req, kind, ref target) in self.deps.iter() { + let target = match &target[..] { + "null" => String::new(), + t => format!("target.{}.", t), + }; + let kind = match kind { "build" => "build-", "dev" => "dev-", _ => "" - }, dep, req)); + }; + manifest.push_str(&format!(r#" + [{}{}dependencies.{}] + version = "{}" + "#, target, kind, dep, req)); } let dst = self.archive_dst(); diff --git a/tests/test_cargo_cfg.rs b/tests/test_cargo_cfg.rs new file mode 100644 index 00000000000..9ac9457dc62 --- /dev/null +++ b/tests/test_cargo_cfg.rs @@ -0,0 +1,333 @@ +use std::str::FromStr; +use std::fmt; + +use cargo::util::{Cfg, CfgExpr}; +use hamcrest::assert_that; + +use support::{project, execs, COMPILING, UPDATING, DOWNLOADING}; +use support::registry::Package; + +macro_rules! c { + ($a:ident) => ( + Cfg::Name(stringify!($a).to_string()) + ); + ($a:ident = $e:expr) => ( + Cfg::KeyPair(stringify!($a).to_string(), $e.to_string()) + ); +} + +macro_rules! e { + (any($($t:tt),*)) => (CfgExpr::Any(vec![$(e!($t)),*])); + (all($($t:tt),*)) => (CfgExpr::All(vec![$(e!($t)),*])); + (not($($t:tt)*)) => (CfgExpr::Not(Box::new(e!($($t)*)))); + (($($t:tt)*)) => (e!($($t)*)); + ($($t:tt)*) => (CfgExpr::Value(c!($($t)*))); +} + +fn good(s: &str, expected: T) + where T: FromStr + PartialEq + fmt::Debug, + T::Err: fmt::Display +{ + let c = match T::from_str(s) { + Ok(c) => c, + Err(e) => panic!("failed to parse `{}`: {}", s, e), + }; + assert_eq!(c, expected); +} + +fn bad(s: &str, err: &str) + where T: FromStr + fmt::Display, T::Err: fmt::Display +{ + let e = match T::from_str(s) { + Ok(cfg) => panic!("expected `{}` to not parse but got {}", s, cfg), + Err(e) => e.to_string(), + }; + assert!(e.contains(err), "when parsing `{}`,\n\"{}\" not contained \ + inside: {}", s, err, e); +} + +#[test] +fn cfg_syntax() { + good("foo", c!(foo)); + good("_bar", c!(_bar)); + good(" foo", c!(foo)); + good(" foo ", c!(foo)); + good(" foo = \"bar\"", c!(foo = "bar")); + good("foo=\"\"", c!(foo = "")); + good(" foo=\"3\" ", c!(foo = "3")); + good("foo = \"3 e\"", c!(foo = "3 e")); +} + +#[test] +fn cfg_syntax_bad() { + bad::("", "found nothing"); + bad::(" ", "found nothing"); + bad::("\t", "unexpected character"); + bad::("7", "unexpected character"); + bad::("=", "expected identifier"); + bad::(",", "expected identifier"); + bad::("(", "expected identifier"); + bad::("foo (", "malformed cfg value"); + bad::("bar =", "expected a string"); + bad::("bar = \"", "unterminated string"); + bad::("foo, bar", "malformed cfg value"); +} + +#[test] +fn cfg_expr() { + good("foo", e!(foo)); + good("_bar", e!(_bar)); + good(" foo", e!(foo)); + good(" foo ", e!(foo)); + good(" foo = \"bar\"", e!(foo = "bar")); + good("foo=\"\"", e!(foo = "")); + good(" foo=\"3\" ", e!(foo = "3")); + good("foo = \"3 e\"", e!(foo = "3 e")); + + good("all()", e!(all())); + good("all(a)", e!(all(a))); + good("all(a, b)", e!(all(a, b))); + good("all(a, )", e!(all(a))); + good("not(a = \"b\")", e!(not(a = "b"))); + good("not(all(a))", e!(not(all(a)))); +} + +#[test] +fn cfg_expr_bad() { + bad::(" ", "found nothing"); + bad::(" all", "expected `(`"); + bad::("all(a", "expected `)`"); + bad::("not", "expected `(`"); + bad::("not(a", "expected `)`"); + bad::("a = ", "expected a string"); + bad::("all(not())", "expected identifier"); + bad::("foo(a)", "consider using all() or any() explicitly"); +} + +#[test] +fn cfg_matches() { + assert!(e!(foo).matches(&[c!(bar), c!(foo), c!(baz)])); + assert!(e!(any(foo)).matches(&[c!(bar), c!(foo), c!(baz)])); + assert!(e!(any(foo, bar)).matches(&[c!(bar)])); + assert!(e!(any(foo, bar)).matches(&[c!(foo)])); + assert!(e!(all(foo, bar)).matches(&[c!(foo), c!(bar)])); + assert!(e!(all(foo, bar)).matches(&[c!(foo), c!(bar)])); + assert!(e!(not(foo)).matches(&[c!(bar)])); + assert!(e!(not(foo)).matches(&[])); + assert!(e!(any((not(foo)), (all(foo, bar)))).matches(&[c!(bar)])); + assert!(e!(any((not(foo)), (all(foo, bar)))).matches(&[c!(foo), c!(bar)])); + + assert!(!e!(foo).matches(&[])); + assert!(!e!(foo).matches(&[c!(bar)])); + assert!(!e!(foo).matches(&[c!(fo)])); + assert!(!e!(any(foo)).matches(&[])); + assert!(!e!(any(foo)).matches(&[c!(bar)])); + assert!(!e!(any(foo)).matches(&[c!(bar), c!(baz)])); + assert!(!e!(all(foo)).matches(&[c!(bar), c!(baz)])); + assert!(!e!(all(foo, bar)).matches(&[c!(bar)])); + assert!(!e!(all(foo, bar)).matches(&[c!(foo)])); + assert!(!e!(all(foo, bar)).matches(&[])); + assert!(!e!(not(bar)).matches(&[c!(bar)])); + assert!(!e!(not(bar)).matches(&[c!(baz), c!(bar)])); + assert!(!e!(any((not(foo)), (all(foo, bar)))).matches(&[c!(foo)])); +} + +fn setup() {} + +test!(cfg_easy { + if !::is_nightly() { return } + + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "a" + version = "0.0.1" + authors = [] + + [target.'cfg(unix)'.dependencies] + b = { path = 'b' } + [target."cfg(windows)".dependencies] + b = { path = 'b' } + "#) + .file("src/lib.rs", "extern crate b;") + .file("b/Cargo.toml", r#" + [package] + name = "b" + version = "0.0.1" + authors = [] + "#) + .file("b/src/lib.rs", ""); + assert_that(p.cargo_process("build").arg("-v"), + execs().with_status(0)); +}); + +test!(dont_include { + if !::is_nightly() { return } + + let other_family = if cfg!(unix) {"windows"} else {"unix"}; + let p = project("foo") + .file("Cargo.toml", &format!(r#" + [package] + name = "a" + version = "0.0.1" + authors = [] + + [target.'cfg({})'.dependencies] + b = {{ path = 'b' }} + "#, other_family)) + .file("src/lib.rs", "") + .file("b/Cargo.toml", r#" + [package] + name = "b" + version = "0.0.1" + authors = [] + "#) + .file("b/src/lib.rs", ""); + assert_that(p.cargo_process("build"), + execs().with_status(0).with_stdout(&format!("\ +{compiling} a v0.0.1 ([..]) +", compiling = COMPILING))); +}); + +test!(works_through_the_registry { + if !::is_nightly() { return } + + Package::new("foo", "0.1.0").publish(); + Package::new("bar", "0.1.0") + .target_dep("foo", "0.1.0", "cfg(unix)") + .target_dep("foo", "0.1.0", "cfg(windows)") + .publish(); + + let p = project("a") + .file("Cargo.toml", &r#" + [package] + name = "a" + version = "0.0.1" + authors = [] + + [dependencies] + bar = "0.1.0" + "#) + .file("src/lib.rs", "extern crate bar;"); + + assert_that(p.cargo_process("build"), + execs().with_status(0).with_stdout(&format!("\ +{updating} registry [..] +{downloading} [..] +{downloading} [..] +{compiling} foo v0.1.0 ([..]) +{compiling} bar v0.1.0 ([..]) +{compiling} a v0.0.1 ([..]) +", compiling = COMPILING, updating = UPDATING, downloading = DOWNLOADING))); +}); + +test!(bad_target_spec { + let p = project("a") + .file("Cargo.toml", &r#" + [package] + name = "a" + version = "0.0.1" + authors = [] + + [target.'cfg(4)'.dependencies] + bar = "0.1.0" + "#) + .file("src/lib.rs", ""); + + assert_that(p.cargo_process("build"), + execs().with_status(101).with_stderr("\ +failed to parse manifest at `[..]` + +Caused by: + failed to parse `4` as a cfg expression + +Caused by: + unexpected character in cfg `4`, [..] +")); +}); + +test!(bad_target_spec2 { + let p = project("a") + .file("Cargo.toml", &r#" + [package] + name = "a" + version = "0.0.1" + authors = [] + + [target.'cfg(foo =)'.dependencies] + bar = "0.1.0" + "#) + .file("src/lib.rs", ""); + + assert_that(p.cargo_process("build"), + execs().with_status(101).with_stderr("\ +failed to parse manifest at `[..]` + +Caused by: + failed to parse `foo =` as a cfg expression + +Caused by: + expected a string, found nothing +")); +}); + +test!(multiple_match_ok { + if !::is_nightly() { return } + + let p = project("foo") + .file("Cargo.toml", &format!(r#" + [package] + name = "a" + version = "0.0.1" + authors = [] + + [target.'cfg(unix)'.dependencies] + b = {{ path = 'b' }} + [target.'cfg(target_family = "unix")'.dependencies] + b = {{ path = 'b' }} + [target."cfg(windows)".dependencies] + b = {{ path = 'b' }} + [target.'cfg(target_family = "windows")'.dependencies] + b = {{ path = 'b' }} + [target."cfg(any(windows, unix))".dependencies] + b = {{ path = 'b' }} + + [target.{}.dependencies] + b = {{ path = 'b' }} + "#, ::rustc_host())) + .file("src/lib.rs", "extern crate b;") + .file("b/Cargo.toml", r#" + [package] + name = "b" + version = "0.0.1" + authors = [] + "#) + .file("b/src/lib.rs", ""); + assert_that(p.cargo_process("build").arg("-v"), + execs().with_status(0)); +}); + +test!(any_ok { + if !::is_nightly() { return } + + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "a" + version = "0.0.1" + authors = [] + + [target."cfg(any(windows, unix))".dependencies] + b = { path = 'b' } + "#) + .file("src/lib.rs", "extern crate b;") + .file("b/Cargo.toml", r#" + [package] + name = "b" + version = "0.0.1" + authors = [] + "#) + .file("b/src/lib.rs", ""); + assert_that(p.cargo_process("build").arg("-v"), + execs().with_status(0)); +}); diff --git a/tests/tests.rs b/tests/tests.rs index a79eb99bd51..b67e02a0ffb 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -70,6 +70,7 @@ mod test_cargo_verify_project; mod test_cargo_version; mod test_shell; mod test_cargo_death; +mod test_cargo_cfg; thread_local!(static RUSTC: Rustc = Rustc::new("rustc").unwrap());