// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Maps Rust targets to Chromium targets.
use std::collections::BTreeSet;
use cargo_platform::{Cfg, CfgExpr};
use once_cell::sync::OnceCell;
pub use cargo_platform::Platform;
/// A set of platforms: either the set of all platforms, or a finite set of
/// platform configurations.
#[derive(Clone, Debug)]
pub enum PlatformSet {
/// Matches any platform configuration.
All,
/// Matches a finite set of configurations.
// Note we use a `BTreeSet` because stable iteration order is desired when
// generating build files.
Platforms(BTreeSet<Platform>),
}
impl PlatformSet {
/// A `PlatformSet` that matches no platforms. Useful as a starting point
/// when iteratively adding platforms with `add`.
pub fn empty() -> Self {
Self::Platforms(BTreeSet::new())
}
/// A `PlatformSet` that matches one platform filter.
#[cfg(test)]
pub fn one(filter: Option<Platform>) -> Self {
let mut ps = Self::empty();
ps.add(filter);
ps
}
/// Add a single platform filter to `self`. The resulting set is superset of
/// the original. If `filter` is `None`, `self` becomes `PlatformSet::All`.
pub fn add(&mut self, filter: Option<Platform>) {
let set = match self {
// If the set is already all platforms, no need to add `filter`.
Self::All => return,
Self::Platforms(set) => set,
};
match filter {
None => *self = Self::All,
Some(platform) => {
set.insert(platform);
}
}
}
}
// Whether a CfgExpr matches any build target supported by Chromium.
fn supported_cfg_expr(e: &CfgExpr) -> bool {
fn validity_can_be_true(v: ExprValidity) -> bool {
match v {
ExprValidity::Valid => true,
ExprValidity::AlwaysTrue => true,
ExprValidity::AlwaysFalse => false,
}
}
fn recurse(e: &CfgExpr) -> ExprValidity {
match e {
CfgExpr::All(x) => {
if x.iter().all(|e| validity_can_be_true(recurse(e))) {
// TODO(danakj): We don't combine to anything fancy.
// Technically, if they are all AlwaysTrue it should combine
// as such, and then it could be inverted to AlwaysFalse.
ExprValidity::Valid
} else {
ExprValidity::AlwaysFalse
}
}
CfgExpr::Any(x) => {
if x.iter().any(|e| validity_can_be_true(recurse(e))) {
// TODO(danakj): We don't combine to anything fancy.
// Technically, if anything is AlwaysTrue it should combine
// as such, and then it could be inverted to AlwaysFalse.
ExprValidity::Valid
} else {
ExprValidity::AlwaysFalse
}
}
CfgExpr::Not(x) => match recurse(x) {
ExprValidity::AlwaysFalse => ExprValidity::AlwaysTrue,
ExprValidity::Valid => ExprValidity::Valid,
ExprValidity::AlwaysTrue => ExprValidity::AlwaysFalse,
},
CfgExpr::Value(v) => supported_cfg_value(v),
}
}
validity_can_be_true(recurse(e))
}
// If a Cfg option is always true/false in Chromium, or needs to be conditional
// in the build file's rules.
fn supported_cfg_value(cfg: &Cfg) -> ExprValidity {
if supported_os_cfgs().iter().any(|c| c == cfg) {
ExprValidity::Valid // OS is always conditional, as we support more than one.
} else if supported_arch_cfgs().iter().any(|c| c == cfg) {
ExprValidity::Valid // Arch is always conditional, as we support more than one.
} else {
// Other configs may resolve to AlwaysTrue or AlwaysFalse. If it's
// unknown, we treat it as AlwaysFalse since we don't know how to
// convert it to a build file condition.
supported_other_cfgs()
.iter()
.find(|(c, _)| c == cfg)
.map(|(_, validity)| *validity)
.unwrap_or(ExprValidity::AlwaysFalse)
}
}
/// Whether `platform`, either an explicit rustc target triple or a `cfg(...)`
/// expression, matches any build target supported by Chromium.
pub fn matches_supported_target(platform: &Platform) -> bool {
match platform {
Platform::Name(name) => SUPPORTED_NAMED_PLATFORMS.iter().any(|p| *p == name),
Platform::Cfg(expr) => supported_cfg_expr(expr),
}
}
/// Remove terms containing unsupported platforms from `platform`, assuming
/// `matches_supported_target(&platform)` is true.
///
/// `platform` may contain a cfg(...) expression referencing platforms we don't
/// support: for example, `cfg(any(unix, target_os = "wasi"))`. However, such an
/// expression may still be true on configurations we do support.
///
/// `filter_unsupported_platform_terms` returns a new platform filter without
/// unsupported terms that is logically equivalent for the set of platforms we
/// do support, or `None` if the new filter would be true for all supported
/// platforms. This is useful when generating conditional expressions in build
/// files from such a cfg(...) expression.
///
/// Assumes `matches_supported_target(&platform)` is true. If not, the function
/// may return an invalid result or panic.
pub fn filter_unsupported_platform_terms(platform: Platform) -> Option<Platform> {
use ExprValidity::*;
match platform {
// If it's a target name, do nothing since `is_supported` is true.
x @ Platform::Name(_) => Some(x),
// Rewrite `cfg_expr` to be valid.
Platform::Cfg(mut cfg_expr) => match cfg_expr_filter_visitor(&mut cfg_expr) {
Valid => Some(Platform::Cfg(cfg_expr)),
AlwaysTrue => None,
AlwaysFalse => unreachable!("cfg would be false on all supported platforms"),
},
}
}
// The validity of a cfg expr for our set of supported platforms.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ExprValidity {
// Contains only terms for supported platforms.
Valid,
// Contains terms for unsupported platforms, and would evaluate to true on
// all supported platforms.
AlwaysTrue,
// Contains terms for unsupported platforms, and would evaluate to false on
// all supported platforms.
AlwaysFalse,
}
// Rewrites `cfg_expr` to exclude unsupported terms. `ExprValidity::Valid` if
// the rewritten expr is valid: it contains no unsupported terms. Otherwise
// returns `AlwaysTrue` or `AlwaysFalse`.
fn cfg_expr_filter_visitor(cfg_expr: &mut CfgExpr) -> ExprValidity {
use ExprValidity::*;
// Any logical operation on a set of valid expressions also yields a valid
// expression. If any of the set is invalid, we must apply special handling
// to remove the invalid term or decide the expression is always true or
// false.
match cfg_expr {
// A not(...) expr inverts the truth value of an invalid expr.
CfgExpr::Not(sub_expr) => match cfg_expr_filter_visitor(sub_expr) {
Valid => Valid,
AlwaysTrue => AlwaysFalse,
AlwaysFalse => AlwaysTrue,
},
// An all(...) expr is always false if any term is always false. If any
// term is always true, it can be removed.
CfgExpr::All(sub_exprs) => {
let mut validity = Valid;
sub_exprs.retain_mut(|e| match cfg_expr_filter_visitor(e) {
// Keep valid terms.
Valid => true,
// Remove always-true terms.
AlwaysTrue => false,
// If a term is always false, it doesn't matter; we will discard
// this expr.
AlwaysFalse => {
validity = AlwaysFalse;
true
}
});
if validity == AlwaysFalse {
AlwaysFalse
} else if sub_exprs.is_empty() {
// We only reach this if all the terms we removed were always
// true, in which case the expression is always true.
AlwaysTrue
} else if sub_exprs.len() == 1 {
// If only one term remains, we can simplify by replacing
// all(<term>) with <term>.
let new_expr = sub_exprs.drain(..).next().unwrap();
*cfg_expr = new_expr;
Valid
} else {
Valid
}
}
// An any(...) expr is always true if any term is always true. If any
// term is always false, it can be removed.
CfgExpr::Any(sub_exprs) => {
let mut validity = Valid;
sub_exprs.retain_mut(|e| match cfg_expr_filter_visitor(e) {
// Keep valid terms.
Valid => true,
// If a term is always true, it doesn't matter; we will discard
// this expr.
AlwaysTrue => {
validity = AlwaysTrue;
true
}
// Remove always-false terms.
AlwaysFalse => false,
});
if validity == AlwaysTrue {
AlwaysTrue
} else if sub_exprs.is_empty() {
// We only reach this if all the terms we removed were always
// false, in which case the expression is always false.
AlwaysFalse
} else if sub_exprs.len() == 1 {
// If only one term remains, we can simplify by replacing
// any(<term>) with <term>.
let new_expr = sub_exprs.drain(..).next().unwrap();
*cfg_expr = new_expr;
Valid
} else {
Valid
}
}
CfgExpr::Value(cfg) => supported_cfg_value(cfg),
}
}
fn supported_os_cfgs() -> &'static [Cfg] {
static CFG_SET: OnceCell<Vec<Cfg>> = OnceCell::new();
CFG_SET.get_or_init(|| {
[
// Set of supported OSes for `cfg(target_os = ...)`.
"android", "darwin", "fuchsia", "ios", "linux", "windows",
]
.into_iter()
.map(|os| Cfg::KeyPair("target_os".to_string(), os.to_string()))
.chain(
// Alternative syntax `cfg(unix)` or `cfg(windows)`.
["unix", "windows"].into_iter().map(|os| Cfg::Name(os.to_string())),
)
.collect()
})
}
fn supported_arch_cfgs() -> &'static [Cfg] {
static CFG_SET: OnceCell<Vec<Cfg>> = OnceCell::new();
CFG_SET.get_or_init(|| {
[
// Set of supported arches for `cfg(target_arch = ...)`.
"aarch64", "arm", "x86", "x86_64",
]
.into_iter()
.map(|a| Cfg::KeyPair("target_arch".to_string(), a.to_string()))
.collect()
})
}
fn supported_other_cfgs() -> &'static [(Cfg, ExprValidity)] {
static CFG_SET: OnceCell<Vec<(Cfg, ExprValidity)>> = OnceCell::new();
CFG_SET.get_or_init(|| {
use ExprValidity::*;
vec![
// target_env = "msvc" is always true for us, so it can be dropped from expressions.
(Cfg::KeyPair("target_env".to_string(), "msvc".to_string()), AlwaysTrue),
]
})
}
static SUPPORTED_NAMED_PLATFORMS: &[&str] = &[
"i686-linux-android",
"x86_64-linux-android",
"armv7-linux-android",
"aarch64-linux-android",
"aarch64-fuchsia",
"x86_64-fuchsia",
"aarch64-apple-ios",
"aarch64-apple-ios-macabi",
"armv7-apple-ios",
"x86_64-apple-ios",
"x86_64-apple-ios-macabi",
"i386-apple-ios",
"i686-pc-windows-msvc",
"x86_64-pc-windows-msvc",
"i686-unknown-linux-gnu",
"x86_64-unknown-linux-gnu",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
];
#[cfg(test)]
mod tests {
use super::*;
use cargo_platform::{CfgExpr, Platform};
use std::str::FromStr;
#[test]
fn platform_is_supported() {
for named_platform in SUPPORTED_NAMED_PLATFORMS {
assert!(matches_supported_target(&Platform::Name(named_platform.to_string())));
}
assert!(!matches_supported_target(&Platform::Name("x86_64-unknown-redox".to_string())));
assert!(!matches_supported_target(&Platform::Name("wasm32-wasi".to_string())));
for os in supported_os_cfgs() {
assert!(matches_supported_target(&Platform::Cfg(CfgExpr::Value(os.clone()))));
}
assert!(!matches_supported_target(&Platform::Cfg(
CfgExpr::from_str("target_os = \"redox\"").unwrap()
)));
assert!(!matches_supported_target(&Platform::Cfg(
CfgExpr::from_str("target_os = \"haiku\"").unwrap()
)));
assert!(matches_supported_target(&Platform::Cfg(
CfgExpr::from_str("any(unix, target_os = \"wasi\")").unwrap()
)));
assert!(!matches_supported_target(&Platform::Cfg(
CfgExpr::from_str("all(unix, target_os = \"wasi\")").unwrap()
)));
for arch in supported_arch_cfgs() {
assert!(matches_supported_target(&Platform::Cfg(CfgExpr::Value(arch.clone()))));
}
assert!(!matches_supported_target(&Platform::Cfg(
CfgExpr::from_str("target_arch = \"sparc\"").unwrap()
)));
assert!(matches_supported_target(&Platform::Cfg(
CfgExpr::from_str("not(windows)").unwrap()
)));
}
#[test]
fn filter_unsupported() {
assert_eq!(
filter_unsupported_platform_terms(Platform::Cfg(
CfgExpr::from_str("any(unix, target_os = \"wasi\")").unwrap()
)),
Some(Platform::Cfg(CfgExpr::from_str("unix").unwrap()))
);
assert_eq!(
filter_unsupported_platform_terms(Platform::Cfg(
CfgExpr::from_str("all(not(unix), not(target_os = \"wasi\"))").unwrap()
)),
Some(Platform::Cfg(CfgExpr::from_str("not(unix)").unwrap()))
);
assert_eq!(
filter_unsupported_platform_terms(Platform::Cfg(
CfgExpr::from_str("not(target_os = \"wasi\")").unwrap()
)),
None
);
assert_eq!(
filter_unsupported_platform_terms(Platform::Cfg(
CfgExpr::from_str("not(all(windows, target_vendor = \"uwp\"))").unwrap()
)),
None
);
assert_eq!(
filter_unsupported_platform_terms(Platform::Cfg(
CfgExpr::from_str("not(all(windows, target_env = \"msvc\"))").unwrap()
)),
Some(Platform::Cfg(CfgExpr::from_str("not(windows)").unwrap()))
);
assert_eq!(
filter_unsupported_platform_terms(Platform::Cfg(
CfgExpr::from_str(
"not(all(windows, target_env = \"msvc\", not(target_vendor = \"uwp\")))"
)
.unwrap()
)),
Some(Platform::Cfg(CfgExpr::from_str("not(windows)").unwrap()))
);
}
#[test]
// From windows-targets crate.
fn windows_target_cfgs() {
// Accepted. `windows_raw_dylib` is not a known cfg so considered AlwaysFalse.
let cfg = "all(target_arch = \"aarch64\", target_env = \"msvc\", not(windows_raw_dylib))";
assert_eq!(
filter_unsupported_platform_terms(Platform::Cfg(CfgExpr::from_str(cfg).unwrap())),
Some(Platform::Cfg(CfgExpr::from_str("target_arch = \"aarch64\"").unwrap()))
);
assert!(matches_supported_target(&Platform::Cfg(CfgExpr::from_str(cfg).unwrap())));
// Accepted. `windows_raw_dylib` is not a known cfg so considered AlwaysFalse.
let cfg = "all(any(target_arch = \"x86_64\", target_arch = \"arm64ec\"), \
target_env = \"msvc\", not(windows_raw_dylib))";
assert_eq!(
filter_unsupported_platform_terms(Platform::Cfg(CfgExpr::from_str(cfg).unwrap())),
Some(Platform::Cfg(CfgExpr::from_str("target_arch = \"x86_64\"").unwrap()))
);
// Accepted. `windows_raw_dylib` is not a known cfg so considered AlwaysFalse.
let cfg = "all(target_arch = \"x86\", target_env = \"msvc\", not(windows_raw_dylib))";
assert_eq!(
filter_unsupported_platform_terms(Platform::Cfg(CfgExpr::from_str(cfg).unwrap())),
Some(Platform::Cfg(CfgExpr::from_str("target_arch = \"x86\"").unwrap()))
);
// Rejected for gnu env.
let cfg = "all(target_arch = \"x86\", target_env = \"gnu\", not(target_abi = \"llvm\"), \
not(windows_raw_dylib))";
assert!(!matches_supported_target(&Platform::Cfg(CfgExpr::from_str(cfg).unwrap())));
}
}