parabuild 0.3.2

A parallel build utility for template heavy projects.
Documentation
use fs_extra;
use ignore;
use std::{path::Path, process::Command};

pub fn copy_dir<P, Q>(from: P, to: Q) -> Result<(), fs_extra::error::Error>
where
    P: AsRef<Path>,
    Q: AsRef<Path>,
{
    fs_extra::dir::create_all(&to, false)?;
    let options = fs_extra::dir::CopyOptions::new()
        .overwrite(true)
        .copy_inside(true)
        .content_only(true);
    fs_extra::dir::copy(from, to, &options)?;
    Ok(())
}

pub fn copy_dir_with_ignore<P, Q>(from: P, to: Q) -> Result<(), std::io::Error>
where
    P: AsRef<Path>,
    Q: AsRef<Path>,
{
    for entry in ignore::WalkBuilder::new(&from).git_ignore(true).build() {
        match entry {
            Ok(ref entry) => {
                let path = entry.path();
                if path.is_file() {
                    let relative_path = path
                        .strip_prefix(from.as_ref())
                        .expect("Failed to strip prefix");
                    let destination = to.as_ref().join(relative_path);
                    if let Some(parent) = destination.parent() {
                        std::fs::create_dir_all(parent).expect("Failed to create parent directory");
                    }
                    std::fs::copy(path, destination).expect("Failed to copy file");
                }
            }
            Err(e) => {
                eprintln!("Error: {}", e);
            }
        }
    }
    Ok(())
}

pub fn copy_dir_with_rsync(from: &Path, to: &Path) -> Result<(), std::io::Error> {
    let from_ends_with_slash = if from.ends_with("/") {
        from.to_str().unwrap().to_string()
    } else {
        format!("{}/", from.to_str().unwrap())
    };
    let to_ends_with_slash = if to.ends_with("/") {
        to.to_str().unwrap().to_string()
    } else {
        format!("{}/", to.to_str().unwrap())
    };
    let gitignore_file = from.join(".gitignore");
    let mut output = Command::new("rsync");
    output.arg("-a");
    if gitignore_file.exists() {
        output.arg(format!(
            "--exclude-from={}",
            gitignore_file.to_str().unwrap()
        ));
    }
    let output = output
        .arg(from_ends_with_slash)
        .arg(to_ends_with_slash)
        .output()?;
    if !output.status.success() {
        return Err(std::io::Error::new(
            std::io::ErrorKind::Other,
            format!("Failed to copy directory: {:?}", output),
        ));
    }
    Ok(())
}

pub fn wait_until_file_ready(file_path: &Path) -> Result<(), std::io::Error> {
    use std::thread::sleep;
    use std::time::Duration;
    let mut attempts = 0;
    fn ready(file_path: &Path) -> bool {
        if !file_path.exists() {
            return false;
        }
        let output = Command::new("lsof").arg(file_path).output().unwrap();
        if output.stdout.is_empty() {
            return true;
        }
        eprintln!(
            "Waiting for file to be ready: {:?}, {:?}",
            file_path, output.stdout
        );
        false
    }
    while !ready(file_path) {
        attempts += 1;
        if attempts > 100 {
            if !file_path.exists() {
                return Err(std::io::Error::new(
                    std::io::ErrorKind::NotFound,
                    format!("File not found: {:?}", file_path),
                ));
            } else {
                return Err(std::io::Error::new(
                    std::io::ErrorKind::Other,
                    format!("File is not ready: {:?}", file_path),
                ));
            }
        }
        sleep(Duration::from_millis(100));
    }
    Ok(())
}

pub fn is_command_installed(command: &str) -> bool {
    Command::new(command).arg("--version").output().is_ok()
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::tempdir;

    const EXAMPLE_PROJECT: &str = crate::test_constants::EXAMPLE_CMAKE_PROJECT_PATH;

    #[test]
    fn test_copy_dir() {
        let source = Path::new(EXAMPLE_PROJECT);
        let destination = &tempdir().unwrap().into_path();
        println!("source: {:?}", source);
        println!("destination: {:?}", destination);
        copy_dir(source, destination).unwrap();
        let main_file = destination.join("src/main.cpp.template");
        let ignore_file = destination.join("src/example.ignore");
        let gitignore_file = destination.join(".gitignore");
        assert!(main_file.exists());
        assert!(ignore_file.exists());
        assert!(gitignore_file.exists());
        std::fs::remove_dir_all(destination).unwrap();
    }

    #[test]
    fn test_copy_dir_with_ignore() {
        let source = Path::new(EXAMPLE_PROJECT);
        let destination = &tempdir().unwrap().into_path();
        println!("destination: {:?}", destination);
        fs_extra::dir::remove(destination).unwrap();
        copy_dir_with_ignore(source, destination).unwrap();
        let main_file = destination.join("src/main.cpp.template");
        let ignore_file = destination.join("src/example.ignore");
        let gitignore_file = destination.join(".gitignore");
        assert!(main_file.exists());
        assert!(!ignore_file.exists());
        assert!(!gitignore_file.exists());
        std::fs::remove_dir_all(destination).unwrap();
    }

    #[test]
    fn test_is_command_installed() {
        assert!(is_command_installed("ls"));
        assert!(!is_command_installed("ls_not_exist"));
    }

    #[test]
    fn test_copy_dir_with_rsync() {
        fn get_mtime(path: &Path) -> std::io::Result<std::time::SystemTime> {
            std::fs::metadata(path).map(|meta| meta.modified())?
        }
        let example_project_dir = Path::new(crate::test_constants::EXAMPLE_CMAKE_PROJECT_PATH);
        let working_dir = tempdir().unwrap().into_path();
        copy_dir(example_project_dir, &working_dir).unwrap();
        let ignore_path = working_dir.join("src/example.ignore");
        assert!(ignore_path.exists());
        let file_path = working_dir.join("src/example.cpp");
        let main_path = working_dir.join("src/main.cpp");
        let main_old_mtime = get_mtime(&main_path).unwrap();
        let mut file = std::fs::File::create(&file_path).unwrap();
        write!(file, "Hello, ").unwrap();
        file.sync_all().unwrap();
        let destination = tempdir().unwrap().into_path();
        copy_dir_with_rsync(&working_dir, &destination).unwrap();
        let ignore_destination = destination.join("src/example.ignore");
        let file_destination = destination.join("src/example.cpp");
        let main_destination = destination.join("src/main.cpp");
        assert!(!ignore_destination.exists());
        assert!(file_destination.exists());
        writeln!(file, "world!").unwrap();
        file.sync_all().unwrap();
        copy_dir_with_rsync(&working_dir, &destination).unwrap();
        assert_eq!(
            std::fs::read_to_string(file_destination).unwrap(),
            "Hello, world!\n"
        );
        assert_eq!(main_old_mtime, get_mtime(&main_destination).unwrap(),);
        std::fs::remove_dir_all(working_dir).unwrap();
        std::fs::remove_dir_all(destination).unwrap();
    }
}