obnam-benchmark 0.1.0

a backup program
Documentation
use std::path::{Path, PathBuf};
use std::process::Command;
use std::str::FromStr;
use tempfile::{tempdir, TempDir};

const OBNAM_URL: &str = "https://gitlab.com/obnam/obnam.git";

#[derive(Debug, Clone)]
pub enum WhichObnam {
    Installed,
    Built(String),
}

impl FromStr for WhichObnam {
    type Err = ObnamBuilderError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "installed" => Ok(WhichObnam::Installed),
            _ => Ok(WhichObnam::Built(s.to_string())),
        }
    }
}

pub fn which_obnam(s: &str) -> Result<WhichObnam, ObnamBuilderError> {
    WhichObnam::from_str(s)
}

#[derive(Debug, thiserror::Error)]
pub enum ObnamBuilderError {
    #[error("don't understand which Obnam to use: {0}")]
    Unknown(String),

    #[error("failed to create temporary directory for building Obnam: {0}")]
    TempDir(std::io::Error),

    #[error("failed to run client binary {0}: {1}")]
    Client(PathBuf, std::io::Error),

    #[error("failed to run git: {0}")]
    Git(std::io::Error),

    #[error("failed to run cargo: {0}")]
    Cargo(std::io::Error),
}

pub struct ObnamBuilder {
    #[allow(dead_code)]
    tempdir: TempDir,
    client: PathBuf,
    server: PathBuf,
    commit: Option<String>,
}

impl ObnamBuilder {
    pub fn new(which: &WhichObnam) -> Result<Self, ObnamBuilderError> {
        let tempdir = tempdir().map_err(ObnamBuilderError::TempDir)?;
        let builder = match which {
            WhichObnam::Installed => Self {
                tempdir,
                client: Path::new("/usr/bin/obnam").to_path_buf(),
                server: Path::new("/usr/bin/obnam-server").to_path_buf(),
                commit: None,
            },
            WhichObnam::Built(commit) => {
                let (commit, bin) = build_obnam(tempdir.path(), commit)?;
                let client = bin.join("obnam");
                let server = bin.join("obnam-server");
                assert!(client.exists());
                assert!(server.exists());
                Self {
                    tempdir,
                    client,
                    server,
                    commit: Some(commit),
                }
            }
        };
        Ok(builder)
    }

    pub fn version(&self) -> Result<String, ObnamBuilderError> {
        let binary = self.client_binary();
        let output = Command::new(binary)
            .arg("--version")
            .output()
            .map_err(|err| ObnamBuilderError::Client(binary.to_path_buf(), err))?;
        if output.status.code() != Some(0) {
            eprintln!("{}", String::from_utf8_lossy(&output.stdout));
            eprintln!("{}", String::from_utf8_lossy(&output.stderr));
            std::process::exit(1);
        }

        let v = String::from_utf8_lossy(&output.stdout);
        let v = v.strip_suffix('\n').or(Some(&v)).unwrap().to_string();
        Ok(v)
    }

    pub fn commit(&self) -> Option<String> {
        self.commit.clone()
    }

    pub fn client_binary(&self) -> &Path {
        &self.client
    }

    pub fn server_binary(&self) -> &Path {
        &self.server
    }
}

fn build_obnam(dir: &Path, commit: &str) -> Result<(String, PathBuf), ObnamBuilderError> {
    let src = dir.join("git");
    let bin = dir.join("bin");
    git_clone(OBNAM_URL, &src)?;
    git_create_branch(&src, "build", commit)?;
    let commit = git_resolve(&src, commit)?;
    cargo_build(&src)?;
    cargo_install(&src, dir)?;
    Ok((commit, bin))
}

fn git_clone(url: &str, dir: &Path) -> Result<(), ObnamBuilderError> {
    eprintln!("cloning {} to {}", url, dir.display());
    run(
        "git",
        &["clone", url, &dir.display().to_string()],
        Path::new("."),
    )
    .map_err(ObnamBuilderError::Git)
    .map(|_| ())
}

fn git_create_branch(dir: &Path, branch: &str, commit: &str) -> Result<(), ObnamBuilderError> {
    eprintln!("checking out {}", commit);
    run("git", &["checkout", "-b", branch, commit], dir)
        .map_err(ObnamBuilderError::Git)
        .map(|_| ())
}

fn git_resolve(dir: &Path, commit: &str) -> Result<String, ObnamBuilderError> {
    run("git", &["rev-parse", commit], dir)
        .map(|s| s.strip_suffix('\n').or(Some("")).unwrap().to_string())
        .map_err(ObnamBuilderError::Git)
}

fn cargo_build(dir: &Path) -> Result<(), ObnamBuilderError> {
    eprintln!("building in {}", dir.display());
    run("cargo", &["build", "--release"], dir)
        .map_err(ObnamBuilderError::Git)
        .map(|_| ())
}

fn cargo_install(src: &Path, bin: &Path) -> Result<(), ObnamBuilderError> {
    eprintln!("install to {}", bin.display());
    run(
        "cargo",
        &["install", "--path=.", "--root", &bin.display().to_string()],
        src,
    )
    .map_err(ObnamBuilderError::Git)
    .map(|_| ())
}

fn run(cmd: &str, args: &[&str], cwd: &Path) -> Result<String, std::io::Error> {
    let output = Command::new(cmd).args(args).current_dir(cwd).output()?;

    if output.status.code() != Some(0) {
        eprintln!("{}", String::from_utf8_lossy(&output.stdout));
        eprintln!("{}", String::from_utf8_lossy(&output.stderr));
        std::process::exit(1);
    }

    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
    Ok(stdout)
}