#[derive(Debug, Clone, Default)]
pub struct SupportedArchitectures {
pub os: Vec<String>,
pub cpu: Vec<String>,
pub libc: Vec<String>,
}
impl SupportedArchitectures {
fn combinations(&self) -> Vec<(String, String, String)> {
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 cfg!(target_os = "linux") {
if cfg!(target_env = "musl") {
"musl"
} else {
"glibc"
}
} else {
""
};
(os, cpu, libc)
}
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 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: std::collections::HashSet<String> = graph.packages.keys().cloned().collect();
let mismatched_packages: std::collections::HashSet<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: std::collections::HashSet<String> = std::collections::HashSet::new();
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 filter_graph_prunes_transitive_optional_platform_mismatches() {
let supported = SupportedArchitectures {
os: s(&["darwin"]),
cpu: s(&["arm64"]),
libc: vec![],
};
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"]),
cpu: s(&["arm64"]),
..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"]),
cpu: s(&["x64"]),
..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"));
}
#[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));
}
}