cargo-temp 0.2.9

A CLI tool that allow you to create a temporary new rust project using cargo with already installed dependencies
use anyhow::{bail, ensure, Context, Result};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::{env, fs};
use tempfile::TempDir;

use crate::{
    config::{Config, Depth},
    dependency::{format_dependency, Dependency},
    Cli,
};

pub fn execute(cli: Cli, config: Config) -> Result<()> {
    let tmp_dir = generate_tmp_project(
        cli.clone(),
        config.temporary_project_dir.clone(),
        config.git_repo_depth.clone(),
        config.vcs.clone(),
    )?;

    add_dependencies_to_project(tmp_dir.path(), &cli.dependencies)?;

    if let Some(maybe_bench_name) = &cli.bench {
        generate_benchmarking(tmp_dir.path(), maybe_bench_name.clone())?;
    }

    let delete_file = generate_delete_file(tmp_dir.path())?;

    let subprocesses = start_subprocesses(&config, tmp_dir.path());

    let res = start_shell(&config, tmp_dir.path());

    clean_up(delete_file, tmp_dir, cli.worktree_branch, subprocesses)?;

    ensure!(res.is_ok(), "problem within the shell process");

    Ok(())
}

pub fn generate_tmp_project(
    cli: Cli,
    temporary_project_dir: PathBuf,
    git_repo_depth: Option<Depth>,
    vcs: Option<String>,
) -> Result<TempDir> {
    let tmp_dir = {
        let mut builder = tempfile::Builder::new();

        if cli.worktree_branch.is_some() {
            builder.prefix("wk-");
        } else {
            builder.prefix("tmp-");
        }

        builder.tempdir_in(temporary_project_dir)?
    };

    let project_name = cli.project_name.unwrap_or_else(|| {
        tmp_dir
            .path()
            .file_name()
            .unwrap()
            .to_string_lossy()
            .to_lowercase()
    });

    if let Some(maybe_branch) = cli.worktree_branch.as_ref() {
        let mut command = std::process::Command::new("git");
        command.args(["worktree", "add"]);

        match maybe_branch {
            Some(branch) => command.arg(tmp_dir.path()).arg(branch),
            None => command.arg("-d").arg(tmp_dir.path()),
        };

        ensure!(
            command.status().context("Could not start git")?.success(),
            "cannot create working tree"
        );
    } else if let Some(url) = cli.git {
        let mut command = std::process::Command::new("git");
        command.arg("clone").arg(url).arg(&tmp_dir.as_ref());

        match git_repo_depth {
            Some(Depth::Active(false)) => {}
            None | Some(Depth::Active(true)) => {
                command.arg("--depth").arg("1");
            }
            Some(Depth::Level(level)) => {
                command.arg("--depth").arg(level.to_string());
            }
        };

        ensure!(
            command.status().context("Could not start git")?.success(),
            "cannot clone repository"
        );
    } else {
        let mut command = std::process::Command::new("cargo");
        command
            .current_dir(&tmp_dir)
            .args(["init", "--name", project_name.as_str()]);

        if cli.lib {
            command.arg("--lib");
        }

        if let Some(arg) = vcs {
            command.args(["--vcs", arg.as_str()]);
        }

        if let Some(num) = cli.edition {
            match num {
                15 | 2015 => {
                    command.args(["--edition", "2015"]);
                }
                18 | 2018 => {
                    command.args(["--edition", "2018"]);
                }
                21 | 2021 => {
                    command.args(["--edition", "2021"]);
                }
                _ => log::error!("cannot find the {} edition, using the latest", num),
            }
        }

        ensure!(
            command.status().context("Could not start cargo")?.success(),
            "cargo command failed"
        );
    }

    Ok(tmp_dir)
}

pub fn add_dependencies_to_project(tmp_dir: &Path, dependencies: &[Dependency]) -> Result<()> {
    let mut toml = fs::OpenOptions::new()
        .append(true)
        .open(tmp_dir.join("Cargo.toml"))?;

    for dependency in dependencies.iter() {
        writeln!(toml, "{}", format_dependency(dependency))?
    }

    Ok(())
}

fn generate_benchmarking(tmp_dir: &Path, maybe_name: Option<String>) -> Result<()> {
    let name = if let Some(name) = maybe_name {
        name
    } else {
        "benchmark".to_string()
    };

    let mut toml = fs::OpenOptions::new()
        .append(true)
        .open(tmp_dir.join("Cargo.toml"))?;

    writeln!(toml, "{}", format_benchmarking(&name))?;

    let bench_folder = tmp_dir.join("benches");
    fs::create_dir_all(&bench_folder)?;
    let mut bench_file = bench_folder.join(name);
    bench_file.set_extension("rs");

    fs::write(
        bench_file,
        "use criterion::{black_box, criterion_group, criterion_main, Criterion};\n\n\
        fn criterion_benchmark(_c: &mut Criterion) {\n\tprintln!(\"Hello, world!\");\n}\n\n\
        criterion_group!(\n\tbenches,\n\tcriterion_benchmark\n);\ncriterion_main!(benches);",
    )?;

    Ok(())
}

fn format_benchmarking(name: &str) -> String {
    format!(
        "
[dev-dependencies]
criterion = \"*\"

[profile.release]
debug = true

[[bench]]
name = \"{}\"
harness = false",
        name
    )
}

pub fn generate_delete_file(tmp_dir: &Path) -> Result<PathBuf> {
    let delete_file = tmp_dir.join("TO_DELETE");
    fs::write(
        &delete_file,
        "Delete this file if you want to preserve this project",
    )?;

    Ok(delete_file)
}

pub fn start_shell(config: &Config, tmp_dir: &Path) -> Result<std::process::ExitStatus> {
    let mut shell_process = match config.editor {
        None => std::process::Command::new(get_shell()),
        Some(ref editor) => {
            let mut ide_process = std::process::Command::new(editor);
            ide_process
                .args(config.editor_args.iter().flatten())
                .arg(tmp_dir);
            ide_process
        }
    };

    if env::var("CARGO_TARGET_DIR").is_err() {
        if let Some(path) = &config.cargo_target_dir {
            env::set_var("CARGO_TARGET_DIR", path);
        }
    }

    let res = shell_process.current_dir(&tmp_dir).spawn();

    #[cfg(windows)]
    if config.editor.is_some() {
        unsafe {
            windows_sys::Win32::System::Console::FreeConsole();
        }
    }

    if let Ok(mut child) = res {
        child.wait().context("cannot wait shell process")
    } else {
        bail!("cannot spawn shell process")
    }
}

#[cfg(unix)]
type Child = std::process::Child;
#[cfg(windows)]
type Child = create_process_w::Child;

pub fn start_subprocesses(config: &Config, tmp_dir: &Path) -> Vec<Child> {
    config
        .subprocesses
        .iter()
        .filter_map(|x| x.spawn(tmp_dir))
        .collect::<Vec<Child>>()
}

pub fn clean_up(
    delete_file: PathBuf,
    tmp_dir: TempDir,
    worktree_branch: Option<Option<String>>,
    mut subprocesses: Vec<Child>,
) -> Result<()> {
    if !delete_file.exists() {
        println!(
            "Project directory preserved at: {}",
            tmp_dir.into_path().display()
        );
    } else if worktree_branch.is_some() {
        let mut command = std::process::Command::new("git");
        command
            .args(["worktree", "remove"])
            .arg(&tmp_dir.path())
            .arg("--force");
        ensure!(
            command.status().context("Could not start git")?.success(),
            "cannot remove working tree"
        );
    }

    #[cfg(unix)]
    {
        for subprocess in subprocesses.iter_mut() {
            {
                unsafe {
                    libc::kill(
                        subprocess
                            .id()
                            .try_into()
                            .context("cannot get process id")?,
                        libc::SIGTERM,
                    );
                }
            }
        }

        std::thread::sleep(std::time::Duration::from_secs(2));
    }

    for subprocess in subprocesses.iter_mut() {
        match subprocess.try_wait() {
            Ok(Some(_)) => {}
            _ => {
                let _ = subprocess.kill();
                let _ = subprocess.wait();
            }
        }
    }

    Ok(())
}

pub fn get_shell() -> String {
    #[cfg(unix)]
    {
        env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
    }

    #[cfg(windows)]
    {
        env::var("COMSPEC").unwrap_or_else(|_| "cmd".to_string())
    }
}