#[derive(Debug, Clone, Default)]
pub struct SupportedArchitectures {
pub os: Vec<String>,
pub cpu: Vec<String>,
pub libc: Vec<String>,
pub accept_all: bool,
}
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 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 {
if supported.accept_all {
return true;
}
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_is_mismatched =
match aube_lockfile::resolve_dep_edge(name, tail, |k| package_keys.contains(k)) {
Some(child_key) => mismatched_packages.contains(&child_key),
None => false,
};
let keep = !ignored.contains(name) && !child_is_mismatched;
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 let Some(child) =
aube_lockfile::resolve_dep_edge(name, tail, |k| graph.packages.contains_key(k))
{
stack.push(child);
}
}
}
}
graph.packages.retain(|k, _| reachable.contains(k));
}
pub fn mark_optional_packages(graph: &mut aube_lockfile::LockfileGraph) {
use crate::FxHashSet;
use aube_lockfile::DepType;
let mut required: FxHashSet<String> = FxHashSet::default();
let mut stack: Vec<String> = Vec::new();
for deps in graph.importers.values() {
for dep in deps {
if dep.dep_type != DepType::Optional {
stack.push(dep.dep_path.clone());
}
}
}
while let Some(dep_path) = stack.pop() {
if !required.insert(dep_path.clone()) {
continue;
}
let Some(pkg) = graph.packages.get(&dep_path) else {
continue;
};
for (name, tail) in &pkg.dependencies {
if pkg.optional_dependencies.contains_key(name) {
continue;
}
if let Some(child) =
aube_lockfile::resolve_dep_edge(name, tail, |k| graph.packages.contains_key(k))
{
stack.push(child);
}
}
}
for (dep_path, pkg) in graph.packages.iter_mut() {
pkg.optional = !required.contains(dep_path);
}
}
pub fn mark_transitive_peer_dependencies(graph: &mut aube_lockfile::LockfileGraph) {
use crate::{FxHashMap, FxHashSet};
use std::collections::BTreeSet;
let mut parents: FxHashMap<String, Vec<String>> = FxHashMap::default();
let mut unresolved: FxHashMap<String, Vec<String>> = FxHashMap::default();
for (dep_path, pkg) in &graph.packages {
for (name, tail) in pkg
.dependencies
.iter()
.chain(pkg.optional_dependencies.iter())
{
if pkg.peer_dependencies.contains_key(name)
|| pkg.peer_dependencies_meta.contains_key(name)
{
continue;
}
if let Some(child) =
aube_lockfile::resolve_dep_edge(name, tail, |k| graph.packages.contains_key(k))
{
parents.entry(child).or_default().push(dep_path.clone());
} else {
tracing::debug!(
parent = %dep_path,
dep = %name,
tail = %tail,
"transitive-peer pass: dependency edge has no graph node, skipping"
);
}
}
let own: BTreeSet<String> = pkg
.peer_dependencies_with_meta_defaults()
.into_keys()
.filter(|p| !pkg.dependencies.contains_key(p))
.collect();
if !own.is_empty() {
unresolved.insert(dep_path.clone(), own.into_iter().collect());
}
}
let mut acc: FxHashMap<String, BTreeSet<String>> = FxHashMap::default();
for (origin, peers) in &unresolved {
let mut visited: FxHashSet<String> = FxHashSet::default();
visited.insert(origin.clone());
let mut stack: Vec<String> = parents.get(origin).cloned().unwrap_or_default();
while let Some(node) = stack.pop() {
if !visited.insert(node.clone()) {
continue;
}
let entry = acc.entry(node.clone()).or_default();
entry.extend(peers.iter().cloned());
if let Some(ps) = parents.get(&node) {
stack.extend(ps.iter().cloned());
}
}
}
for (dep_path, pkg) in graph.packages.iter_mut() {
pkg.transitive_peer_dependencies = acc
.get(dep_path)
.map(|s| s.iter().cloned().collect())
.unwrap_or_default();
}
}
#[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 accept_all_accepts_every_arch_including_non_host_triples() {
let sup = SupportedArchitectures {
accept_all: true,
..Default::default()
};
assert!(is_supported(&s(&["darwin"]), &s(&["x64"]), &[], &sup));
assert!(is_supported(&s(&["freebsd"]), &s(&["arm64"]), &[], &sup));
assert!(is_supported(
&s(&["linux"]),
&s(&["ppc64"]),
&s(&["glibc"]),
&sup
));
assert!(is_supported(
&s(&["openharmony"]),
&s(&["arm64"]),
&[],
&sup
));
assert!(is_supported(&s(&["win32"]), &s(&["ia32"]), &[], &sup));
let host_only = SupportedArchitectures::default();
let (host_os, _, _) = host_triple();
if host_os != "freebsd" {
assert!(!is_supported(
&s(&["freebsd"]),
&s(&["arm64"]),
&[],
&host_only
));
}
}
#[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"));
}
fn dep(name: &str, dep_type: aube_lockfile::DepType) -> aube_lockfile::DirectDep {
aube_lockfile::DirectDep {
name: name.to_string(),
dep_path: format!("{name}@1.0.0"),
dep_type,
specifier: Some("1.0.0".to_string()),
}
}
fn pkg(name: &str, deps: &[&str], opt_deps: &[&str]) -> (String, aube_lockfile::LockedPackage) {
let dep_path = format!("{name}@1.0.0");
(
dep_path.clone(),
aube_lockfile::LockedPackage {
name: name.to_string(),
version: "1.0.0".to_string(),
dep_path,
dependencies: deps
.iter()
.map(|d| ((*d).to_string(), "1.0.0".to_string()))
.collect(),
optional_dependencies: opt_deps
.iter()
.map(|d| ((*d).to_string(), "1.0.0".to_string()))
.collect(),
..Default::default()
},
)
}
#[test]
fn mark_optional_packages_marks_optional_only_reachable() {
use aube_lockfile::DepType;
let mut graph = aube_lockfile::LockfileGraph::default();
graph.importers.insert(
".".to_string(),
vec![
dep("host", DepType::Production),
dep("also-required", DepType::Production),
dep("opt-root", DepType::Optional),
],
);
graph.packages.extend([
pkg(
"host",
&["shared", "native-darwin", "native-linux", "dual"],
&["native-darwin", "native-linux", "dual"],
),
pkg("also-required", &["dual"], &[]),
pkg("shared", &[], &[]),
pkg("native-darwin", &[], &[]),
pkg("native-linux", &[], &[]),
pkg("dual", &[], &[]),
pkg("opt-root", &[], &[]),
]);
mark_optional_packages(&mut graph);
let is_opt = |k: &str| graph.packages[k].optional;
assert!(!is_opt("host@1.0.0"));
assert!(!is_opt("also-required@1.0.0"));
assert!(!is_opt("shared@1.0.0"));
assert!(!is_opt("dual@1.0.0"));
assert!(is_opt("native-darwin@1.0.0"));
assert!(is_opt("native-linux@1.0.0"));
assert!(is_opt("opt-root@1.0.0"));
}
fn pkg_with_peers(
name: &str,
deps: &[&str],
peers: &[&str],
) -> (String, aube_lockfile::LockedPackage) {
let (key, mut p) = pkg(name, deps, &[]);
p.peer_dependencies = peers
.iter()
.map(|d| ((*d).to_string(), "*".to_string()))
.collect();
(key, p)
}
#[test]
fn transitive_peer_dependencies_bubble_unresolved_peers() {
let mut graph = aube_lockfile::LockfileGraph::default();
graph.packages.extend([
pkg("app", &["host", "mid"], &[]),
pkg_with_peers("host", &["core"], &["core"]),
pkg("core", &[], &[]),
pkg("mid", &["leaf"], &[]),
pkg_with_peers("leaf", &["ms"], &["supports-color"]),
pkg("ms", &[], &[]),
]);
mark_transitive_peer_dependencies(&mut graph);
let tp = |k: &str| graph.packages[k].transitive_peer_dependencies.clone();
assert_eq!(tp("app@1.0.0"), vec!["supports-color".to_string()]);
assert_eq!(tp("mid@1.0.0"), vec!["supports-color".to_string()]);
assert!(tp("leaf@1.0.0").is_empty());
assert!(tp("ms@1.0.0").is_empty());
assert!(tp("host@1.0.0").is_empty());
assert!(tp("core@1.0.0").is_empty());
}
#[test]
fn transitive_peer_dependencies_handle_cycles_without_self() {
let mut graph = aube_lockfile::LockfileGraph::default();
graph.packages.extend([
pkg_with_peers("a", &["b"], &["pa"]),
pkg_with_peers("b", &["a"], &["pb"]),
]);
mark_transitive_peer_dependencies(&mut graph);
assert_eq!(
graph.packages["a@1.0.0"].transitive_peer_dependencies,
vec!["pb".to_string()]
);
assert_eq!(
graph.packages["b@1.0.0"].transitive_peer_dependencies,
vec!["pa".to_string()]
);
}
#[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));
}
}