#[derive(Debug, Clone, Default)]
pub struct SupportedArchitectures {
pub os: Vec<String>,
pub cpu: Vec<String>,
pub libc: Vec<String>,
pub explicit_combinations: Option<Vec<(String, String, String)>>,
}
impl SupportedArchitectures {
pub fn aube_lock_default() -> Self {
let mut combos = vec![
("darwin".to_string(), "arm64".to_string(), String::new()),
("linux".to_string(), "x64".to_string(), "glibc".to_string()),
("linux".to_string(), "x64".to_string(), "musl".to_string()),
(
"linux".to_string(),
"arm64".to_string(),
"glibc".to_string(),
),
("linux".to_string(), "arm64".to_string(), "musl".to_string()),
("win32".to_string(), "x64".to_string(), String::new()),
("win32".to_string(), "arm64".to_string(), String::new()),
];
let host = host_triple();
let host_triple_owned = (host.0.to_string(), host.1.to_string(), host.2.to_string());
if !combos.contains(&host_triple_owned) {
combos.push(host_triple_owned);
}
Self {
os: Vec::new(),
cpu: Vec::new(),
libc: Vec::new(),
explicit_combinations: Some(combos),
}
}
fn combinations(&self) -> Vec<(String, String, String)> {
if let Some(ref explicit) = self.explicit_combinations {
return explicit.clone();
}
let host = host_triple();
let expand = |field: &[String], host_val: &str| -> Vec<String> {
if field.is_empty() {
return vec![host_val.to_string()];
}
field
.iter()
.map(|v| {
if v == "current" {
host_val.to_string()
} else {
v.clone()
}
})
.collect()
};
let os = expand(&self.os, host.0);
let cpu = expand(&self.cpu, host.1);
let libc = expand(&self.libc, host.2);
let mut out = Vec::with_capacity(os.len() * cpu.len() * libc.len());
for o in &os {
for c in &cpu {
for l in &libc {
out.push((o.clone(), c.clone(), l.clone()));
}
}
}
out
}
}
pub fn host_triple() -> (&'static str, &'static str, &'static str) {
let os = match std::env::consts::OS {
"macos" => "darwin",
"windows" => "win32",
other => other,
};
let cpu = match std::env::consts::ARCH {
"x86_64" => "x64",
"x86" => "ia32",
"aarch64" => "arm64",
"powerpc64" => "ppc64",
other => other,
};
let libc = if std::env::consts::OS == "linux" {
detect_linux_libc()
} else {
""
};
(os, cpu, libc)
}
fn detect_linux_libc() -> &'static str {
use std::sync::OnceLock;
static CACHE: OnceLock<&'static str> = OnceLock::new();
CACHE.get_or_init(|| {
if let Ok(maps) = std::fs::read_to_string("/proc/self/maps") {
if maps.contains("/ld-musl-") {
return "musl";
}
if maps.contains("/ld-linux") {
return "glibc";
}
}
let glibc_dirs = [
"/lib",
"/lib64",
"/lib/x86_64-linux-gnu",
"/lib/aarch64-linux-gnu",
];
for dir in glibc_dirs {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let name = entry.file_name();
if name.to_string_lossy().starts_with("ld-linux") {
return "glibc";
}
}
}
}
if let Ok(entries) = std::fs::read_dir("/lib") {
for entry in entries.flatten() {
let name = entry.file_name();
if name.to_string_lossy().starts_with("ld-musl-") {
return "musl";
}
}
}
"glibc"
})
}
fn field_matches(pkg_field: &[String], host: &str) -> bool {
if pkg_field.is_empty() {
return true;
}
let mut has_positive = false;
let mut positive_matched = false;
for entry in pkg_field {
if let Some(neg) = entry.strip_prefix('!') {
if neg == host {
return false;
}
} else {
has_positive = true;
if entry == host {
positive_matched = true;
}
}
}
!has_positive || positive_matched
}
pub fn is_supported(
pkg_os: &[String],
pkg_cpu: &[String],
pkg_libc: &[String],
supported: &SupportedArchitectures,
) -> bool {
for (os, cpu, libc) in supported.combinations() {
if !field_matches(pkg_os, &os) {
continue;
}
if !field_matches(pkg_cpu, &cpu) {
continue;
}
if !libc.is_empty() && !field_matches(pkg_libc, &libc) {
continue;
}
return true;
}
false
}
pub fn filter_graph(
graph: &mut aube_lockfile::LockfileGraph,
supported: &SupportedArchitectures,
ignored: &std::collections::BTreeSet<String>,
) {
use crate::FxHashSet;
use aube_lockfile::DepType;
let is_mismatched =
|pkg: &aube_lockfile::LockedPackage| !is_supported(&pkg.os, &pkg.cpu, &pkg.libc, supported);
for deps in graph.importers.values_mut() {
deps.retain(|dep| {
if dep.dep_type != DepType::Optional {
return true;
}
if ignored.contains(&dep.name) {
return false;
}
!matches!(graph.packages.get(&dep.dep_path), Some(pkg) if is_mismatched(pkg))
});
}
let package_keys: FxHashSet<String> = graph.packages.keys().cloned().collect();
let mismatched_packages: FxHashSet<String> = graph
.packages
.iter()
.filter(|(_, pkg)| is_mismatched(pkg))
.map(|(dep_path, _)| dep_path.clone())
.collect();
for pkg in graph.packages.values_mut() {
let mut removed = Vec::new();
pkg.optional_dependencies.retain(|name, tail| {
let child_key = if package_keys.contains(tail) {
tail.clone()
} else {
format!("{name}@{tail}")
};
let keep = !ignored.contains(name) && !mismatched_packages.contains(&child_key);
if !keep {
removed.push(name.clone());
}
keep
});
for name in removed {
pkg.dependencies.remove(&name);
}
}
let mut reachable: FxHashSet<String> = FxHashSet::default();
let mut stack: Vec<String> = Vec::new();
for deps in graph.importers.values() {
for dep in deps {
stack.push(dep.dep_path.clone());
}
}
while let Some(dep_path) = stack.pop() {
if !reachable.insert(dep_path.clone()) {
continue;
}
if let Some(pkg) = graph.packages.get(&dep_path) {
for (name, tail) in &pkg.dependencies {
if graph.packages.contains_key(tail) {
stack.push(tail.clone());
} else {
let child_key = format!("{name}@{tail}");
if graph.packages.contains_key(&child_key) {
stack.push(child_key);
}
}
}
}
}
graph.packages.retain(|k, _| reachable.contains(k));
}
#[cfg(test)]
mod tests {
use super::*;
fn s(xs: &[&str]) -> Vec<String> {
xs.iter().map(|x| (*x).to_string()).collect()
}
#[test]
fn empty_fields_accept_any_host() {
let sup = SupportedArchitectures::default();
assert!(is_supported(&[], &[], &[], &sup));
}
#[test]
fn positive_match_rules() {
assert!(field_matches(&s(&["linux", "darwin"]), "linux"));
assert!(!field_matches(&s(&["linux", "darwin"]), "win32"));
}
#[test]
fn negation_rejects_match() {
assert!(!field_matches(&s(&["!win32"]), "win32"));
assert!(field_matches(&s(&["!win32"]), "linux"));
}
#[test]
fn mixed_negation_and_positive() {
assert!(!field_matches(&s(&["linux", "!linux"]), "linux"));
}
#[test]
fn supported_architectures_widens_with_current() {
let sup = SupportedArchitectures {
os: s(&["current", "linux"]),
..Default::default()
};
assert!(is_supported(&s(&["linux"]), &[], &[], &sup));
}
#[test]
fn aube_lock_default_accepts_every_common_native() {
let sup = SupportedArchitectures::aube_lock_default();
assert!(is_supported(&s(&["darwin"]), &s(&["arm64"]), &[], &sup));
assert!(is_supported(
&s(&["linux"]),
&s(&["x64"]),
&s(&["glibc"]),
&sup
));
assert!(is_supported(
&s(&["linux"]),
&s(&["arm64"]),
&s(&["musl"]),
&sup
));
assert!(is_supported(&s(&["win32"]), &s(&["x64"]), &[], &sup));
assert!(is_supported(&s(&["win32"]), &s(&["arm64"]), &[], &sup));
}
#[test]
fn aube_lock_default_always_accepts_host_triple() {
let sup = SupportedArchitectures::aube_lock_default();
let (os, cpu, libc) = host_triple();
let pkg_libc = if libc.is_empty() { vec![] } else { s(&[libc]) };
assert!(is_supported(&s(&[os]), &s(&[cpu]), &pkg_libc, &sup));
}
#[test]
fn aube_lock_default_rejects_exotic_non_host_triples() {
let sup = SupportedArchitectures::aube_lock_default();
let (os, _, _) = host_triple();
if os != "openbsd" {
assert!(!is_supported(&s(&["openbsd"]), &s(&["x64"]), &[], &sup));
}
if os != "aix" {
assert!(!is_supported(&s(&["aix"]), &s(&["ppc64"]), &[], &sup));
}
}
#[test]
fn filter_graph_prunes_transitive_optional_platform_mismatches() {
let supported = SupportedArchitectures {
os: s(&["darwin"]),
cpu: s(&["arm64"]),
..Default::default()
};
let mut graph = aube_lockfile::LockfileGraph::default();
graph.importers.insert(
".".to_string(),
vec![aube_lockfile::DirectDep {
name: "host".to_string(),
dep_path: "host@1.0.0".to_string(),
dep_type: aube_lockfile::DepType::Production,
specifier: Some("1.0.0".to_string()),
}],
);
graph.packages.insert(
"host@1.0.0".to_string(),
aube_lockfile::LockedPackage {
name: "host".to_string(),
version: "1.0.0".to_string(),
dep_path: "host@1.0.0".to_string(),
dependencies: [
("native-darwin".to_string(), "1.0.0".to_string()),
("native-linux".to_string(), "1.0.0".to_string()),
]
.into(),
optional_dependencies: [
("native-darwin".to_string(), "1.0.0".to_string()),
("native-linux".to_string(), "1.0.0".to_string()),
]
.into(),
..Default::default()
},
);
graph.packages.insert(
"native-darwin@1.0.0".to_string(),
aube_lockfile::LockedPackage {
name: "native-darwin".to_string(),
version: "1.0.0".to_string(),
dep_path: "native-darwin@1.0.0".to_string(),
os: s(&["darwin"]).into(),
cpu: s(&["arm64"]).into(),
..Default::default()
},
);
graph.packages.insert(
"native-linux@1.0.0".to_string(),
aube_lockfile::LockedPackage {
name: "native-linux".to_string(),
version: "1.0.0".to_string(),
dep_path: "native-linux@1.0.0".to_string(),
os: s(&["linux"]).into(),
cpu: s(&["x64"]).into(),
..Default::default()
},
);
filter_graph(&mut graph, &supported, &Default::default());
let host = graph.packages.get("host@1.0.0").unwrap();
assert!(host.dependencies.contains_key("native-darwin"));
assert!(!host.dependencies.contains_key("native-linux"));
assert!(graph.packages.contains_key("native-darwin@1.0.0"));
assert!(!graph.packages.contains_key("native-linux@1.0.0"));
}
#[test]
fn filter_graph_prunes_npm_lockfile_transitive_optional_platform_mismatch() {
let content = r#"{
"name": "platform-optional-root",
"version": "1.0.0",
"lockfileVersion": 3,
"packages": {
"": {
"name": "platform-optional-root",
"version": "1.0.0",
"dependencies": { "host": "file:host" }
},
"node_modules/host": {
"resolved": "host",
"link": true
},
"host": {
"name": "host",
"version": "1.0.0",
"optionalDependencies": { "native-win": "1.0.0" }
},
"node_modules/native-win": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/native-win/-/native-win-1.0.0.tgz",
"integrity": "sha512-native",
"optional": true,
"os": ["win32"],
"cpu": ["x64"],
"libc": ["glibc"]
}
}
}"#;
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), content).unwrap();
let mut graph = aube_lockfile::npm::parse(tmp.path()).unwrap();
let host_dep_path = graph.importers["."][0].dep_path.clone();
assert!(
graph.packages.contains_key(&host_dep_path),
"fixture must contain the host package before filtering"
);
assert!(
graph.packages.contains_key("native-win@1.0.0"),
"fixture must contain native-win before filtering"
);
let host = &graph.packages[&host_dep_path];
assert!(host.dependencies.contains_key("native-win"));
assert!(host.optional_dependencies.contains_key("native-win"));
let supported = SupportedArchitectures {
os: s(&["linux"]),
cpu: s(&["x64"]),
libc: s(&["glibc"]),
..Default::default()
};
filter_graph(&mut graph, &supported, &Default::default());
assert!(graph.packages.contains_key(&host_dep_path));
assert!(!graph.packages.contains_key("native-win@1.0.0"));
let host = &graph.packages[&host_dep_path];
assert!(!host.dependencies.contains_key("native-win"));
assert!(!host.optional_dependencies.contains_key("native-win"));
}
#[cfg(not(target_os = "linux"))]
#[test]
fn libc_ignored_off_linux() {
let sup = SupportedArchitectures::default();
assert!(is_supported(&[], &[], &s(&["musl"]), &sup));
}
#[cfg(target_os = "linux")]
#[test]
fn linux_glibc_host_rejects_musl_only_package() {
if cfg!(target_env = "musl") {
return;
}
let sup = SupportedArchitectures::default();
assert!(!is_supported(&[], &[], &s(&["musl"]), &sup));
}
}