1use std::io;
2use std::path::Path;
3use std::process::Command;
4
5use thiserror::Error;
6
7#[derive(Debug, Error)]
8pub enum BuildError {
9 #[error("build step {step}/{total} failed: `{command}` (exit {code:?})")]
10 StepFailed {
11 step: usize,
12 total: usize,
13 command: String,
14 code: Option<i32>,
15 },
16
17 #[error(transparent)]
18 Io(#[from] io::Error),
19}
20
21pub fn run(steps: &[String], cwd: &Path) -> Result<(), BuildError> {
22 for (i, step) in steps.iter().enumerate() {
23 let status = Command::new("sh")
24 .arg("-c")
25 .arg(step)
26 .current_dir(cwd)
27 .status()?;
28
29 if !status.success() {
30 return Err(BuildError::StepFailed {
31 step: i + 1,
32 total: steps.len(),
33 command: step.clone(),
34 code: status.code(),
35 });
36 }
37 }
38
39 Ok(())
40}
41
42#[cfg(test)]
43mod tests {
44 use super::*;
45
46 #[test]
47 fn run_succeeds_for_all_passing_steps() {
48 let dir = tempfile::tempdir().unwrap();
49 let steps = vec!["true".to_string(), "echo hello > out.txt".to_string()];
50 run(&steps, dir.path()).unwrap();
51 assert_eq!(
52 std::fs::read_to_string(dir.path().join("out.txt"))
53 .unwrap()
54 .trim(),
55 "hello"
56 );
57 }
58
59 #[test]
60 fn run_runs_steps_in_provided_cwd() {
61 let dir = tempfile::tempdir().unwrap();
62 let steps = vec!["echo cwd > marker".to_string()];
63 run(&steps, dir.path()).unwrap();
64 assert!(dir.path().join("marker").exists());
65 }
66
67 #[test]
68 fn run_fails_on_first_failing_step_with_index() {
69 let dir = tempfile::tempdir().unwrap();
70 let steps = vec!["true".to_string(), "false".to_string(), "true".to_string()];
71 let err = run(&steps, dir.path()).unwrap_err();
72 match err {
73 BuildError::StepFailed {
74 step,
75 total,
76 command,
77 code,
78 } => {
79 assert_eq!(step, 2);
80 assert_eq!(total, 3);
81 assert_eq!(command, "false");
82 assert_eq!(code, Some(1));
83 }
84 _ => panic!("expected StepFailed, got {err:?}"),
85 }
86 }
87
88 #[test]
89 fn run_handles_shell_features() {
90 let dir = tempfile::tempdir().unwrap();
91 let steps = vec!["echo a > a && echo b > b".to_string()];
92 run(&steps, dir.path()).unwrap();
93 assert!(dir.path().join("a").exists());
94 assert!(dir.path().join("b").exists());
95 }
96
97 #[test]
98 fn run_with_no_steps_is_ok() {
99 let dir = tempfile::tempdir().unwrap();
100 let steps: Vec<String> = vec![];
101 run(&steps, dir.path()).unwrap();
102 }
103}