use crate::build_spec::BuildSpec;
use crate::platform_features::{lookup, PlatformTag};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "rule", rename_all = "kebab-case")]
pub enum Diagnostic {
PlatformFeatureLeakAcrossTargets {
crate_key: String,
name: String,
feature: String,
feature_platform: PlatformTag,
triples: Vec<String>,
upstream_fix_hint: String,
},
}
#[must_use]
pub fn diagnose(spec: &BuildSpec) -> Vec<Diagnostic> {
let mut out = Vec::new();
diagnose_platform_feature_leaks(spec, &mut out);
out
}
fn diagnose_platform_feature_leaks(spec: &BuildSpec, out: &mut Vec<Diagnostic>) {
let Some(compact) = spec.target_resolves.as_ref() else {
return;
};
let target_resolves = compact.expand();
let mut grouped: indexmap::IndexMap<(String, String, String, PlatformTag), Vec<String>> =
indexmap::IndexMap::new();
for (triple, resolve) in &target_resolves {
for (key, edges) in &resolve.crates {
let Some(crate_spec) = spec.crates.get(key) else {
continue;
};
for feature in &edges.features {
let Some(entry) = lookup(&crate_spec.name, feature) else {
continue;
};
if entry.tag.matches_triple(triple) {
continue;
}
grouped
.entry((
key.clone(),
crate_spec.name.clone(),
feature.clone(),
entry.tag,
))
.or_default()
.push(triple.clone());
}
}
}
for ((crate_key, name, feature, feature_platform), mut triples) in grouped {
triples.sort();
triples.dedup();
let upstream_fix_hint = upstream_fix_hint_for(&name, &feature);
out.push(Diagnostic::PlatformFeatureLeakAcrossTargets {
crate_key,
name,
feature,
feature_platform,
triples,
upstream_fix_hint,
});
}
}
fn upstream_fix_hint_for(crate_name: &str, _feature: &str) -> String {
match crate_name {
"notify" => "In the consumer's Cargo.toml, set the bare notify dep with no \
macOS feature flags, then opt macOS into a backend via a \
target-conditional block:\n \
[dependencies]\n \
notify = { version = \"<v>\", default-features = false }\n\n \
[target.'cfg(target_os = \"macos\")'.dependencies]\n \
notify = { version = \"<v>\", default-features = false, \
features = [\"macos_kqueue\"] }\n\n \
Linux falls back to inotify (notify's only Linux backend) \
with no macOS-side dep activation. The target-gate is required \
because Cargo features unify globally per-target — putting \
`features = [\"macos_kqueue\"]` in [dependencies] would still \
activate kqueue → kqueue-sys on every Linux build."
.to_string(),
_ => format!(
"In the consumer's Cargo.toml, set `default-features = false` on \
{crate_name}, then re-add only the cross-platform features the \
consumer actually uses. Platform-specific features (apple-only / \
linux-only / windows-only) must live in a \
`[target.'cfg(target_os = \"X\")'.dependencies]` block — putting \
them in plain [dependencies] activates them globally per-target \
due to Cargo's feature unification, which is the leak this \
diagnostic detected."
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::build_spec::{
BuildRustCrateArgs, CompactTargetResolves, CrateSource, CrateSpec, CrateTargetEdges,
TargetResolve, WorkspaceSpec,
};
use indexmap::IndexMap;
use std::collections::BTreeMap;
fn empty_spec() -> BuildSpec {
BuildSpec {
version: crate::build_spec::SCHEMA_VERSION,
workspace: WorkspaceSpec {
root: "/x".into(),
members: vec![],
},
crates: BTreeMap::new(),
root_crate: String::new(),
workspace_members: vec![],
flake_metadata: BTreeMap::new(),
target_resolves: None,
cargo_lock_sha256: None,
}
}
fn registry_crate(name: &str, version: &str, features: Vec<String>) -> (String, CrateSpec) {
let key = format!("{name}-{version}");
let args = BuildRustCrateArgs {
crate_name: Some(name.into()),
version: Some(version.into()),
edition: Some("2024".into()),
features: features.clone(),
crate_renames: BTreeMap::new(),
release: Some(true),
proc_macro: None,
build: None,
links: None,
lib_name: None,
lib_path: None,
pre_build: Some(format!("export CARGO_CRATE_NAME={};", name.replace('-', "_"))),
};
(
key,
CrateSpec {
name: name.into(),
version: version.into(),
edition: "2024".into(),
source: CrateSource::Registry {
url: "https://crates.io/x".into(),
sha256: "deadbeef".into(),
name_with_ext: format!("{name}.tar.gz"),
},
features,
proc_macro: false,
build_script: None,
links: None,
quirks: vec![],
build_rust_crate_args: args,
binaries: vec![],
lib_target: None,
dependencies: vec![],
runtime_dependencies: vec![],
build_dependencies: vec![],
crate_renames: BTreeMap::new(),
},
)
}
fn target_resolve_for(key: &str, features: Vec<String>) -> TargetResolve {
let mut crates = BTreeMap::new();
crates.insert(
key.to_string(),
CrateTargetEdges {
dependencies: vec![],
runtime_dependencies: vec![],
build_dependencies: vec![],
features,
},
);
TargetResolve { crates }
}
#[test]
fn empty_spec_yields_no_diagnostics() {
let s = empty_spec();
assert!(diagnose(&s).is_empty());
}
#[test]
fn old_shape_spec_without_target_resolves_is_skipped() {
let mut s = empty_spec();
let (k, c) = registry_crate("notify", "8.2.0", vec!["macos_fsevent".into()]);
s.crates.insert(k, c);
assert!(diagnose(&s).is_empty());
}
#[test]
fn notify_macos_fsevent_on_linux_musl_triggers_diagnostic() {
let mut s = empty_spec();
let (k, c) = registry_crate(
"notify",
"8.2.0",
vec!["default".into(), "macos_fsevent".into()],
);
s.crates.insert(k.clone(), c);
let mut tr = IndexMap::new();
tr.insert(
"x86_64-unknown-linux-musl".to_string(),
target_resolve_for(&k, vec!["default".into(), "macos_fsevent".into()]),
);
tr.insert(
"aarch64-apple-darwin".to_string(),
target_resolve_for(&k, vec!["default".into(), "macos_fsevent".into()]),
);
s.target_resolves = Some(CompactTargetResolves::from_full(tr));
let d = diagnose(&s);
assert_eq!(d.len(), 1, "expected exactly one leak diagnostic");
match &d[0] {
Diagnostic::PlatformFeatureLeakAcrossTargets {
name,
feature,
feature_platform,
triples,
..
} => {
assert_eq!(name, "notify");
assert_eq!(feature, "macos_fsevent");
assert_eq!(*feature_platform, PlatformTag::Apple);
assert_eq!(triples, &vec!["x86_64-unknown-linux-musl".to_string()]);
}
}
}
#[test]
fn apple_targets_alone_yield_no_diagnostic() {
let mut s = empty_spec();
let (k, c) = registry_crate("notify", "8.2.0", vec!["macos_fsevent".into()]);
s.crates.insert(k.clone(), c);
let mut tr = IndexMap::new();
tr.insert(
"aarch64-apple-darwin".to_string(),
target_resolve_for(&k, vec!["macos_fsevent".into()]),
);
tr.insert(
"x86_64-apple-darwin".to_string(),
target_resolve_for(&k, vec!["macos_fsevent".into()]),
);
s.target_resolves = Some(CompactTargetResolves::from_full(tr));
assert!(diagnose(&s).is_empty());
}
#[test]
fn linux_only_with_macos_kqueue_fires_leak_diagnostic() {
let mut s = empty_spec();
let (k, c) = registry_crate("notify", "8.2.0", vec!["macos_kqueue".into()]);
s.crates.insert(k.clone(), c);
let mut tr = IndexMap::new();
tr.insert(
"x86_64-unknown-linux-musl".to_string(),
target_resolve_for(&k, vec!["macos_kqueue".into()]),
);
s.target_resolves = Some(CompactTargetResolves::from_full(tr));
let d = diagnose(&s);
assert_eq!(d.len(), 1, "macos_kqueue on linux must flag");
match &d[0] {
Diagnostic::PlatformFeatureLeakAcrossTargets {
name,
feature,
feature_platform,
triples,
..
} => {
assert_eq!(name, "notify");
assert_eq!(feature, "macos_kqueue");
assert_eq!(*feature_platform, PlatformTag::Apple);
assert_eq!(triples, &vec!["x86_64-unknown-linux-musl".to_string()]);
}
}
}
#[test]
fn linux_only_with_unregistered_feature_yields_no_diagnostic() {
let mut s = empty_spec();
let (k, c) = registry_crate("notify", "8.2.0", vec!["unregistered-feature".into()]);
s.crates.insert(k.clone(), c);
let mut tr = IndexMap::new();
tr.insert(
"x86_64-unknown-linux-musl".to_string(),
target_resolve_for(&k, vec!["unregistered-feature".into()]),
);
s.target_resolves = Some(CompactTargetResolves::from_full(tr));
assert!(diagnose(&s).is_empty());
}
#[test]
fn leak_collects_every_offending_triple() {
let mut s = empty_spec();
let (k, c) = registry_crate("notify", "8.2.0", vec!["macos_fsevent".into()]);
s.crates.insert(k.clone(), c);
let mut tr = IndexMap::new();
tr.insert(
"x86_64-unknown-linux-musl".to_string(),
target_resolve_for(&k, vec!["macos_fsevent".into()]),
);
tr.insert(
"aarch64-unknown-linux-gnu".to_string(),
target_resolve_for(&k, vec!["macos_fsevent".into()]),
);
tr.insert(
"aarch64-apple-darwin".to_string(),
target_resolve_for(&k, vec!["macos_fsevent".into()]),
);
s.target_resolves = Some(CompactTargetResolves::from_full(tr));
let d = diagnose(&s);
assert_eq!(d.len(), 1);
match &d[0] {
Diagnostic::PlatformFeatureLeakAcrossTargets { triples, .. } => {
assert_eq!(
triples,
&vec![
"aarch64-unknown-linux-gnu".to_string(),
"x86_64-unknown-linux-musl".to_string(),
]
);
}
}
}
#[test]
fn fix_hint_for_notify_recommends_target_gated_macos_kqueue() {
let mut s = empty_spec();
let (k, c) = registry_crate("notify", "8.2.0", vec!["macos_fsevent".into()]);
s.crates.insert(k.clone(), c);
let mut tr = IndexMap::new();
tr.insert(
"x86_64-unknown-linux-musl".to_string(),
target_resolve_for(&k, vec!["macos_fsevent".into()]),
);
s.target_resolves = Some(CompactTargetResolves::from_full(tr));
let d = diagnose(&s);
match &d[0] {
Diagnostic::PlatformFeatureLeakAcrossTargets {
upstream_fix_hint, ..
} => {
assert!(
upstream_fix_hint.contains("default-features = false"),
"hint must name `default-features = false`, got: {upstream_fix_hint}"
);
assert!(
upstream_fix_hint.contains("target_os = \"macos\""),
"hint must steer to the cfg(target_os = \"macos\") target-gate, got: {upstream_fix_hint}"
);
assert!(
upstream_fix_hint.contains("macos_kqueue"),
"hint must name a concrete macOS backend (macos_kqueue), got: {upstream_fix_hint}"
);
assert!(
upstream_fix_hint.contains("inotify"),
"hint must explain Linux falls back to inotify, got: {upstream_fix_hint}"
);
}
}
}
}