Skip to content

Commit

Permalink
Implement cfg-based target-specific dependencies
Browse files Browse the repository at this point in the history
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!
  • Loading branch information
alexcrichton committed Feb 14, 2016
1 parent 3afc341 commit f5d786e
Show file tree
Hide file tree
Showing 11 changed files with 823 additions and 116 deletions.
96 changes: 74 additions & 22 deletions src/cargo/core/dependency.rs
Original file line number Diff line number Diff line change
@@ -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<DependencyInner>,
}

/// The data underlying a Dependency.
#[derive(PartialEq,Clone,Debug)]
#[derive(PartialEq, Clone, Debug)]
pub struct DependencyInner {
name: String,
source_id: SourceId,
Expand All @@ -21,17 +31,15 @@ pub struct DependencyInner {

// This dependency should be used only for this platform.
// `None` means *all platforms*.
only_for_platform: Option<String>,
platform: Option<Platform>,
}

/// Information about a dependency requested by a Cargo manifest.
/// Cheap to copy.
#[derive(PartialEq,Clone,Debug)]
pub struct Dependency {
inner: Rc<DependencyInner>,
#[derive(Clone, Debug, PartialEq)]
pub enum Platform {
Name(String),
Cfg(CfgExpr),
}


#[derive(RustcEncodable)]
struct SerializedDependency<'a> {
name: &'a str,
Expand All @@ -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 {
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -106,7 +114,7 @@ impl DependencyInner {
features: Vec::new(),
default_features: true,
specified_req: None,
only_for_platform: None,
platform: None,
}
}

Expand All @@ -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 {
Expand Down Expand Up @@ -159,9 +167,9 @@ impl DependencyInner {
self
}

pub fn set_only_for_platform(mut self, platform: Option<String>)
-> DependencyInner {
self.only_for_platform = platform;
pub fn set_platform(mut self, platform: Option<Platform>)
-> DependencyInner {
self.platform = platform;
self
}

Expand Down Expand Up @@ -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
Expand All @@ -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<S: Encoder>(&self, s: &mut S) -> Result<(), S::Error> {
self.to_string().encode(s)
}
}

impl FromStr for Platform {
type Err = Box<CargoError>;

fn from_str(s: &str) -> CargoResult<Platform> {
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),
}
}
}
100 changes: 61 additions & 39 deletions src/cargo/ops/cargo_rustc/context.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
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;

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};
Expand Down Expand Up @@ -41,16 +41,20 @@ pub struct Context<'a, 'cfg: 'a> {
host: Layout,
target: Option<Layout>,
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<Vec<Cfg>>,
}

impl<'a, 'cfg> Context<'a, 'cfg> {
pub fn new(resolve: &'a Resolve,
sources: &'a SourceMap<'cfg>,
Expand All @@ -62,12 +66,11 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
profiles: &'a Profiles) -> CargoResult<Context<'a, 'cfg>> {
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[..]
Expand All @@ -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,
Expand All @@ -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<TargetInfo> {
let mut process = util::process(cfg.rustc());
process.arg("-")
.arg("--crate-name").arg("_")
Expand All @@ -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();
Expand All @@ -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
Expand Down Expand Up @@ -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),
Expand All @@ -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),
Expand Down Expand Up @@ -284,9 +306,9 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
pub fn target_filenames(&self, unit: &Unit) -> CargoResult<Vec<String>> {
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();
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/cargo/ops/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 6 additions & 1 deletion src/cargo/sources/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())
}
Expand Down
Loading

0 comments on commit f5d786e

Please sign in to comment.