use crate::core::config::{Language, ResolvedCrateConfig};
use crate::core::ir::ApiSurface;
use anyhow::Context as _;
use std::collections::BTreeSet;
use std::path::Path;
pub fn collect_cfg_feature_names(cfg_str: &str, out: &mut BTreeSet<String>) {
let normalized = cfg_str.trim().replace(" (", "(");
let cfg_str = normalized.as_str();
if let Some(feature) = cfg_str.strip_prefix("feature = \"").and_then(|s| s.strip_suffix('"')) {
out.insert(feature.to_string());
return;
}
if let Some(inner) = cfg_str
.strip_prefix("any(")
.and_then(|s| s.strip_suffix(')'))
.or_else(|| cfg_str.strip_prefix("all(").and_then(|s| s.strip_suffix(')')))
{
for cond in parse_cfg_list(inner) {
collect_cfg_feature_names(&cond, out);
}
return;
}
if let Some(inner) = cfg_str.strip_prefix("not(").and_then(|s| s.strip_suffix(')')) {
collect_cfg_feature_names(inner.trim(), out);
}
}
pub fn collect_cfg_features(api: &ApiSurface) -> BTreeSet<String> {
let mut out = BTreeSet::new();
let host_crate = api.crate_name.replace('-', "_");
let is_host = |rust_path: &str| -> bool {
if host_crate.is_empty() {
return true;
}
match rust_path.split("::").next() {
Some(first) if !first.is_empty() => first == host_crate,
_ => true,
}
};
for typ in &api.types {
if !is_host(&typ.rust_path) {
continue;
}
if let Some(cfg) = &typ.cfg {
collect_cfg_feature_names(cfg, &mut out);
}
for field in &typ.fields {
if let Some(cfg) = &field.cfg {
collect_cfg_feature_names(cfg, &mut out);
}
}
for method in &typ.methods {
if let Some(cfg) = &method.cfg {
collect_cfg_feature_names(cfg, &mut out);
}
}
}
for enum_def in &api.enums {
if !is_host(&enum_def.rust_path) {
continue;
}
if let Some(cfg) = &enum_def.cfg {
collect_cfg_feature_names(cfg, &mut out);
}
for variant in &enum_def.variants {
if let Some(cfg) = &variant.cfg {
collect_cfg_feature_names(cfg, &mut out);
}
}
for method in &enum_def.methods {
if let Some(cfg) = &method.cfg {
collect_cfg_feature_names(cfg, &mut out);
}
}
}
for func in &api.functions {
if let Some(cfg) = &func.cfg {
collect_cfg_feature_names(cfg, &mut out);
}
}
for service in &api.services {
if !is_host(&service.rust_path) {
continue;
}
if let Some(cfg) = &service.cfg {
collect_cfg_feature_names(cfg, &mut out);
}
if let Some(cfg) = &service.constructor.cfg {
collect_cfg_feature_names(cfg, &mut out);
}
for configurator in &service.configurators {
if let Some(cfg) = &configurator.cfg {
collect_cfg_feature_names(cfg, &mut out);
}
}
}
out
}
#[must_use]
pub fn undeclared_cfg_features(api: &ApiSurface, present: &BTreeSet<String>) -> BTreeSet<String> {
collect_cfg_features(api).difference(present).cloned().collect()
}
fn features_reachable_from_default(members_of: impl Fn(&str) -> Vec<String>) -> BTreeSet<String> {
let mut enabled = BTreeSet::new();
let mut visited = BTreeSet::new();
let mut queue = std::collections::VecDeque::from([String::from("default")]);
while let Some(name) = queue.pop_front() {
if !visited.insert(name.clone()) {
continue;
}
if name != "default" {
enabled.insert(name.clone());
}
queue.extend(members_of(&name));
}
enabled
}
#[must_use]
pub fn read_declared_cargo_features(manifest_path: &Path) -> Option<BTreeSet<String>> {
let content = std::fs::read_to_string(manifest_path).ok()?;
let manifest = toml::from_str::<toml::Value>(&content).ok()?;
let features = manifest
.get("features")
.and_then(toml::Value::as_table)
.map(|table| table.keys().cloned().collect())
.unwrap_or_default();
Some(features)
}
#[must_use]
pub fn read_default_enabled_cargo_features(manifest_path: &Path) -> Option<BTreeSet<String>> {
let content = std::fs::read_to_string(manifest_path).ok()?;
let manifest = toml::from_str::<toml::Value>(&content).ok()?;
let features_table = manifest.get("features").and_then(toml::Value::as_table);
Some(features_reachable_from_default(|name| {
features_table
.and_then(|table| table.get(name))
.and_then(toml::Value::as_array)
.into_iter()
.flatten()
.filter_map(toml::Value::as_str)
.filter(|member| !member.contains('/'))
.map(str::to_owned)
.collect()
}))
}
#[must_use]
pub fn core_crate_declared_features(config: &ResolvedCrateConfig) -> BTreeSet<String> {
let Some(manifest_path) = crate::scaffold::core_crate_manifest_path(config) else {
return BTreeSet::new();
};
read_declared_cargo_features(&manifest_path).unwrap_or_default()
}
pub fn merge_missing_cfg_features(
existing: &str,
api: &ApiSurface,
core_crate_name: &str,
core_declared_features: &BTreeSet<String>,
) -> anyhow::Result<Option<String>> {
let mut doc = existing
.parse::<toml_edit::DocumentMut>()
.context("existing manifest is not valid TOML")?;
let features_table_ref = doc.get("features").and_then(toml_edit::Item::as_table);
let declared: BTreeSet<String> = features_table_ref
.map(|table| table.iter().map(|(key, _)| key.to_string()).collect())
.unwrap_or_default();
let enabled_by_default = features_reachable_from_default(|name| {
features_table_ref
.and_then(|table| table.get(name))
.and_then(toml_edit::Item::as_array)
.into_iter()
.flat_map(toml_edit::Array::iter)
.filter_map(toml_edit::Value::as_str)
.filter(|member| !member.contains('/'))
.map(str::to_owned)
.collect()
});
let referenced: BTreeSet<String> = collect_cfg_features(api)
.into_iter()
.filter(|feature| core_declared_features.contains(feature))
.collect();
let needs_declaration: BTreeSet<String> = referenced.difference(&declared).cloned().collect();
let needs_default: BTreeSet<String> = referenced.difference(&enabled_by_default).cloned().collect();
if needs_declaration.is_empty() && needs_default.is_empty() {
return Ok(None);
}
let features_table = doc
.entry("features")
.or_insert_with(|| toml_edit::Item::Table(toml_edit::Table::new()))
.as_table_mut()
.context("[features] exists in the manifest but is not a table")?;
for feature in &needs_declaration {
let mut forwarded = toml_edit::Array::new();
forwarded.push(format!("{core_crate_name}/{feature}"));
features_table.insert(feature, toml_edit::Item::Value(toml_edit::Value::Array(forwarded)));
}
if !needs_default.is_empty() {
let default_array = features_table
.entry("default")
.or_insert_with(|| toml_edit::Item::Value(toml_edit::Value::Array(toml_edit::Array::new())))
.as_array_mut()
.context("features.default exists but is not an array")?;
let already_listed: BTreeSet<String> = default_array
.iter()
.filter_map(toml_edit::Value::as_str)
.map(str::to_owned)
.collect();
for feature in &needs_default {
if !already_listed.contains(feature) {
default_array.push(feature.clone());
}
}
}
Ok(Some(doc.to_string()))
}
#[must_use]
pub fn resolve_against_workspace_root(config: &ResolvedCrateConfig, relative: &Path) -> std::path::PathBuf {
let root = config
.workspace_root
.clone()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
root.join(relative)
}
pub fn warn_on_undeclared_binding_cfg_features(api: &ApiSurface, language: Language, manifest_path: &Path) {
let Some(enabled_by_default) = read_default_enabled_cargo_features(manifest_path) else {
return;
};
let missing = undeclared_cfg_features(api, &enabled_by_default);
if missing.is_empty() {
return;
}
tracing::warn!(
language = %language,
manifest = %manifest_path.display(),
missing_features = ?missing,
"generated bindings reference #[cfg(feature = \"...\")] gates this crate's own Cargo.toml \
does not enable by default; the affected definitions (and their registrations) will \
silently compile out of this binding even though the core crate has the feature on -- \
re-run `alef scaffold` to add the missing features to this crate's [features] table and \
its default list"
);
}
pub fn warn_on_ffi_feature_drift(config: &ResolvedCrateConfig, lang: Language) {
if lang == Language::Ffi {
return;
}
let lang_features: BTreeSet<&str> = config.features_for_language(lang).iter().map(String::as_str).collect();
let ffi_features: BTreeSet<&str> = config
.features_for_language(Language::Ffi)
.iter()
.map(String::as_str)
.collect();
if lang_features != ffi_features {
tracing::warn!(
language = %lang,
lang_features = ?lang_features,
ffi_features = ?ffi_features,
"configured feature set for this binding differs from [crates.ffi]'s; cfg-gated FFI \
exports are included/omitted based on this binding's own feature list, but the \
linked native library is built once using the FFI crate's feature list — keep them \
in sync (or set them explicitly to the same value) or generated glue may reference \
symbols the shipped library doesn't export"
);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CfgPredicate {
Feature(String),
All(Vec<CfgPredicate>),
Any(Vec<CfgPredicate>),
Not(Box<CfgPredicate>),
Other,
}
pub fn parse_cfg_predicate(cfg_str: &str) -> CfgPredicate {
let normalized = cfg_str.trim().replace(" (", "(");
let cfg_str = normalized.as_str();
if let Some(feature) = cfg_str.strip_prefix("feature = \"").and_then(|s| s.strip_suffix('"')) {
return CfgPredicate::Feature(feature.to_string());
}
if let Some(inner) = cfg_str.strip_prefix("any(").and_then(|s| s.strip_suffix(')')) {
return CfgPredicate::Any(parse_cfg_list(inner).iter().map(|c| parse_cfg_predicate(c)).collect());
}
if let Some(inner) = cfg_str.strip_prefix("all(").and_then(|s| s.strip_suffix(')')) {
return CfgPredicate::All(parse_cfg_list(inner).iter().map(|c| parse_cfg_predicate(c)).collect());
}
if let Some(inner) = cfg_str.strip_prefix("not(").and_then(|s| s.strip_suffix(')')) {
return CfgPredicate::Not(Box::new(parse_cfg_predicate(inner.trim())));
}
CfgPredicate::Other
}
#[must_use]
pub fn combine_gates(owner_cfg: &str, member_cfg: &str) -> String {
let (owner, member) = (owner_cfg.trim(), member_cfg.trim());
if predicate_implies(&parse_cfg_predicate(member), &parse_cfg_predicate(owner)) {
return member.to_string();
}
format!("all({owner}, {member})")
}
fn predicate_implies(predicate: &CfgPredicate, required: &CfgPredicate) -> bool {
if matches!(required, CfgPredicate::Other) {
return false;
}
if predicate == required {
return true;
}
match predicate {
CfgPredicate::All(arms) => arms.iter().any(|arm| predicate_implies(arm, required)),
_ => false,
}
}
fn parse_cfg_list(s: &str) -> Vec<String> {
let mut result = Vec::new();
let mut depth = 0usize;
let mut current = String::new();
for ch in s.chars() {
match ch {
'(' => {
depth += 1;
current.push(ch);
}
')' => {
depth = depth.saturating_sub(1);
current.push(ch);
}
',' if depth == 0 => {
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
result.push(trimmed);
}
current.clear();
}
_ => current.push(ch),
}
}
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
result.push(trimmed);
}
result
}
#[cfg(test)]
mod tests;