use crate::core::backend::GeneratedFile;
use crate::core::config::{Language, ResolvedCrateConfig, ScaffoldCargo, ScaffoldCargoEnvValue};
use crate::core::ir::ApiSurface;
use anyhow::Context as _;
mod languages;
pub(crate) mod naming;
mod template_env;
pub(crate) mod version_floor;
pub use languages::{
PUBLISHED_RUNTIME_IDENTIFIERS, render_csharp_csproj, render_csharp_runtime_csproj,
render_csharp_runtime_json_template,
};
pub(crate) use languages::{
elixir_native_crate_dir, migrate_build_zig_test_target, migrate_dart_placeholder_test, migrate_dart_pubignore,
migrate_java_checkstyle_line_length, migrate_kotlin_build_gradle, migrate_node_package_json_service_export,
migrate_php_composer_phpunit_constraint, migrate_swift_placeholder_test, migrate_wasm_package_json_exports,
migrate_zig_build_ffi_include_default, migrate_zig_example, ruby_native_manifest_path,
};
#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct WorkspacePackageInheritance {
pub version: bool,
pub readme: bool,
pub keywords: bool,
pub categories: bool,
pub license: bool,
}
pub(crate) fn detect_workspace_inheritance(workspace_root: Option<&std::path::Path>) -> WorkspacePackageInheritance {
let cargo_toml_path = workspace_root
.map(|r| r.join("Cargo.toml"))
.unwrap_or_else(|| std::path::PathBuf::from("Cargo.toml"));
let Ok(contents) = std::fs::read_to_string(&cargo_toml_path) else {
return WorkspacePackageInheritance::default();
};
let Ok(doc) = toml::from_str::<toml::Value>(&contents) else {
return WorkspacePackageInheritance::default();
};
let Some(workspace) = doc.get("workspace") else {
return WorkspacePackageInheritance::default();
};
let pkg = workspace.get("package");
WorkspacePackageInheritance {
version: pkg.map(|p| p.get("version").is_some()).unwrap_or(false),
readme: pkg.map(|p| p.get("readme").is_some()).unwrap_or(false),
keywords: pkg.map(|p| p.get("keywords").is_some()).unwrap_or(false),
categories: pkg.map(|p| p.get("categories").is_some()).unwrap_or(false),
license: pkg.map(|p| p.get("license").is_some()).unwrap_or(false),
}
}
fn read_workspace_package_fields(dir: &std::path::Path) -> Option<WorkspacePackageInheritance> {
let contents = std::fs::read_to_string(dir.join("Cargo.toml")).ok()?;
let doc = toml::from_str::<toml::Value>(&contents).ok()?;
let pkg = doc.get("workspace")?.get("package")?;
Some(WorkspacePackageInheritance {
version: pkg.get("version").is_some(),
readme: pkg.get("readme").is_some(),
keywords: pkg.get("keywords").is_some(),
categories: pkg.get("categories").is_some(),
license: pkg.get("license").is_some(),
})
}
fn crate_dir_is_excluded(root_doc: &toml::Value, crate_relative_dir: &str) -> bool {
let Some(excludes) = root_doc
.get("workspace")
.and_then(|w| w.get("exclude"))
.and_then(|e| e.as_array())
else {
return false;
};
let normalized = crate_relative_dir.trim_matches('/');
excludes.iter().filter_map(|entry| entry.as_str()).any(|pattern| {
let pattern = pattern.trim_matches('/');
normalized == pattern || normalized.starts_with(&format!("{pattern}/"))
})
}
pub(crate) fn detect_workspace_inheritance_for_crate(
workspace_root: Option<&std::path::Path>,
crate_relative_dir: &str,
) -> WorkspacePackageInheritance {
let Some(root) = workspace_root else {
return WorkspacePackageInheritance::default();
};
let root_reaches = std::fs::read_to_string(root.join("Cargo.toml"))
.ok()
.and_then(|contents| toml::from_str::<toml::Value>(&contents).ok())
.filter(|doc| doc.get("workspace").is_some())
.is_some_and(|doc| !crate_dir_is_excluded(&doc, crate_relative_dir));
if root_reaches && let Some(inheritance) = read_workspace_package_fields(root) {
return inheritance;
}
read_workspace_package_fields(&root.join(crate_relative_dir)).unwrap_or_default()
}
pub(crate) fn cargo_package_header(
name: &str,
version: &str,
edition: &str,
meta: &ScaffoldMeta,
ws: &WorkspacePackageInheritance,
) -> String {
let version_line = if ws.version {
"version.workspace = true".to_string()
} else {
format!("version = \"{version}\"")
};
let edition_line = format!("edition = \"{edition}\"");
let license_line = if ws.license {
Some("license.workspace = true".to_string())
} else {
meta.license.as_ref().map(|license| format!("license = \"{license}\""))
};
let readme_line = if ws.readme {
"readme.workspace = true".to_string()
} else {
"readme = false".to_string()
};
let keywords_line = if ws.keywords {
"keywords.workspace = true".to_string()
} else if meta.keywords.is_empty() {
"keywords = []".to_string()
} else {
let quoted: Vec<String> = meta.keywords.iter().map(|k| format!("\"{k}\"")).collect();
format!("keywords = [{}]", quoted.join(", "))
};
let categories_line = if ws.categories {
"categories.workspace = true".to_string()
} else if meta.categories.is_empty() {
"categories = []".to_string()
} else {
let quoted: Vec<String> = meta.categories.iter().map(|k| format!("\"{k}\"")).collect();
format!("categories = [{}]", quoted.join(", "))
};
let mut lines = vec![
"[package]".to_string(),
format!("name = \"{name}\""),
version_line,
edition_line,
format!("description = \"{}\"", meta.description),
readme_line,
keywords_line,
categories_line,
];
if let Some(license_line) = license_line {
lines.insert(4, license_line);
}
lines.join("\n")
}
pub(crate) fn to_pep440(version: &str) -> String {
if let Some((base, pre)) = version.split_once('-') {
let pep = pre
.replace("alpha.", "a")
.replace("alpha", "a")
.replace("beta.", "b")
.replace("beta", "b")
.replace("rc.", "rc")
.replace('.', "");
format!("{base}{pep}")
} else {
version.to_string()
}
}
pub(crate) fn render_core_dep(crate_name: &str, rel_path: &str, features: &str, version: &str) -> String {
if version.is_empty() {
format!("{crate_name} = {{ path = \"{rel_path}\"{features} }}")
} else {
format!("{crate_name} = {{ version = \"{version}\", path = \"{rel_path}\"{features} }}")
}
}
pub(crate) fn render_core_dep_with_overrides(
crate_name: &str,
rel_path: &str,
default_features: &str,
version: &str,
overrides: &[crate::core::config::FfiTargetDepOverride],
) -> (String, String) {
if overrides.is_empty() {
return (
render_core_dep(crate_name, rel_path, default_features, version),
String::new(),
);
}
let combined_cfg = if overrides.len() == 1 {
overrides[0].cfg.clone()
} else {
let cfgs: Vec<String> = overrides.iter().map(|o| o.cfg.clone()).collect();
format!("any({})", cfgs.join(", "))
};
let mut entries: Vec<(String, String)> = vec![(
format!("not({combined_cfg})"),
render_core_dep(crate_name, rel_path, default_features, version),
)];
for override_ in overrides {
let default_block = if override_.default_features {
String::new()
} else {
", default-features = false".to_string()
};
let feats = if override_.features.is_empty() {
String::new()
} else {
let quoted: Vec<String> = override_.features.iter().map(|f| format!("\"{f}\"")).collect();
format!(", features = [{}]", quoted.join(", "))
};
entries.push((
override_.cfg.clone(),
render_core_dep(crate_name, rel_path, &format!("{default_block}{feats}"), version),
));
}
(String::new(), join_sorted_target_dep_blocks(entries))
}
pub(crate) fn join_sorted_target_dep_blocks(mut entries: Vec<(String, String)>) -> String {
entries.sort_by(|a, b| a.0.cmp(&b.0));
entries
.into_iter()
.map(|(cfg, dep_line)| format!("[target.'cfg({cfg})'.dependencies]\n{dep_line}\n"))
.collect::<Vec<_>>()
.join("\n")
}
pub(crate) fn dependency_sort_key(line: &str) -> String {
let mut name = String::new();
let mut quote: Option<char> = None;
let mut escaped = false;
for character in line.chars() {
if escaped {
name.push(character);
escaped = false;
continue;
}
match quote {
Some('"') if character == '\\' => escaped = true,
Some(open) if character == open => quote = None,
Some(_) => name.push(character),
None if character == '"' || character == '\'' => quote = Some(character),
None if character == '.' || character == '=' => break,
None => name.push(character),
}
}
name.trim().to_owned()
}
pub(crate) fn sort_dependency_lines(lines: &mut [String]) {
lines.sort_by_key(|a| dependency_sort_key(a));
}
pub(crate) fn render_extra_deps(config: &ResolvedCrateConfig, lang: Language) -> String {
let deps = config.extra_deps_for_language(lang);
if deps.is_empty() {
return String::new();
}
let member_versions = workspace_member_versions(config);
let ws_dep_specs = workspace_dep_specs(config);
let mut lines: Vec<String> = deps
.iter()
.map(|(name, value)| match value {
toml::Value::String(version) => format!("{name} = \"{version}\""),
toml::Value::Table(table) => {
if table.get("workspace").and_then(|v| v.as_bool()) == Some(true) {
if let Some(concrete) = ws_dep_specs.get(name) {
return format!("{name} = {concrete}");
}
return format!("{name} = {value}");
}
let needs_version = table.contains_key("path") && !table.contains_key("version");
if let (true, Some(member_version)) = (needs_version, member_versions.get(name)) {
let mut injected = table.clone();
injected.insert("version".to_string(), toml::Value::String(member_version.clone()));
format!("{name} = {}", toml::Value::Table(injected))
} else {
format!("{name} = {value}")
}
}
other => format!("{name} = {other}"),
})
.collect();
sort_dependency_lines(&mut lines);
lines.join("\n")
}
fn workspace_member_versions(config: &ResolvedCrateConfig) -> std::collections::BTreeMap<String, String> {
let Some(root) = config.workspace_root.as_deref() else {
return std::collections::BTreeMap::new();
};
match crate::publish::workspace::workspace_member_crates(root) {
Ok(members) => members.versions,
Err(_) => std::collections::BTreeMap::new(),
}
}
fn workspace_dep_specs(config: &ResolvedCrateConfig) -> std::collections::BTreeMap<String, toml::Value> {
let start = config.workspace_root.clone().or_else(|| std::env::current_dir().ok());
let Some(mut dir) = start else {
return std::collections::BTreeMap::new();
};
if !dir.is_absolute()
&& let Ok(abs) = std::fs::canonicalize(&dir)
{
dir = abs;
}
loop {
let cargo_path = dir.join("Cargo.toml");
if let Ok(contents) = std::fs::read_to_string(&cargo_path)
&& let Ok(doc) = contents.parse::<toml_edit::DocumentMut>()
&& let Some(workspace) = doc.get("workspace")
&& let Some(dependencies) = workspace.get("dependencies")
&& let Some(table) = dependencies.as_table()
{
let mut result = std::collections::BTreeMap::new();
for (key, value) in table.iter() {
let val_str = value.to_string().trim().to_string();
let wrapped = format!("x = {}", val_str);
if let Ok(map) = toml::from_str::<std::collections::HashMap<String, toml::Value>>(&wrapped)
&& let Some(v) = map.get("x")
{
result.insert(key.to_string(), v.clone());
}
}
return result;
}
if !dir.pop() {
return std::collections::BTreeMap::new();
}
}
}
pub(crate) fn render_workspace_dep_or(config: &ResolvedCrateConfig, name: &str, fallback: &str) -> String {
if workspace_dep_specs(config).contains_key(name) {
format!("{name}.workspace = true")
} else {
format!("{name} = {fallback}")
}
}
const CLIPPY_WORKSPACE_LINTS_RATIONALE: &str = "\
# This crate deliberately does not use `[lints]` / `workspace = true`: its C-ABI /\n\
# PyO3 / napi / ext-php-rs / NIF boundary requires `unsafe` code, and the workspace's\n\
# `[workspace.lints.rust]` sets `unsafe_code = \"deny\"` -- an all-or-nothing table that\n\
# would turn every such boundary into a compile error. The `[lints.clippy]` block below\n\
# instead carries the subset of the workspace's deny-by-default lint policy this crate\n\
# can actually satisfy. ~keep";
fn with_clippy_rationale(rendered: &str) -> String {
match rendered.find("[lints.clippy]") {
Some(index) => {
let (before, from_header) = rendered.split_at(index);
format!("{before}{CLIPPY_WORKSPACE_LINTS_RATIONALE}\n{from_header}")
}
None => rendered.to_string(),
}
}
pub(crate) fn cargo_lints_clippy_block_with_rationale(config: &ResolvedCrateConfig) -> String {
with_clippy_rationale(&config.cargo_lints.clippy_block())
}
pub(crate) fn cargo_lints_section(config: &ResolvedCrateConfig) -> String {
let rendered = with_clippy_rationale(&config.cargo_lints.render());
if rendered.is_empty() {
String::new()
} else {
format!("\n{rendered}\n")
}
}
pub(crate) fn core_dep_features(config: &ResolvedCrateConfig, lang: Language) -> String {
let features = config.features_for_language(lang);
if features.is_empty() {
String::new()
} else {
let quoted: Vec<String> = features.iter().map(|f| format!("\"{f}\"")).collect();
format!(", features = [{}]", quoted.join(", "))
}
}
fn core_crate_manifest_path(config: &ResolvedCrateConfig) -> Option<std::path::PathBuf> {
let workspace_root = config.workspace_root.as_deref()?;
let first_source = config.sources.first()?;
let mut current = std::path::Path::new(first_source).parent();
while let Some(dir) = current {
if dir.file_name().is_some_and(|n| n == "src") {
let crate_dir = dir.parent()?;
return Some(workspace_root.join(crate_dir).join("Cargo.toml"));
}
current = dir.parent();
}
None
}
fn resolve_core_aggregate_features(
features_table: &toml::value::Table,
aggregate: &str,
) -> Option<std::collections::BTreeSet<String>> {
let _ = features_table.get(aggregate)?;
let mut reachable: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
let mut queue: Vec<String> = vec![aggregate.to_string()];
let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
while let Some(name) = queue.pop() {
if !visited.insert(name.clone()) {
continue;
}
let Some(members) = features_table.get(&name).and_then(|v| v.as_array()) else {
continue;
};
for member in members {
let Some(token) = member.as_str() else { continue };
if token.starts_with("dep:") || token.contains('/') {
continue;
}
reachable.insert(token.to_string());
if features_table.contains_key(token) {
queue.push(token.to_string());
}
}
}
Some(reachable)
}
pub(crate) fn core_feature_closure(
config: &ResolvedCrateConfig,
requested: &[String],
) -> (std::collections::BTreeSet<String>, std::collections::BTreeSet<String>) {
let mut active: std::collections::BTreeSet<String> = requested.iter().cloned().collect();
let no_defaults = std::collections::BTreeSet::new();
let Some(manifest_path) = core_crate_manifest_path(config) else {
return (active, no_defaults);
};
let Ok(contents) = std::fs::read_to_string(&manifest_path) else {
return (active, no_defaults);
};
let Ok(doc) = toml::from_str::<toml::Value>(&contents) else {
return (active, no_defaults);
};
let Some(features_table) = doc.get("features").and_then(|v| v.as_table()) else {
return (active, no_defaults);
};
for name in requested {
if let Some(members) = resolve_core_aggregate_features(features_table, name) {
active.extend(members);
}
}
let defaults: std::collections::BTreeSet<String> = features_table
.get("default")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str()).map(String::from).collect())
.unwrap_or_default();
(active, defaults)
}
pub(crate) fn android_target_feature_line(
config: &ResolvedCrateConfig,
passthrough_feature_names: &[&str],
) -> Option<String> {
android_target_feature_line_for_dep(config, &config.name, passthrough_feature_names)
}
pub(crate) fn android_target_feature_line_for_dep(
config: &ResolvedCrateConfig,
core_dep_key: &str,
passthrough_feature_names: &[&str],
) -> Option<String> {
let manifest_path = core_crate_manifest_path(config)?;
let contents = std::fs::read_to_string(&manifest_path).ok()?;
let doc = toml::from_str::<toml::Value>(&contents).ok()?;
let features_table = doc.get("features")?.as_table()?;
let aggregate_members = resolve_core_aggregate_features(features_table, "android-target")?;
let mut selected: Vec<&str> = passthrough_feature_names
.iter()
.copied()
.filter(|name| *name != "full" && aggregate_members.contains(*name))
.collect();
selected.sort_unstable();
selected.dedup();
let mut tokens: Vec<String> = vec![format!("{core_dep_key}/android-target")];
tokens.extend(selected.iter().map(|name| (*name).to_string()));
let list = tokens.iter().map(|t| format!("\"{t}\"")).collect::<Vec<_>>().join(", ");
Some(format!("android-target = [{list}]"))
}
pub fn scaffold(
api: &ApiSurface,
config: &ResolvedCrateConfig,
languages: &[Language],
) -> anyhow::Result<Vec<GeneratedFile>> {
let mut files = vec![];
for &lang in languages {
files.extend(scaffold_language(api, config, lang)?);
}
version_floor::apply_version_floors(&mut files, config);
files.extend(scaffold_poly_config(config, languages));
files.extend(scaffold_license_files(config, languages));
if !std::path::Path::new("rust-toolchain.toml").exists() {
files.push(rust_toolchain_file(languages));
}
if let Some(cargo) = config.scaffold.as_ref().and_then(|s| s.cargo.as_ref()) {
files.push(GeneratedFile {
path: std::path::PathBuf::from(".cargo/config.toml"),
content: render_cargo_config(cargo),
generated_header: true,
});
} else if languages.contains(&Language::Wasm) && !std::path::Path::new(".cargo/config.toml").exists() {
files.push(wasm_cargo_config_file());
}
files.extend(scaffold_gitattributes(config, languages));
Ok(files)
}
fn wasm_cargo_config_file() -> GeneratedFile {
GeneratedFile {
path: std::path::PathBuf::from(".cargo/config.toml"),
content: "[build]\nincremental = true\n\n[target.wasm32-unknown-unknown]\nrustflags = [\"-C\", \"target-feature=+bulk-memory\", \"--cfg\", \"getrandom_backend=\\\"wasm_js\\\"\", \"-C\", \"link-arg=--allow-multiple-definition\"]\n\n[net]\ngit-fetch-with-cli = true\n\n[registries.crates-io]\nprotocol = \"sparse\"\n".to_string(),
generated_header: true,
}
}
fn rust_toolchain_file(languages: &[Language]) -> GeneratedFile {
let targets = if languages.contains(&Language::Wasm) {
"targets = [\"wasm32-unknown-unknown\"]\n"
} else {
""
};
GeneratedFile {
path: std::path::PathBuf::from("rust-toolchain.toml"),
content: format!(
"[toolchain]\nchannel = \"1.95\"\ncomponents = [\"rust-src\", \"rustfmt\", \"clippy\"]\n{targets}"
),
generated_header: true,
}
}
const STALE_WASM_CARGO_CONFIG: &str = "[build]\nincremental = true\n\n[target.wasm32-unknown-unknown]\nrustflags = [\"-C\", \"target-feature=+bulk-memory\", \"--cfg\", \"getrandom_backend=\\\"wasm_js\\\"\"]\n\n[net]\ngit-fetch-with-cli = true\n\n[registries.crates-io]\nprotocol = \"sparse\"\n";
pub(crate) fn migrate_wasm_cargo_config_allow_multiple_definition(base_dir: &std::path::Path) -> anyhow::Result<bool> {
let path = base_dir.join(".cargo/config.toml");
let Ok(existing) = std::fs::read_to_string(&path) else {
return Ok(false);
};
if existing != STALE_WASM_CARGO_CONFIG {
return Ok(false);
}
let replacement = wasm_cargo_config_file().content;
let parent = path
.parent()
.context(".cargo/config.toml path has no parent directory")?;
let mut temporary = tempfile::NamedTempFile::new_in(parent)
.with_context(|| format!("failed to create temporary file in {}", parent.display()))?;
std::io::Write::write_all(&mut temporary, replacement.as_bytes())
.with_context(|| format!("failed to write temporary file for {}", path.display()))?;
temporary
.persist(&path)
.map_err(|error| error.error)
.with_context(|| format!("failed to replace {}", path.display()))?;
tracing::warn!(
path = %path.display(),
"repaired pre-existing .cargo/config.toml: added -C link-arg=--allow-multiple-definition \
to the wasm32-unknown-unknown rustflags"
);
Ok(true)
}
pub fn render_cargo_config(cargo: &ScaffoldCargo) -> String {
let mut out = String::new();
out.push_str("# This file is auto-generated by alef. DO NOT EDIT.\n");
out.push_str("# Re-generate with: alef scaffold\n");
out.push('\n');
out.push_str("[build]\nincremental = true\n");
if cargo.build_jobs > 0 {
out.push_str(&format!("jobs = {}\n", cargo.build_jobs));
}
if let Some(wrapper) = cargo.rustc_wrapper.as_deref() {
out.push_str(&format!("rustc-wrapper = \"{}\"\n", escape_toml_string(wrapper)));
}
out.push('\n');
out.push_str("[net]\ngit-fetch-with-cli = true\n\n");
out.push_str("[registries.crates-io]\nprotocol = \"sparse\"\n");
let t = &cargo.targets;
if t.macos_dynamic_lookup {
out.push_str(
"\n# Required for PyO3 / ext-php-rs cdylibs: Python and Zend C-API symbols are\n\
# resolved at runtime when the host loads the extension, not at link time.\n\
# macOS ld is strict and rejects unresolved symbols by default.\n\
[target.'cfg(target_os = \"macos\")']\n\
rustflags = [\"-C\", \"link-arg=-Wl,-undefined,dynamic_lookup\"]\n",
);
}
if t.x86_64_pc_windows_msvc {
out.push_str("\n[target.x86_64-pc-windows-msvc]\nlinker = \"rust-lld\"\n");
}
if t.i686_pc_windows_msvc {
out.push_str("\n[target.i686-pc-windows-msvc]\nlinker = \"rust-lld\"\n");
}
if t.aarch64_unknown_linux_gnu {
out.push_str("\n[target.aarch64-unknown-linux-gnu]\nlinker = \"aarch64-linux-gnu-gcc\"\n");
}
if t.x86_64_unknown_linux_musl {
out.push_str("\n[target.x86_64-unknown-linux-musl]\nlinker = \"musl-gcc\"\n");
}
if t.wasm32_unknown_unknown {
out.push_str(
"\n[target.wasm32-unknown-unknown]\n\
rustflags = [\"-C\", \"target-feature=+bulk-memory\", \"--cfg\", \"getrandom_backend=\\\"wasm_js\\\"\", \"-C\", \"link-arg=--allow-multiple-definition\"]\n",
);
}
if !cargo.env.is_empty() {
out.push_str("\n[env]\n");
let mut keys: Vec<&String> = cargo.env.keys().collect();
keys.sort();
for key in keys {
let value = &cargo.env[key];
match value {
ScaffoldCargoEnvValue::Plain(s) => {
out.push_str(&template_env::render(
"cargo_env_plain.jinja",
minijinja::context! { key => key, value => escape_toml_string(s) },
));
}
ScaffoldCargoEnvValue::Structured { value, relative } => {
out.push_str(&template_env::render(
"cargo_env_structured.jinja",
minijinja::context! {
key => key,
value => escape_toml_string(value),
relative => if *relative { "true" } else { "false" },
},
));
}
}
}
}
out
}
fn escape_toml_string(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}
pub struct ScaffoldMeta {
pub description: String,
pub license: Option<String>,
pub repository: Option<String>,
pub configured_repository: Option<String>,
pub homepage: String,
pub documentation: String,
pub issues: String,
pub funding: String,
pub authors: Vec<String>,
pub keywords: Vec<String>,
pub categories: Vec<String>,
}
pub fn scaffold_meta(config: &ResolvedCrateConfig) -> ScaffoldMeta {
let scaffold = config.scaffold.as_ref();
let package = config.package_metadata.as_ref();
let truncate = package.map(|p| p.truncate_registry_lists).unwrap_or(false);
let configured_repository = package
.and_then(|p| p.repository.clone())
.or_else(|| scaffold.and_then(|s| s.repository.clone()));
let mut keywords = package
.filter(|p| !p.keywords.is_empty())
.map(|p| p.keywords.clone())
.or_else(|| scaffold.map(|s| s.keywords.clone()))
.unwrap_or_default();
let mut categories = package.map(|p| p.categories.clone()).unwrap_or_default();
keywords.sort();
categories.sort();
if truncate {
keywords.truncate(5);
categories.truncate(5);
}
ScaffoldMeta {
description: package
.and_then(|p| p.description.clone())
.or_else(|| scaffold.and_then(|s| s.description.clone()))
.unwrap_or_else(|| format!("Bindings for {}", config.name)),
license: package
.and_then(|p| p.license.clone())
.or_else(|| scaffold.and_then(|s| s.license.clone())),
repository: configured_repository.clone(),
configured_repository,
homepage: package
.and_then(|p| p.homepage.clone())
.or_else(|| scaffold.and_then(|s| s.homepage.clone()))
.unwrap_or_default(),
documentation: package.and_then(|p| p.documentation.clone()).unwrap_or_default(),
issues: package.and_then(|p| p.issues.clone()).unwrap_or_default(),
funding: package.and_then(|p| p.funding.clone()).unwrap_or_default(),
authors: package
.filter(|p| !p.authors.is_empty())
.map(|p| p.authors.clone())
.or_else(|| scaffold.map(|s| s.authors.clone()))
.unwrap_or_default(),
keywords,
categories,
}
}
pub(crate) fn readme_language_configured(config: &ResolvedCrateConfig, lang_code: &str) -> bool {
config
.readme
.as_ref()
.is_some_and(|readme| readme.languages.contains_key(lang_code))
}
pub fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
pub fn parse_author(s: &str) -> (&str, &str) {
if let Some(start) = s.find('<')
&& let Some(end) = s.find('>')
{
let name = s[..start].trim();
let email = &s[start + 1..end];
return (name, email);
}
(s.trim(), "")
}
pub(crate) fn capitalize_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().to_string() + chars.as_str(),
}
}
fn scaffold_license_files(config: &ResolvedCrateConfig, languages: &[Language]) -> Vec<GeneratedFile> {
let license_path = config
.workspace_root
.as_deref()
.map(|r| r.join("LICENSE"))
.unwrap_or_else(|| std::path::PathBuf::from("LICENSE"));
let license_content = match std::fs::read_to_string(&license_path) {
Ok(content) => content,
Err(_) => {
tracing::warn!(
"No LICENSE file found at {} — skipping LICENSE sync into package directories",
license_path.display()
);
return vec![];
}
};
let mut seen = std::collections::BTreeSet::new();
let mut files = vec![];
for &lang in languages {
match lang {
Language::Rust | Language::C | Language::Ffi | Language::Jni => continue,
_ => {}
}
let pkg_dir = config.package_dir(lang);
if seen.insert(pkg_dir.clone()) {
files.push(GeneratedFile {
path: std::path::PathBuf::from(format!("{pkg_dir}/LICENSE")),
content: license_content.clone(),
generated_header: false,
});
}
}
files
}
fn scaffold_gitattributes(config: &ResolvedCrateConfig, languages: &[Language]) -> Vec<GeneratedFile> {
let mut dirs: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for &lang in languages {
match lang {
Language::Rust | Language::C => {}
Language::Ffi => {
dirs.insert(format!("crates/{}-ffi", config.core_crate_dir()));
}
Language::Jni => {
dirs.insert(format!("crates/{}-jni", config.name));
}
Language::Python => {
dirs.insert(config.package_dir(lang));
dirs.insert(format!("crates/{}-py", config.core_crate_dir()));
}
Language::Php => {
dirs.insert(config.package_dir(lang));
dirs.insert(format!("crates/{}-php", config.core_crate_dir()));
}
Language::Kotlin => {
let dir = if let Some(k) = config.kotlin.as_ref() {
if k.mode.as_deref() == Some("kmp") || k.target == crate::core::config::KotlinTarget::Multiplatform
{
"packages/kotlin-mpp".to_string()
} else if k.target == crate::core::config::KotlinTarget::Native {
"packages/kotlin-native".to_string()
} else {
config.package_dir(lang)
}
} else {
config.package_dir(lang)
};
dirs.insert(dir);
}
Language::Node => {
let dir = config
.node
.as_ref()
.and_then(|c| c.crate_dir.as_ref())
.map(|s| s.to_string())
.unwrap_or_else(|| format!("crates/{}-node", config.name));
dirs.insert(dir);
}
_ => {
dirs.insert(config.package_dir(lang));
}
}
}
let e2e_dir = config.e2e.as_ref().map(|e| e.output.as_str()).unwrap_or("e2e");
dirs.insert(e2e_dir.to_string());
let test_apps_dir = config
.e2e
.as_ref()
.map(|e| e.registry.output.as_str())
.unwrap_or("test_apps");
dirs.insert(test_apps_dir.to_string());
let mut content = String::from("# Generated by alef scaffold.\n");
for dir in dirs {
let dir = dir.trim_end_matches('/');
content.push_str(&format!("{dir}/** linguist-generated=true\n"));
}
vec![GeneratedFile {
path: std::path::PathBuf::from(".gitattributes"),
content,
generated_header: false,
}]
}
use languages::{
scaffold_csharp, scaffold_dart, scaffold_elixir, scaffold_elixir_cargo, scaffold_ffi, scaffold_gleam, scaffold_go,
scaffold_java, scaffold_jni, scaffold_kotlin, scaffold_node, scaffold_node_cargo, scaffold_php, scaffold_php_cargo,
scaffold_poly_config, scaffold_python, scaffold_python_cargo, scaffold_r, scaffold_r_cargo, scaffold_ruby,
scaffold_ruby_cargo, scaffold_swift, scaffold_wasm, scaffold_zig,
};
fn scaffold_language(
api: &ApiSurface,
config: &ResolvedCrateConfig,
lang: Language,
) -> anyhow::Result<Vec<GeneratedFile>> {
match lang {
Language::Python => {
let mut files = scaffold_python(api, config)?;
files.extend(scaffold_python_cargo(api, config)?);
Ok(files)
}
Language::Node => {
let mut files = scaffold_node(api, config)?;
files.extend(scaffold_node_cargo(api, config)?);
Ok(files)
}
Language::Ffi => scaffold_ffi(api, config),
Language::Go => scaffold_go(api, config),
Language::Java => scaffold_java(api, config),
Language::Csharp => scaffold_csharp(api, config),
Language::Ruby => {
let mut files = scaffold_ruby(api, config)?;
files.extend(scaffold_ruby_cargo(api, config)?);
Ok(files)
}
Language::Php => {
let mut files = scaffold_php(api, config)?;
files.extend(scaffold_php_cargo(api, config)?);
Ok(files)
}
Language::Elixir => {
let mut files = scaffold_elixir(api, config)?;
files.extend(scaffold_elixir_cargo(api, config)?);
Ok(files)
}
Language::Wasm => scaffold_wasm(api, config),
Language::R => {
let mut files = scaffold_r(api, config)?;
files.extend(scaffold_r_cargo(api, config)?);
Ok(files)
}
Language::Rust | Language::C => Ok(vec![]),
Language::Jni => scaffold_jni(api, config),
Language::Kotlin => scaffold_kotlin(api, config),
Language::KotlinAndroid => Ok(vec![]),
Language::Gleam => scaffold_gleam(api, config),
Language::Zig => scaffold_zig(api, config),
Language::Dart => scaffold_dart(api, config),
Language::Swift => scaffold_swift(api, config),
}
}
#[cfg(test)]
mod tests;