dolly-cli 0.1.1

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

use thiserror::Error;

#[derive(Debug, Error)]
pub enum BinaryError {
    #[error("build output not found at `{0}`; check `[build].output` in your recipe")]
    OutputMissing(PathBuf),

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

pub fn place(src: &Path, dest: &Path) -> Result<(), BinaryError> {
    if !src.exists() {
        return Err(BinaryError::OutputMissing(src.to_path_buf()));
    }

    if let Some(parent) = dest.parent() {
        std::fs::create_dir_all(parent)?;
    }

    std::fs::copy(src, dest)?;
    Ok(())
}

pub fn make_executable(path: &Path) -> Result<(), BinaryError> {
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = std::fs::metadata(path)?.permissions();
        perms.set_mode(0o755);
        std::fs::set_permissions(path, perms)?;
    }
    Ok(())
}

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

    #[test]
    fn place_copies_file_contents() {
        let dir = tempfile::tempdir().unwrap();
        let src = dir.path().join("src.bin");
        let dest = dir.path().join("subdir").join("dest.bin");

        std::fs::write(&src, b"hello world").unwrap();
        place(&src, &dest).unwrap();

        let contents = std::fs::read(&dest).unwrap();
        assert_eq!(contents, b"hello world");
    }

    #[test]
    fn place_creates_missing_parent_dir() {
        let dir = tempfile::tempdir().unwrap();
        let src = dir.path().join("src.bin");
        let dest = dir.path().join("nested").join("dirs").join("dest.bin");

        std::fs::write(&src, b"x").unwrap();
        place(&src, &dest).unwrap();

        assert!(dest.exists());
    }

    #[test]
    fn place_overwrites_existing_dest() {
        let dir = tempfile::tempdir().unwrap();
        let src = dir.path().join("src.bin");
        let dest = dir.path().join("dest.bin");

        std::fs::write(&src, b"new").unwrap();
        std::fs::write(&dest, b"old").unwrap();
        place(&src, &dest).unwrap();

        assert_eq!(std::fs::read(&dest).unwrap(), b"new");
    }

    #[test]
    fn place_returns_output_missing_when_src_absent() {
        let dir = tempfile::tempdir().unwrap();
        let src = dir.path().join("missing.bin");
        let dest = dir.path().join("dest.bin");

        let err = place(&src, &dest).unwrap_err();
        assert!(matches!(err, BinaryError::OutputMissing(p) if p == src));
    }

    #[cfg(unix)]
    #[test]
    fn make_executable_sets_mode_755() {
        use std::os::unix::fs::PermissionsExt;

        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("bin");
        std::fs::write(&path, b"x").unwrap();

        let mut perms = std::fs::metadata(&path).unwrap().permissions();
        perms.set_mode(0o644);
        std::fs::set_permissions(&path, perms).unwrap();

        make_executable(&path).unwrap();

        let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
        assert_eq!(mode, 0o755);
    }
}