use anyhow::{Context, Result};
fn run_required_post_builds(
languages: &[crate::core::config::Language],
config: &crate::core::config::ResolvedCrateConfig,
base_dir: &std::path::Path,
) -> Result<()> {
for &language in languages {
let Some(backend) = crate::cli::registry::try_get_backend(language) else {
continue;
};
let Some(build_config) = backend.build_config_with_config(config) else {
continue;
};
if build_config.post_build.is_empty() {
continue;
}
tracing::info!(" [{language}] running post-build...");
crate::cli::pipeline::run_post_build(language, &build_config, config, base_dir)
.with_context(|| format!("failed to run required post-build steps for {language}"))?;
tracing::info!(" [{language}] post-build processing complete");
}
Ok(())
}
pub(crate) fn complete_generated_artifacts(
languages: &[crate::core::config::Language],
config: &crate::core::config::ResolvedCrateConfig,
base_dir: &std::path::Path,
) -> Result<()> {
run_required_post_builds(languages, config, base_dir)?;
if !languages.contains(&crate::core::config::Language::Ffi) {
return Ok(());
}
crate::cli::pipeline::ensure_ffi_header_freshness(config, base_dir, || {
crate::cli::pipeline::build(config, &[crate::core::config::Language::Ffi], false)
})
}
pub(crate) fn generated_files_match_disk(
lang_files: &[crate::core::backend::GeneratedFile],
base_dir: &std::path::Path,
) -> bool {
lang_files.iter().all(|file| {
let normalized = crate::cli::pipeline::normalize_content(&file.path, &file.content);
match std::fs::read_to_string(base_dir.join(&file.path)) {
Ok(disk) => crate::core::hash::strip_hash_line(&disk) == crate::core::hash::strip_hash_line(&normalized),
Err(_) => false,
}
})
}
pub(crate) fn default_log_level(verbose: u8, quiet: bool) -> &'static str {
if quiet {
return "error";
}
match verbose {
0 => "info",
1 => "debug",
_ => "trace",
}
}
pub(crate) fn init_tracing(verbose: u8, quiet: bool, no_color: bool) {
use tracing_subscriber::EnvFilter;
let default_level = default_log_level(verbose, quiet);
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_level));
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_ansi(!no_color)
.with_writer(std::io::stderr)
.without_time()
.with_target(false)
.init();
}
pub(crate) fn load_config(
path: &std::path::Path,
) -> Result<(
crate::core::config::WorkspaceConfig,
Vec<crate::core::config::ResolvedCrateConfig>,
)> {
let content =
std::fs::read_to_string(path).with_context(|| format!("Failed to read config: {}", path.display()))?;
crate::core::config::detect_legacy_keys(&content).with_context(|| {
format!(
"legacy schema detected in {} — run `alef migrate` to update automatically",
path.display()
)
})?;
let mut toml_value: toml::Value =
toml::from_str(&content).with_context(|| format!("Failed to parse alef.toml ({})", path.display()))?;
let deprecation_warnings = crate::core::config::legacy::strip_deprecated_keys(&mut toml_value);
for warning in &deprecation_warnings {
tracing::warn!("{}", warning);
}
let cfg: crate::core::config::NewAlefConfig = toml_value
.try_into()
.with_context(|| format!("Failed to deserialize alef.toml ({})", path.display()))?;
let resolved = cfg
.resolve()
.with_context(|| format!("failed to resolve crates in {}", path.display()))?;
for resolved_cfg in &resolved {
crate::core::config::validation::validate_resolved(resolved_cfg)
.with_context(|| format!("invalid resolved config for crate `{}`", resolved_cfg.name))?;
}
Ok((cfg.workspace, resolved))
}
pub(crate) fn resolve_languages(
config: &crate::core::config::ResolvedCrateConfig,
filter: Option<&[String]>,
) -> Result<Vec<crate::core::config::Language>> {
resolve_languages_inner(config, filter, false)
}
pub(crate) fn resolve_doc_languages(
config: &crate::core::config::ResolvedCrateConfig,
filter: Option<&[String]>,
) -> Result<Vec<crate::core::config::Language>> {
resolve_languages_inner(config, filter, true)
}
pub(crate) fn resolve_readme_languages(
config: &crate::core::config::ResolvedCrateConfig,
filter: Option<&[String]>,
) -> Result<Vec<crate::core::config::Language>> {
resolve_languages_inner(config, filter, true)
}
pub(crate) fn resolve_test_languages(
config: &crate::core::config::ResolvedCrateConfig,
filter: Option<&[String]>,
include_e2e: bool,
) -> Result<Vec<crate::core::config::Language>> {
match filter {
Some(langs) => {
let mut result = vec![];
for lang_str in langs {
let lang = parse_language(lang_str)?;
if config.languages.contains(&lang) || config.test.contains_key(&lang.to_string()) {
result.push(lang);
} else {
anyhow::bail!("Language '{lang_str}' not in config languages list or test configuration");
}
}
Ok(result)
}
None => {
let mut langs = config.languages.clone();
if include_e2e {
let mut extra_test_langs = vec![];
for (lang_str, test_config) in &config.test {
if test_config.e2e.is_none() {
continue;
}
let lang = parse_language(lang_str)
.with_context(|| format!("Invalid test language in alef.toml: {lang_str}"))?;
if !langs.contains(&lang) {
extra_test_langs.push(lang);
}
}
extra_test_langs.sort_by_key(|lang| lang.to_string());
for lang in extra_test_langs {
if !langs.contains(&lang) {
langs.push(lang);
}
}
}
Ok(langs)
}
}
}
pub(crate) fn resolve_languages_inner(
config: &crate::core::config::ResolvedCrateConfig,
filter: Option<&[String]>,
allow_rust: bool,
) -> Result<Vec<crate::core::config::Language>> {
match filter {
Some(langs) => {
let mut result = vec![];
for lang_str in langs {
let lang = parse_language(lang_str)?;
if config.languages.contains(&lang) || (allow_rust && lang == crate::core::config::Language::Rust) {
result.push(lang);
} else {
anyhow::bail!("Language '{lang_str}' not in config languages list");
}
}
Ok(result)
}
None => {
let mut langs = config.languages.clone();
if allow_rust && !langs.contains(&crate::core::config::Language::Rust) {
langs.push(crate::core::config::Language::Rust);
}
Ok(langs)
}
}
}
pub(crate) fn parse_language(lang_str: &str) -> Result<crate::core::config::Language> {
toml::Value::String(lang_str.to_string())
.try_into()
.with_context(|| format!("Unknown language: {lang_str}"))
}
pub(crate) fn format_languages(languages: &[crate::core::config::Language]) -> String {
languages.iter().map(|l| l.to_string()).collect::<Vec<_>>().join(", ")
}
pub(crate) struct StaleMismatch {
pub(crate) path: String,
pub(crate) embedded: String,
pub(crate) computed: Vec<String>,
}
const VERIFY_SKIP_DIRS: &[&str] = &[
".git",
".alef",
"target",
"node_modules",
"_build",
"deps",
"parsers",
"dist",
"dist-node",
"vendor",
".venv",
".cache",
".remote-cache",
"__pycache__",
"build",
"tmp",
"out",
".idea",
".vscode",
"worktrees",
];
const VERIFY_SCAN_DOT_DIRS: &[&str] = &[
".cargo", ".github",
".agents", ".claude", ".codex", ".cursor", ".gemini",
];
const VERIFY_SCAN_EXTENSIONS: &[&str] = &[
"rs",
"py",
"pyi",
"ts",
"tsx",
"js",
"mjs",
"cjs",
"rb",
"rbs",
"php",
"phpstub",
"go",
"java",
"cs",
"ex",
"exs",
"R",
"r",
"toml",
"json",
"md",
"h",
"c",
"yaml",
"yml",
"zig",
"dart",
"kt",
"kts",
"swift",
"gleam",
"properties",
"pro",
"sh",
"props",
"xml",
"csproj",
"zon",
"cmake",
"gemspec",
];
const VERIFY_SCAN_FILENAMES: &[&str] = &[
".gitignore",
".gitattributes",
".editorconfig",
"Makefile",
"GNUmakefile",
"makefile",
"go.mod",
"Rakefile",
"Makevars",
"Makevars.in",
"Makevars.win.in",
];
fn collect_alef_hashes(base_dir: &std::path::Path) -> Vec<(std::path::PathBuf, Option<String>, String)> {
let mut found = Vec::new();
let mut stack: Vec<std::path::PathBuf> = vec![base_dir.to_path_buf()];
while let Some(dir) = stack.pop() {
let entries = match std::fs::read_dir(&dir) {
Ok(e) => e,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
let file_type = match entry.file_type() {
Ok(ft) => ft,
Err(_) => continue,
};
if file_type.is_dir() {
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
let pruned_as_dotfile = name.starts_with('.') && !VERIFY_SCAN_DOT_DIRS.contains(&name);
if VERIFY_SKIP_DIRS.contains(&name) || pruned_as_dotfile {
continue;
}
stack.push(path);
continue;
}
if !file_type.is_file() {
continue;
}
let name_ok = path
.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| VERIFY_SCAN_FILENAMES.contains(&n));
let ext_ok = name_ok
|| path
.extension()
.and_then(|e| e.to_str())
.map(|e| {
VERIFY_SCAN_EXTENSIONS
.iter()
.any(|allowed| allowed.eq_ignore_ascii_case(e))
})
.unwrap_or(false);
if !ext_ok {
continue;
}
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => continue,
};
if crate::core::hash::content_has_alef_marker(&content) {
found.push((path, crate::core::hash::extract_hash(&content), content));
}
}
}
found
}
pub(crate) struct StampDisagreement {
pub(crate) key: String,
pub(crate) examples: Vec<(String, String)>,
}
pub(crate) fn find_stamp_disagreement(base_dir: &std::path::Path, key: &str) -> Option<StampDisagreement> {
let mut by_value: std::collections::BTreeMap<String, String> = std::collections::BTreeMap::new();
for (path, _hash, content) in collect_alef_hashes(base_dir) {
let Some(value) = crate::core::hash::extract_stamp(&content, key) else {
continue;
};
by_value.entry(value).or_insert_with(|| path.display().to_string());
}
if by_value.len() < 2 {
return None;
}
Some(StampDisagreement {
key: key.to_string(),
examples: by_value.into_iter().map(|(value, path)| (path, value)).collect(),
})
}
fn missing_managed_paths(files: &[crate::core::backend::GeneratedFile], base_dir: &std::path::Path) -> Vec<String> {
crate::cli::pipeline::managed_generated_files(files)
.into_iter()
.filter(|file| !base_dir.join(&file.path).exists())
.map(|file| base_dir.join(&file.path).display().to_string())
.collect()
}
pub(crate) struct FrozenFile {
pub(crate) path: String,
pub(crate) remedy: Option<String>,
pub(crate) near_miss: Option<String>,
}
fn marker_line(content: &str) -> Option<&str> {
content
.lines()
.find(|line| crate::core::hash::content_has_alef_marker(line))
}
fn frozen_managed_paths(files: &[crate::core::backend::GeneratedFile], base_dir: &std::path::Path) -> Vec<FrozenFile> {
crate::cli::pipeline::managed_generated_files(files)
.into_iter()
.filter_map(|file| {
let full_path = base_dir.join(&file.path);
let existing = std::fs::read_to_string(&full_path).ok()?;
if crate::core::hash::content_has_alef_marker(&existing) {
return None;
}
let is_markable = crate::cli::pipeline::marker_comment_style(&full_path).is_some();
if !is_markable && crate::cli::pipeline::is_owned_by_ownership_record(base_dir, &full_path) {
return None;
}
let remedy = marker_line(&file.content).map(str::to_owned).or_else(|| {
let header = crate::cli::pipeline::provenance_header_for_path(&file.path)?;
marker_line(&header).map(str::to_owned)
});
let near_miss = crate::core::hash::near_miss_marker(&existing).map(str::to_owned);
Some(FrozenFile {
path: full_path.display().to_string(),
remedy,
near_miss,
})
})
.collect()
}
#[derive(Default)]
pub(crate) struct MissingAndFrozenFiles {
pub(crate) missing: Vec<String>,
pub(crate) frozen: Vec<FrozenFile>,
pub(crate) stage_failures: Vec<String>,
}
pub(crate) fn find_missing_and_frozen_generated_files(
languages: &[crate::core::config::Language],
api: &crate::core::ir::ApiSurface,
config: &crate::core::config::ResolvedCrateConfig,
config_path: &std::path::Path,
base_dir: &std::path::Path,
) -> anyhow::Result<MissingAndFrozenFiles> {
let (surface, stage_failures) = collect_managed_surface(languages, api, config, config_path, base_dir)?;
let mut result = MissingAndFrozenFiles {
missing: missing_managed_paths(&surface, base_dir),
frozen: frozen_managed_paths(&surface, base_dir),
stage_failures: stage_failures
.into_iter()
.map(|failure| format!("[{}] {}", failure.stage, failure.message))
.collect(),
};
result.missing.sort();
result.missing.dedup();
result.frozen.sort_by(|a, b| a.path.cmp(&b.path));
result.frozen.dedup_by(|a, b| a.path == b.path);
result.stage_failures.sort();
result.stage_failures.dedup();
Ok(result)
}
fn absorb_stage(
surface: &mut std::collections::BTreeMap<std::path::PathBuf, crate::core::backend::GeneratedFile>,
files: Vec<crate::core::backend::GeneratedFile>,
) {
for file in files {
surface.insert(file.path.clone(), file);
}
}
pub(crate) struct StageFailure {
pub(crate) stage: &'static str,
pub(crate) message: String,
pub(crate) paths: Vec<std::path::PathBuf>,
}
impl StageFailure {
pub(crate) fn affects_any(&self, targets: &[String]) -> bool {
self.paths.iter().any(|path| {
targets
.iter()
.any(|target| crate::cli::commands::adopt::matches_target(target, path))
})
}
}
pub(crate) fn collect_managed_surface(
languages: &[crate::core::config::Language],
api: &crate::core::ir::ApiSurface,
config: &crate::core::config::ResolvedCrateConfig,
config_path: &std::path::Path,
base_dir: &std::path::Path,
) -> anyhow::Result<(Vec<crate::core::backend::GeneratedFile>, Vec<StageFailure>)> {
type StageOutcome = anyhow::Result<(Vec<crate::core::backend::GeneratedFile>, Vec<StageFailure>)>;
type Stage<'a> = Box<dyn Fn() -> StageOutcome + Send + Sync + 'a>;
let bindings_stage: Stage<'_> = Box::new(|| {
let mut files = Vec::new();
for (_, produced) in crate::cli::pipeline::generate(api, config, languages, false, config_path, false)? {
files.extend(produced);
}
for (_, produced) in crate::cli::pipeline::generate_service_api(api, config, languages)? {
files.extend(produced);
}
Ok((files, Vec::new()))
});
let scaffold_stage: Stage<'_> = Box::new(|| {
let mut files = crate::cli::pipeline::scaffold(api, config, languages, config_path)?;
for (_, produced) in crate::cli::pipeline::generate_stubs(api, config, languages)? {
files.extend(produced);
}
if config.generate.public_api {
for (_, produced) in crate::cli::pipeline::generate_public_api(api, config, languages, config_path)? {
files.extend(produced);
}
}
Ok((files, Vec::new()))
});
let e2e_local_stage: Stage<'_> = Box::new(|| {
let Some(e2e_config) = &config.e2e else {
return Ok((Vec::new(), Vec::new()));
};
let (files, generator_error) = crate::e2e::generate_e2e(
config,
e2e_config,
None,
&api.types,
&api.enums,
&api.functions,
&api.errors,
)
.context("failed to render the e2e stage of alef's managed output")?;
Ok(stage_failure_for("e2e", generator_error, files))
});
let e2e_registry_stage: Stage<'_> = Box::new(|| {
let Some(e2e_config) = &config.e2e else {
return Ok((Vec::new(), Vec::new()));
};
let mut registry_config = e2e_config.clone();
registry_config.dep_mode = crate::core::config::e2e::DependencyMode::Registry;
let (files, generator_error) = crate::e2e::generate_e2e(
config,
®istry_config,
None,
&api.types,
&api.enums,
&api.functions,
&api.errors,
)
.context("failed to render the registry-mode test-app stage of alef's managed output")?;
Ok(stage_failure_for("test-apps (registry mode)", generator_error, files))
});
let readme_stage: Stage<'_> = Box::new(|| {
let readme_languages = crate::readme::expand_configured_readme_languages(config, languages);
Ok((
crate::cli::pipeline::readme(api, config, &readme_languages)?,
Vec::new(),
))
});
let docs_stage: Stage<'_> = Box::new(|| {
let doc_languages = resolve_doc_languages(config, None)?;
let (doc_files, doc_result) = crate::docs::generate_docs_stage_without_snippet_compile_validation(
api,
config,
&doc_languages,
None,
base_dir,
);
if let Err(error) = doc_result {
tracing::debug!("docs stage reported {error:#}; using the pages it rendered before the failure");
}
Ok((doc_files, Vec::new()))
});
let stages = [
bindings_stage,
scaffold_stage,
e2e_local_stage,
e2e_registry_stage,
readme_stage,
docs_stage,
];
let outcomes: Vec<StageOutcome> = {
use rayon::prelude::*;
stages.par_iter().map(|stage| stage()).collect()
};
let mut surface = std::collections::BTreeMap::new();
let mut stage_failures = Vec::new();
for outcome in outcomes {
let (files, failures) = outcome?;
stage_failures.extend(failures);
absorb_stage(&mut surface, files);
}
Ok((surface.into_values().collect(), stage_failures))
}
fn stage_failure_for(
stage: &'static str,
generator_error: Option<anyhow::Error>,
files: Vec<crate::core::backend::GeneratedFile>,
) -> (Vec<crate::core::backend::GeneratedFile>, Vec<StageFailure>) {
let failures = generator_error
.map(|error| StageFailure {
stage,
message: format!("{error:#}"),
paths: files.iter().map(|file| file.path.clone()).collect(),
})
.into_iter()
.collect();
(files, failures)
}
pub(crate) fn verify_walk_multi(
base_dir: &std::path::Path,
inputs_hashes: &[String],
) -> anyhow::Result<Vec<StaleMismatch>> {
if inputs_hashes.is_empty() {
return Ok(Vec::new());
}
if inputs_hashes.len() == 1 {
return verify_walk(base_dir, &inputs_hashes[0]);
}
let mut stale: Vec<StaleMismatch> = collect_alef_hashes(base_dir)
.into_iter()
.filter(|(_, disk_hash, content)| {
disk_hash.as_ref().is_none_or(|disk_hash| {
!inputs_hashes
.iter()
.any(|inputs_hash| crate::core::hash::compute_file_hash(inputs_hash, content) == *disk_hash)
})
})
.map(|(path, disk_hash, content)| StaleMismatch {
path: path.display().to_string(),
embedded: disk_hash.unwrap_or_else(|| "<missing>".to_owned()),
computed: inputs_hashes
.iter()
.map(|inputs_hash| crate::core::hash::compute_file_hash(inputs_hash, &content))
.collect(),
})
.collect();
stale.sort_by(|a, b| a.path.cmp(&b.path));
Ok(stale)
}
pub(crate) fn verify_walk(base_dir: &std::path::Path, inputs_hash: &str) -> anyhow::Result<Vec<StaleMismatch>> {
let mut stale: Vec<StaleMismatch> = collect_alef_hashes(base_dir)
.into_iter()
.filter(|(_, disk_hash, content)| {
disk_hash
.as_ref()
.is_none_or(|disk_hash| crate::core::hash::compute_file_hash(inputs_hash, content) != *disk_hash)
})
.map(|(path, disk_hash, content)| StaleMismatch {
path: path.display().to_string(),
embedded: disk_hash.unwrap_or_else(|| "<missing>".to_owned()),
computed: vec![crate::core::hash::compute_file_hash(inputs_hash, &content)],
})
.collect();
stale.sort_by(|a, b| a.path.cmp(&b.path));
Ok(stale)
}
#[cfg(test)]
mod tests;