use std::path::Path;
use std::process::Command;
#[derive(Debug, thiserror::Error)]
pub(crate) enum BuildRunnerError {
#[error(
"`--run-build-runner` was passed, but no Dart SDK was found on PATH (looked for the \
`dart` executable). Install the Dart or Flutter SDK \
(https://docs.flutter.dev/get-started/install), make sure it's on PATH, and re-run — \
or drop `--run-build-runner` and run this yourself from '{out}':\n\n \
dart run build_runner build --delete-conflicting-outputs\n"
)]
DartNotFound { out: String },
#[error(
"failed to launch `dart run build_runner build --delete-conflicting-outputs` in '{out}': {source}"
)]
Spawn {
out: String,
#[source]
source: std::io::Error,
},
#[error(
"`dart run build_runner build --delete-conflicting-outputs` failed in '{out}' ({status}) \
— see its output above for the underlying build_runner/riverpod_generator error"
)]
Failed {
out: String,
status: std::process::ExitStatus,
},
}
pub(crate) fn run_build_runner(out: &Path) -> Result<(), BuildRunnerError> {
spawn_build_runner(out, "dart")
}
fn spawn_build_runner(out: &Path, program: &str) -> Result<(), BuildRunnerError> {
let out_display = out.display().to_string();
let status = Command::new(program)
.args([
"run",
"build_runner",
"build",
"--delete-conflicting-outputs",
])
.current_dir(out)
.status()
.map_err(|source| {
if source.kind() == std::io::ErrorKind::NotFound {
BuildRunnerError::DartNotFound {
out: out_display.clone(),
}
} else {
BuildRunnerError::Spawn {
out: out_display.clone(),
source,
}
}
})?;
if !status.success() {
return Err(BuildRunnerError::Failed {
out: out_display,
status,
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::io;
use tempfile::TempDir;
use super::{BuildRunnerError, spawn_build_runner};
#[test]
fn dart_not_found_message_names_the_tool_and_manual_command() {
let error = BuildRunnerError::DartNotFound {
out: "/tmp/client".to_owned(),
};
let message = error.to_string();
assert!(message.contains("no Dart SDK was found on PATH"));
assert!(message.contains("dart run build_runner build --delete-conflicting-outputs"));
assert!(message.contains("/tmp/client"));
}
#[test]
fn spawn_error_does_not_claim_the_sdk_is_missing() {
let error = BuildRunnerError::Spawn {
out: "/tmp/client".to_owned(),
source: io::Error::new(io::ErrorKind::PermissionDenied, "denied"),
};
let message = error.to_string();
assert!(!message.contains("no Dart SDK was found"));
assert!(message.contains("failed to launch"));
}
#[test]
fn a_genuinely_missing_program_is_reported_as_dart_not_found() {
let dir = TempDir::new().expect("tempdir");
let error = spawn_build_runner(
dir.path(),
"cratestack-cli-test-definitely-not-a-real-binary-9f3c2b",
)
.expect_err("a nonexistent program must fail to spawn");
assert!(
matches!(error, BuildRunnerError::DartNotFound { .. }),
"expected DartNotFound, got {error:?}"
);
}
#[cfg(unix)]
#[test]
fn a_real_nonzero_exit_is_reported_as_failed() {
let dir = TempDir::new().expect("tempdir");
let error =
spawn_build_runner(dir.path(), "false").expect_err("`false` always exits non-zero");
match error {
BuildRunnerError::Failed { status, .. } => {
assert_eq!(status.code(), Some(1));
}
other => panic!("expected Failed, got {other:?}"),
}
}
#[cfg(unix)]
#[test]
fn a_real_zero_exit_succeeds() {
let dir = TempDir::new().expect("tempdir");
spawn_build_runner(dir.path(), "true").expect("`true` always exits zero");
}
}