use anyhow::{Context, Result};
pub(crate) mod frozen;
pub(crate) use frozen::FrozenFile;
use frozen::frozen_managed_paths;
mod post_build;
use post_build::run_required_post_builds;
pub(crate) use post_build::{languages_have_post_build_steps, languages_with_post_build_steps};
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_with_environment(
config,
&[crate::core::config::Language::Ffi],
false,
&[("ALEF_EXPORT_GENERATED_HEADERS", "1")],
)
})
}
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>,
}
pub(crate) use super::verify_scan::collect_alef_hashes;
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()
}
fn marker_line(content: &str) -> Option<&str> {
content
.lines()
.find(|line| crate::core::hash::content_has_alef_marker(line))
}
#[derive(Default)]
pub(crate) struct MissingAndFrozenFiles {
pub(crate) missing: Vec<String>,
pub(crate) missing_gitignored: Vec<String>,
pub(crate) frozen: Vec<FrozenFile>,
pub(crate) managed_paths: std::collections::HashSet<std::path::PathBuf>,
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 managed_paths: std::collections::HashSet<std::path::PathBuf> =
surface.iter().map(|file| base_dir.join(&file.path)).collect();
managed_paths.extend(super::verify_orphans::post_build_owned_paths(
languages, config, base_dir,
));
let (missing, missing_gitignored) =
super::verify_gitignore::split_missing_by_gitignore(base_dir, &missing_managed_paths(&surface, base_dir));
let mut result = MissingAndFrozenFiles {
missing,
missing_gitignored,
frozen: frozen_managed_paths(&surface, base_dir),
managed_paths,
stage_failures: stage_failures
.into_iter()
.map(|failure| format!("[{}] {}", failure.stage, failure.message))
.collect(),
};
result.missing.sort();
result.missing.dedup();
result.missing_gitignored.sort();
result.missing_gitignored.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, true, 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 stale_among(
scanned: &[(std::path::PathBuf, Option<String>, String)],
inputs_hashes: &[String],
) -> Vec<StaleMismatch> {
if inputs_hashes.is_empty() {
return Vec::new();
}
let mut stale: Vec<StaleMismatch> = scanned
.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.clone().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));
stale
}
#[cfg(test)]
pub(crate) fn verify_walk(base_dir: &std::path::Path, inputs_hash: &str) -> anyhow::Result<Vec<StaleMismatch>> {
Ok(stale_among(&collect_alef_hashes(base_dir), &[inputs_hash.to_owned()]))
}
#[cfg(test)]
mod tests;