use std::{
collections::{BTreeSet, HashMap},
process::Command,
str::FromStr,
sync::OnceLock,
};
use bitflags::bitflags;
use cargo_platform::{Cfg, CfgExpr, Platform};
use crate::buckal_warn;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Os {
Windows,
Macos,
Linux,
}
impl Os {
pub fn buck_label(self) -> &'static str {
match self {
Os::Windows => "prelude//os/constraints:windows",
Os::Macos => "prelude//os/constraints:macos",
Os::Linux => "prelude//os/constraints:linux",
}
}
pub fn key(self) -> &'static str {
match self {
Os::Windows => "windows",
Os::Macos => "macos",
Os::Linux => "linux",
}
}
}
static SUPPORTED_TARGETS: &[(Os, &str)] = &[
(Os::Macos, "aarch64-apple-darwin"),
(Os::Windows, "x86_64-pc-windows-msvc"),
(Os::Linux, "x86_64-unknown-linux-gnu"),
];
static CFG_CACHE: OnceLock<HashMap<&'static str, Vec<Cfg>>> = OnceLock::new();
fn get_rustc_cfgs_for_triple(triple: &'static str) -> Option<Vec<Cfg>> {
match Command::new("rustc")
.args(["--print=cfg", "--target", triple])
.output()
{
Ok(output) if output.status.success() => {
let cfgs: Vec<Cfg> = String::from_utf8_lossy(&output.stdout)
.lines()
.filter_map(|line| Cfg::from_str(line).ok())
.collect();
Some(cfgs)
}
Ok(output) => {
buckal_warn!(
"Failed to run `rustc --print=cfg --target {}`: {}",
triple,
String::from_utf8_lossy(&output.stderr)
);
None
}
Err(error) => {
buckal_warn!(
"Failed to execute `rustc --print=cfg --target {}`: {}",
triple,
error
);
None
}
}
}
fn cfg_cache() -> &'static HashMap<&'static str, Vec<Cfg>> {
CFG_CACHE.get_or_init(|| {
let results = std::thread::scope(|scope| {
let handles = SUPPORTED_TARGETS
.iter()
.map(|(_, triple)| {
let triple = *triple;
scope.spawn(move || (triple, get_rustc_cfgs_for_triple(triple)))
})
.collect::<Vec<_>>();
handles
.into_iter()
.map(|handle| {
handle
.join()
.expect("Thread panicked while querying rustc cfg values. This may indicate rustc is not properly installed or accessible.")
})
.collect::<Vec<_>>()
});
let mut map = HashMap::new();
for (triple, cfgs) in results {
if let Some(cfgs) = cfgs {
map.insert(triple, cfgs);
}
}
map
})
}
pub fn buck_labels(oses: &BTreeSet<Os>) -> BTreeSet<String> {
oses.iter().map(|os| os.buck_label().to_string()).collect()
}
pub fn oses_from_platform(platform: &Platform) -> BTreeSet<Os> {
let cfgs = cfg_cache();
SUPPORTED_TARGETS
.iter()
.filter_map(|(os, triple)| {
cfgs.get(triple).and_then(|cfgs| {
if platform.matches(triple, cfgs) {
Some(*os)
} else {
None
}
})
})
.collect()
}
fn cfg_is_target_only(cfg: &Cfg) -> bool {
match cfg {
Cfg::Name(name) => matches!(name.as_str(), "windows" | "unix"),
Cfg::KeyPair(key, _) => matches!(
key.as_str(),
"target_arch"
| "target_os"
| "target_family"
| "target_env"
| "target_vendor"
| "target_endian"
| "target_pointer_width"
| "target_feature"
),
}
}
fn cfg_expr_is_target_only(expr: &CfgExpr) -> bool {
match expr {
CfgExpr::Not(inner) => cfg_expr_is_target_only(inner),
CfgExpr::All(items) | CfgExpr::Any(items) => items.iter().all(cfg_expr_is_target_only),
CfgExpr::Value(cfg) => cfg_is_target_only(cfg),
CfgExpr::True | CfgExpr::False => false,
}
}
pub fn platform_is_target_only(platform: &Platform) -> bool {
match platform {
Platform::Name(_) => true,
Platform::Cfg(expr) => cfg_expr_is_target_only(expr),
}
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PlatformMask: u32 {
const WINDOWS = 0b0001;
const MACOS = 0b0010;
const LINUX = 0b0100;
}
}
impl PlatformMask {
pub fn to_oses(self) -> BTreeSet<Os> {
let mut set = BTreeSet::new();
if self.contains(Self::WINDOWS) {
set.insert(Os::Windows);
}
if self.contains(Self::MACOS) {
set.insert(Os::Macos);
}
if self.contains(Self::LINUX) {
set.insert(Os::Linux);
}
set
}
}
static PACKAGE_PLATFORMS: phf::Map<&'static str, PlatformMask> = phf::phf_map! {
"android_system_properties" => PlatformMask::LINUX,
"hyper-named-pipe" => PlatformMask::WINDOWS,
"libredox" => PlatformMask::LINUX,
"redox_syscall" => PlatformMask::LINUX,
"system-configuration" => PlatformMask::MACOS,
"windows-future" => PlatformMask::WINDOWS,
"windows" => PlatformMask::WINDOWS,
"winreg" => PlatformMask::WINDOWS,
};
pub fn lookup_platforms(package_name: &str) -> Option<BTreeSet<Os>> {
PACKAGE_PLATFORMS
.get(package_name)
.map(|mask| mask.to_oses())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))]
fn test_rustc_cfgs_for_triple_with_available_rustc() {
let cfgs = get_rustc_cfgs_for_triple("x86_64-unknown-linux-gnu").expect(
"expected `rustc --print=cfg --target x86_64-unknown-linux-gnu` to succeed (rustc missing or target not installed?)",
);
assert!(!cfgs.is_empty(), "rustc cfgs should not be empty");
let rendered_cfgs: BTreeSet<String> = cfgs.iter().map(ToString::to_string).collect();
let has_name = |name: &str| {
cfgs.iter()
.any(|cfg| matches!(cfg, Cfg::Name(n) if n == name))
};
let has_key_pair = |key: &str, val: &str| {
cfgs.iter()
.any(|cfg| matches!(cfg, Cfg::KeyPair(k, v) if k == key && v == val))
};
assert!(
has_key_pair("target_arch", "x86_64"),
"missing `target_arch = \"x86_64\"` in rustc cfgs: {rendered_cfgs:?}"
);
assert!(
has_key_pair("target_os", "linux"),
"missing `target_os = \"linux\"` in rustc cfgs: {rendered_cfgs:?}"
);
assert!(
has_key_pair("target_env", "gnu"),
"missing `target_env = \"gnu\"` in rustc cfgs: {rendered_cfgs:?}"
);
assert!(
has_key_pair("target_family", "unix"),
"missing `target_family = \"unix\"` in rustc cfgs: {rendered_cfgs:?}"
);
assert!(
has_name("unix"),
"missing `unix` flag in rustc cfgs: {rendered_cfgs:?}"
);
assert!(
has_name("debug_assertions"),
"missing `debug_assertions` flag in rustc cfgs: {rendered_cfgs:?}"
);
let expr = cargo_platform::CfgExpr::from_str(
"all(target_arch = \"x86_64\", target_os = \"linux\", target_env = \"gnu\", unix)",
)
.expect("test cfg expression should parse");
assert!(
expr.matches(&cfgs),
"expected cfg expression `{expr}` to match rustc cfgs: {rendered_cfgs:?}"
);
}
#[test]
fn test_cfg_parsing_direct() {
let test_output = "target_arch=\"x86_64\"\ntarget_os=\"linux\"\ntarget_endian=\"little\"\n";
let cfgs: Vec<Cfg> = String::from_utf8_lossy(test_output.as_bytes())
.lines()
.filter_map(|line| Cfg::from_str(line).ok())
.collect();
assert_eq!(cfgs.len(), 3);
let target_arch_cfg = cfgs
.iter()
.find(|cfg| cfg.to_string().contains("target_arch"));
assert!(target_arch_cfg.is_some());
assert!(target_arch_cfg.unwrap().to_string().contains("x86_64"));
let target_os_cfg = cfgs
.iter()
.find(|cfg| cfg.to_string().contains("target_os"));
assert!(target_os_cfg.is_some());
assert!(target_os_cfg.unwrap().to_string().contains("linux"));
}
#[test]
fn test_cfg_parsing_boolean() {
let test_output = "debug_assertions\nverbose_errors\n";
let cfgs: Vec<Cfg> = String::from_utf8_lossy(test_output.as_bytes())
.lines()
.filter_map(|line| Cfg::from_str(line).ok())
.collect();
assert_eq!(cfgs.len(), 2);
assert!(cfgs.iter().any(|cfg| cfg.to_string() == "debug_assertions"));
assert!(cfgs.iter().any(|cfg| cfg.to_string() == "verbose_errors"));
}
#[test]
fn test_cfg_parsing_invalid_lines() {
let test_output = "target_arch=\"x86_64\"\ninvalid_line=bad_value\nrandom text\n";
let cfgs: Vec<Cfg> = String::from_utf8_lossy(test_output.as_bytes())
.lines()
.filter_map(|line| Cfg::from_str(line).ok())
.collect();
assert_eq!(cfgs.len(), 1);
assert!(
cfgs.iter()
.any(|cfg| cfg.to_string().contains("target_arch"))
);
}
#[test]
fn test_platform_mask_operations() {
let mask = PlatformMask::WINDOWS | PlatformMask::LINUX;
assert!(mask.contains(PlatformMask::WINDOWS));
assert!(!mask.contains(PlatformMask::MACOS));
assert!(mask.contains(PlatformMask::LINUX));
let oses = mask.to_oses();
let mut expected = BTreeSet::new();
expected.insert(Os::Windows);
expected.insert(Os::Linux);
assert_eq!(oses, expected);
}
#[test]
fn test_os_buck_labels() {
assert_eq!(Os::Windows.buck_label(), "prelude//os/constraints:windows");
assert_eq!(Os::Macos.buck_label(), "prelude//os/constraints:macos");
assert_eq!(Os::Linux.buck_label(), "prelude//os/constraints:linux");
assert_eq!(Os::Windows.key(), "windows");
assert_eq!(Os::Macos.key(), "macos");
assert_eq!(Os::Linux.key(), "linux");
}
#[test]
fn test_lookup_platforms() {
let windows_pkgs = lookup_platforms("windows-future").unwrap();
let mut expected = BTreeSet::new();
expected.insert(Os::Windows);
assert_eq!(windows_pkgs, expected);
let macos_pkgs = lookup_platforms("system-configuration").unwrap();
let mut expected = BTreeSet::new();
expected.insert(Os::Macos);
assert_eq!(macos_pkgs, expected);
assert!(lookup_platforms("unknown-package").is_none());
}
#[test]
fn test_buck_labels_utility() {
let mut oses = BTreeSet::new();
oses.insert(Os::Windows);
oses.insert(Os::Linux);
let labels = buck_labels(&oses);
let mut expected = BTreeSet::new();
expected.insert("prelude//os/constraints:windows".to_string());
expected.insert("prelude//os/constraints:linux".to_string());
assert_eq!(labels, expected);
}
#[test]
fn test_supported_targets() {
assert!(!SUPPORTED_TARGETS.is_empty());
for (os, triple) in SUPPORTED_TARGETS {
assert!(matches!(os, Os::Windows | Os::Macos | Os::Linux));
assert!(!triple.is_empty());
}
}
}