| // 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()))); |
| } |
| } |