use anyhow::Result;
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.build_config_with_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()
}
pub(super) fn run_required_post_builds(
languages: &[crate::core::config::Language],
config: &crate::core::config::ResolvedCrateConfig,
base_dir: &std::path::Path,
) -> Result<()> {
let resolved: Vec<(crate::core::config::Language, crate::core::backend::BuildConfig)> = languages
.iter()
.filter_map(|&language| {
let backend = crate::cli::registry::try_get_backend(language)?;
let build_config = backend.build_config_with_config(config)?;
if build_config.post_build.is_empty() {
return None;
}
Some((language, build_config))
})
.collect();
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::PreferOnDisk,
) {
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, run_required_post_builds, run_resolved_post_builds};
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 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(),
)
.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}");
}
}