use anyhow::Result;
use crate::core::backend::CompilePolicy;
fn language_has_post_build_steps(
language: crate::core::config::Language,
config: &crate::core::config::ResolvedCrateConfig,
) -> bool {
crate::cli::registry::try_get_backend(language)
.and_then(|backend| backend.generate_post_build_config(config))
.is_some_and(|build_config| !build_config.post_build.is_empty())
}
pub(crate) fn languages_have_post_build_steps(
languages: &[crate::core::config::Language],
config: &crate::core::config::ResolvedCrateConfig,
) -> bool {
languages
.iter()
.any(|&language| language_has_post_build_steps(language, config))
}
pub(crate) fn languages_with_post_build_steps(
languages: &[crate::core::config::Language],
config: &crate::core::config::ResolvedCrateConfig,
) -> Vec<crate::core::config::Language> {
languages
.iter()
.copied()
.filter(|&language| language_has_post_build_steps(language, config))
.collect()
}
fn resolve_post_build_configs(
languages: &[crate::core::config::Language],
config: &crate::core::config::ResolvedCrateConfig,
compile: CompilePolicy,
) -> Vec<(crate::core::config::Language, crate::core::backend::BuildConfig)> {
languages
.iter()
.filter_map(|&language| {
let backend = crate::cli::registry::try_get_backend(language)?;
let mut build_config = backend.generate_post_build_config(config)?;
if compile == CompilePolicy::Skipped {
let dropped = build_config
.post_build
.iter()
.filter(|step| step.invokes_rust_compiler())
.count();
if dropped > 0 {
tracing::warn!(
" [{language}] skipping {dropped} compiling post-build step(s) -- artifacts \
derived from them keep their current on-disk content until `alef build` runs"
);
}
build_config.post_build.retain(|step| !step.invokes_rust_compiler());
}
if build_config.post_build.is_empty() {
return None;
}
Some((language, build_config))
})
.collect()
}
pub(super) fn run_required_post_builds(
languages: &[crate::core::config::Language],
config: &crate::core::config::ResolvedCrateConfig,
base_dir: &std::path::Path,
compile: CompilePolicy,
) -> Result<()> {
let resolved = resolve_post_build_configs(languages, config, compile);
run_resolved_post_builds(&resolved, languages.len(), config, base_dir)
}
fn run_resolved_post_builds(
resolved: &[(crate::core::config::Language, crate::core::backend::BuildConfig)],
total_languages: usize,
config: &crate::core::config::ResolvedCrateConfig,
base_dir: &std::path::Path,
) -> Result<()> {
let mut failures: Vec<String> = Vec::new();
for (language, build_config) in resolved {
let language = *language;
tracing::info!(" [{language}] running post-build...");
match crate::cli::pipeline::run_post_build(
language,
build_config,
config,
base_dir,
crate::cli::pipeline::StagingProfile::NoBuildRequested,
) {
Ok(outcome) if outcome.skipped_missing_tools.is_empty() => {
tracing::info!(" [{language}] post-build processing complete");
}
Ok(outcome) => tracing::warn!(
" [{language}] post-build completed but skipped tool(s) not on PATH: {} -- \
falling back to committed generated files",
outcome.skipped_missing_tools.join(", ")
),
Err(error) => {
tracing::warn!("[{language}] post-build failed, continuing with remaining languages: {error:#}");
failures.push(format!("[{language}] {error:#}"));
}
}
}
if failures.is_empty() {
return Ok(());
}
anyhow::bail!(
"post-build failed for {} of {} language(s): {}",
failures.len(),
total_languages,
failures.join("; ")
);
}
#[cfg(test)]
mod tests {
use super::{
languages_have_post_build_steps, resolve_post_build_configs, run_required_post_builds, run_resolved_post_builds,
};
use crate::core::backend::CompilePolicy;
use crate::core::config::Language;
#[test]
fn detects_a_language_with_a_configured_post_build_step() {
assert!(languages_have_post_build_steps(
&[Language::Swift],
&crate::core::config::ResolvedCrateConfig::default()
));
}
#[test]
fn reports_false_for_a_language_with_no_post_build_step() {
assert!(!languages_have_post_build_steps(
&[Language::Python],
&crate::core::config::ResolvedCrateConfig::default()
));
}
#[test]
fn detects_a_post_build_language_mixed_with_languages_that_have_none() {
assert!(languages_have_post_build_steps(
&[Language::Python, Language::Swift],
&crate::core::config::ResolvedCrateConfig::default()
));
}
#[test]
fn reports_false_for_an_empty_language_list() {
assert!(!languages_have_post_build_steps(
&[],
&crate::core::config::ResolvedCrateConfig::default()
));
}
#[test]
fn swifts_generation_post_build_contains_a_compiling_step_by_default() {
let resolved = resolve_post_build_configs(
&[Language::Swift],
&crate::core::config::ResolvedCrateConfig::default(),
CompilePolicy::Allowed,
);
let (_, build_config) = resolved
.first()
.expect("swift must resolve a generation-time post-build config");
assert_eq!(
build_config
.post_build
.iter()
.filter(|step| step.invokes_rust_compiler())
.count(),
1,
"swift's generate config must still carry exactly the one cargo step the \
generation-only mode exists to drop: {:?}",
build_config.post_build
);
}
#[test]
fn generation_only_mode_drops_the_compiling_step_and_keeps_materialization() {
use crate::core::backend::PostBuildStep;
let resolved = resolve_post_build_configs(
&[Language::Swift],
&crate::core::config::ResolvedCrateConfig::default(),
CompilePolicy::Skipped,
);
let (_, build_config) = resolved
.first()
.expect("swift must still resolve a post-build config once its cargo step is dropped");
assert!(
!build_config.post_build.iter().any(|step| step.invokes_rust_compiler()),
"a generation-only run must invoke no compiler: {:?}",
build_config.post_build
);
assert!(
build_config
.post_build
.iter()
.any(|step| matches!(step, PostBuildStep::MaterializeSwiftBridge { .. })),
"the non-compiling materialization step must survive the drop: {:?}",
build_config.post_build
);
}
#[tracing_test::traced_test]
#[test]
fn generation_only_mode_never_spawns_the_swift_compile() {
let _skip_guard = crate::test_support::SkipCommandsGuard::set("");
let directory = tempfile::tempdir().expect("temporary project");
run_required_post_builds(
&[Language::Swift],
&crate::core::config::ResolvedCrateConfig::default(),
directory.path(),
CompilePolicy::Skipped,
)
.expect("a generation-only post-build pass must not attempt the swift-bridge compile");
assert!(
logs_contain("skipping 1 compiling post-build step"),
"the skip must be announced, not silent -- a consumer whose swift-bridge trio stops \
refreshing has to be able to see why"
);
}
#[tracing_test::traced_test]
#[test]
fn generation_post_build_does_not_warn_about_an_unbuilt_native_library() {
let directory = tempfile::tempdir().expect("temporary project");
run_required_post_builds(
&[Language::Go],
&crate::core::config::ResolvedCrateConfig::default(),
directory.path(),
CompilePolicy::Allowed,
)
.expect("staging nothing must not fail a generation run");
assert!(
!logs_contain("no built FFI shared library found"),
"a generation command never asked for a cdylib, so its absence must not be reported \
as a missing build"
);
assert!(
logs_contain("no built FFI shared library on disk"),
"the step must still run and still report the miss -- only its severity changed"
);
}
#[test]
fn required_post_build_failure_is_propagated_with_language_context() {
let _skip_guard = crate::test_support::SkipCommandsGuard::set("");
let directory = tempfile::tempdir().expect("temporary project");
let error = run_required_post_builds(
&[Language::Swift],
&crate::core::config::ResolvedCrateConfig::default(),
directory.path(),
CompilePolicy::Allowed,
)
.expect_err("missing Swift build project must fail");
assert!(error.to_string().contains("swift"));
}
#[test]
fn a_failing_language_does_not_abort_the_remaining_post_builds() {
use crate::core::backend::PostBuildStep;
use crate::core::config::ResolvedCrateConfig;
let _skip_guard = crate::test_support::SkipCommandsGuard::set("");
let directory = tempfile::tempdir().expect("temporary project");
let config = ResolvedCrateConfig::default();
let mut dart_build_config = crate::cli::registry::try_get_backend(Language::Dart)
.and_then(|backend| backend.build_config_with_config(&config))
.expect("Dart backend must produce a build config for the default crate config");
let (facade_path, bridge_path) = dart_build_config
.post_build
.iter()
.find_map(|step| match step {
PostBuildStep::VerifyFrbBridgeCoverage {
facade_path,
bridge_path,
..
} => Some((facade_path.clone(), bridge_path.clone())),
_ => None,
})
.expect("Dart's default post-build steps must include VerifyFrbBridgeCoverage");
for step in &mut dart_build_config.post_build {
if let PostBuildStep::RunCommand { cmd, .. } = step {
*cmd = "alef-frb-codegen-intentionally-not-on-path-xyz789";
}
}
let swift_build_config = crate::cli::registry::try_get_backend(Language::Swift)
.and_then(|backend| backend.build_config_with_config(&config))
.expect("Swift backend must produce a build config for the default crate config");
let facade_file = directory.path().join(&facade_path);
std::fs::create_dir_all(facade_file.parent().expect("facade path must have a parent")).unwrap();
std::fs::write(
&facade_file,
"pub fn count_widgets(collection: String) -> Result<i64, String> {\n Ok(0)\n}\n\
pub fn record_price(id: String, price_cents: i64) -> Result<(), String> {\n Ok(())\n}\n",
)
.unwrap();
let bridge_file = directory.path().join(&bridge_path);
std::fs::create_dir_all(bridge_file.parent().expect("bridge path must have a parent")).unwrap();
std::fs::write(
&bridge_file,
"Future<int> countWidgets({required String collection}) => \
RustLib.instance.api.crateCountWidgets(collection: collection);\n",
)
.unwrap();
let resolved = [
(Language::Swift, swift_build_config),
(Language::Dart, dart_build_config),
];
let result = run_resolved_post_builds(&resolved, resolved.len(), &config, directory.path());
let error = result.expect_err("missing Swift build project and a stale Dart bridge must both fail");
let message = error.to_string();
assert!(message.contains("swift"), "got: {message}");
assert!(message.contains("dart"), "got: {message}");
assert!(message.contains("2 of 2"), "got: {message}");
}
}