dolly-cli 0.1.3

Like apt, but for GitHub repositories — clone, build, install and update tools from source.
Documentation
use std::io;
use std::path::Path;
use std::process::Command;

use thiserror::Error;

#[derive(Debug, Error)]
pub enum BuildError {
    #[error("build step {step}/{total} failed: `{command}` (exit {code:?})")]
    StepFailed {
        step: usize,
        total: usize,
        command: String,
        code: Option<i32>,
    },

    #[error(transparent)]
    Io(#[from] io::Error),
}

pub fn run(steps: &[String], cwd: &Path) -> Result<(), BuildError> {
    for (i, step) in steps.iter().enumerate() {
        let status = Command::new("sh")
            .arg("-c")
            .arg(step)
            .current_dir(cwd)
            .status()?;

        if !status.success() {
            return Err(BuildError::StepFailed {
                step: i + 1,
                total: steps.len(),
                command: step.clone(),
                code: status.code(),
            });
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn run_succeeds_for_all_passing_steps() {
        let dir = tempfile::tempdir().unwrap();
        let steps = vec!["true".to_string(), "echo hello > out.txt".to_string()];
        run(&steps, dir.path()).unwrap();
        assert_eq!(
            std::fs::read_to_string(dir.path().join("out.txt"))
                .unwrap()
                .trim(),
            "hello"
        );
    }

    #[test]
    fn run_runs_steps_in_provided_cwd() {
        let dir = tempfile::tempdir().unwrap();
        let steps = vec!["echo cwd > marker".to_string()];
        run(&steps, dir.path()).unwrap();
        assert!(dir.path().join("marker").exists());
    }

    #[test]
    fn run_fails_on_first_failing_step_with_index() {
        let dir = tempfile::tempdir().unwrap();
        let steps = vec!["true".to_string(), "false".to_string(), "true".to_string()];
        let err = run(&steps, dir.path()).unwrap_err();
        match err {
            BuildError::StepFailed {
                step,
                total,
                command,
                code,
            } => {
                assert_eq!(step, 2);
                assert_eq!(total, 3);
                assert_eq!(command, "false");
                assert_eq!(code, Some(1));
            }
            _ => panic!("expected StepFailed, got {err:?}"),
        }
    }

    #[test]
    fn run_handles_shell_features() {
        let dir = tempfile::tempdir().unwrap();
        let steps = vec!["echo a > a && echo b > b".to_string()];
        run(&steps, dir.path()).unwrap();
        assert!(dir.path().join("a").exists());
        assert!(dir.path().join("b").exists());
    }

    #[test]
    fn run_with_no_steps_is_ok() {
        let dir = tempfile::tempdir().unwrap();
        let steps: Vec<String> = vec![];
        run(&steps, dir.path()).unwrap();
    }
}