use std::process::Command;
pub(super) const FLUTTER_RUST_BRIDGE_CODEGEN: &str = "flutter_rust_bridge_codegen";
pub(super) fn run(cmd: &str, expected_version: &str) -> anyhow::Result<()> {
let output = match Command::new(cmd).arg("--version").output() {
Ok(output) if output.status.success() => output,
Ok(_) | Err(_) => return Ok(()),
};
let stdout = String::from_utf8_lossy(&output.stdout);
let Some(installed_version) = parse_version(&stdout) else {
return Ok(());
};
if installed_version != expected_version {
anyhow::bail!(
"installed {cmd} is version {installed_version}, but this project pins flutter_rust_bridge \
{expected_version} (`[crates.dart] frb_version` in alef.toml, else alef's default); bridge output \
is not deterministic across codegen versions.\n \
Fix: cargo install flutter_rust_bridge_codegen --version {expected_version} --locked\n \
Or: set `[crates.dart] frb_version` to {installed_version}"
);
}
Ok(())
}
fn parse_version(output: &str) -> Option<String> {
output
.lines()
.next()?
.split_whitespace()
.next_back()
.map(str::to_string)
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
use std::io::Write as _;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt as _;
#[test]
fn parse_version_extracts_trailing_token_from_clap_style_output() {
assert_eq!(
parse_version("flutter_rust_bridge_codegen 2.13.0\n"),
Some("2.13.0".to_string())
);
}
#[test]
fn parse_version_handles_missing_newline() {
assert_eq!(
parse_version("flutter_rust_bridge_codegen 2.13.0"),
Some("2.13.0".to_string())
);
}
#[test]
fn parse_version_returns_none_for_empty_output() {
assert_eq!(parse_version(""), None);
}
#[cfg(unix)]
fn fake_codegen_binary(dir: &std::path::Path, version: &str) -> std::path::PathBuf {
let path = dir.join("fake_flutter_rust_bridge_codegen.sh");
let mut file = std::fs::File::create(&path).expect("must create fake binary script");
writeln!(file, "#!/bin/sh").expect("must write shebang");
writeln!(file, "echo 'flutter_rust_bridge_codegen {version}'").expect("must write echo line");
drop(file);
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).expect("must chmod +x");
path
}
#[cfg(unix)]
#[test]
fn run_is_ok_when_installed_version_matches_the_pin() {
let dir = tempfile::tempdir().expect("temp dir");
let binary = fake_codegen_binary(dir.path(), "2.13.0");
let result = run(binary.to_str().expect("utf8 path"), "2.13.0");
assert!(result.is_ok(), "matching versions must not fail: {result:?}");
}
#[cfg(unix)]
#[test]
fn run_fails_loudly_when_installed_version_disagrees_with_the_pin() {
let dir = tempfile::tempdir().expect("temp dir");
let binary = fake_codegen_binary(dir.path(), "2.9.0");
let result = run(binary.to_str().expect("utf8 path"), "2.13.0");
let err = result.expect_err("mismatched versions must fail");
let message = format!("{err:#}");
assert!(
message.contains("2.9.0") && message.contains("2.13.0"),
"error must name both the installed and pinned versions: {message}"
);
}
#[test]
fn run_is_ok_when_binary_is_not_on_path() {
let result = run("alef-frb-version-check-intentionally-not-on-path-xyz789", "2.13.0");
assert!(
result.is_ok(),
"a missing binary must not be treated as a mismatch: {result:?}"
);
}
}