evoke/
main.rs

1//! Format, lint, build, and deploy workspace binaries and Nvim libs.
2//!
3//! # Arguments
4//! - `--debug` Use debug profile; skip clippy; copy from `target/debug`.
5//! - `bins_path` Destination for binaries (default: `$HOME/.local/bin`).
6//! - `cargo_target_path` Cargo target root (default: workspace `target/`).
7//! - `nvim_libs_path` Destination for Neovim libs (default: `$HOME/.config/nvim/lua`).
8//!
9//! # Usage
10//! ```bash
11//! evoke # fmt + clippy + build --release, copy bins & libs
12//! evoke --debug # fmt + build debug (skip clippy) copy from target/debug
13//! evoke --debug "$HOME/.local/bin" "$PWD/target" "$HOME/.config/nvim/lua"
14//! ```
15//!
16//! # Errors
17//! - Resolving required environment variables fails or yields invalid Unicode.
18//! - Running `cargo fmt`, `cargo clippy`, or `cargo build` fails.
19//! - Copying a binary or library fails.
20#![feature(exit_status_error)]
21
22use std::path::Path;
23use std::path::PathBuf;
24
25use color_eyre::owo_colors::OwoColorize;
26use ytil_sys::cli::Args;
27
28/// List of binaries that should be copied after building.
29/// NOTE: if a new binary is added this list must be updated!
30const BINS: &[&str] = &[
31    "idt", "catl", "fkr", "gch", "gcu", "ghl", "oe", "rmr", "tec", "try", "vpg", "yghfl", "yhfp",
32];
33/// List of library files that need to be renamed after building, mapping (`source_name`, `target_name`).
34const LIBS: &[(&str, &str)] = &[("libnvrim.dylib", "nvrim.so")];
35/// Path segments for the default binaries install dir.
36const BINS_DEFAULT_PATH: &[&str] = &[".local", "bin"];
37/// Path segments for the Nvim libs install dir.
38const NVIM_LIBS_DEFAULT_PATH: &[&str] = &[".config", "nvim", "lua"];
39
40/// Removes the last `n` directories from a [`PathBuf`].
41fn remove_last_n_dirs(path: &mut PathBuf, n: usize) {
42    for _ in 0..n {
43        if !path.pop() {
44            return;
45        }
46    }
47}
48
49/// Removes the first occurrence of an element from a vector.
50/// Returns `true` if found and removed, `false` otherwise.
51fn drop_element<T, U: ?Sized>(vec: &mut Vec<T>, target: &U) -> bool
52where
53    T: PartialEq<U>,
54{
55    if let Some(idx) = vec.iter().position(|x| x == target) {
56        vec.swap_remove(idx);
57        return true;
58    }
59    false
60}
61
62/// Copies a built binary or library from `from` to `to` using
63/// [`ytil_sys::file::atomic_cp`] and prints an "Installed" status line.
64///
65/// # Errors
66/// - [`ytil_sys::file::atomic_cp`] fails to copy.
67/// - The final rename or write cannot be performed.
68fn cp(from: &Path, to: &Path) -> color_eyre::Result<()> {
69    ytil_sys::file::atomic_cp(from, to)?;
70    println!("{} {} to {}", "Copied".green().bold(), from.display(), to.display());
71    Ok(())
72}
73
74/// Format, lint, build, and deploy workspace binaries and Nvim libs.
75fn main() -> color_eyre::Result<()> {
76    color_eyre::install()?;
77
78    let mut args = ytil_sys::cli::get();
79
80    if args.has_help() {
81        println!("{}", include_str!("../help.txt"));
82        return Ok(());
83    }
84
85    let is_debug = drop_element(&mut args, "--debug");
86    let bins_path = args.first().cloned().map_or_else(
87        || ytil_sys::dir::build_home_path(BINS_DEFAULT_PATH),
88        |supplied_bins_path| Ok(PathBuf::from(supplied_bins_path)),
89    )?;
90    let cargo_target_path = args.get(1).cloned().map_or_else(
91        || {
92            std::env::var("CARGO_MANIFEST_DIR").map(|cargo_manifest_dir| {
93                let mut x = PathBuf::from(cargo_manifest_dir);
94                remove_last_n_dirs(&mut x, 2);
95                x.join("target")
96            })
97        },
98        |x| Ok(PathBuf::from(x)),
99    )?;
100    let nvim_libs_path = args.get(2).cloned().map_or_else(
101        || ytil_sys::dir::build_home_path(NVIM_LIBS_DEFAULT_PATH),
102        |supplied_nvim_libs_path| Ok(PathBuf::from(supplied_nvim_libs_path)),
103    )?;
104
105    let (cargo_target_location, build_profile) = if is_debug {
106        (cargo_target_path.join("debug"), None)
107    } else {
108        (cargo_target_path.join("release"), Some("--release"))
109    };
110
111    ytil_cmd::silent_cmd("cargo").args(["fmt"]).status()?.exit_ok()?;
112
113    // Skip clippy if debugging
114    if !is_debug {
115        ytil_cmd::silent_cmd("cargo")
116            .args(["clippy", "--all-targets", "--all-features", "--", "-D", "warnings"])
117            .status()?
118            .exit_ok()?;
119    }
120
121    ytil_cmd::silent_cmd("cargo")
122        .args([Some("build"), build_profile].into_iter().flatten())
123        .status()?
124        .exit_ok()?;
125
126    for bin in BINS {
127        cp(&cargo_target_location.join(bin), &bins_path.join(bin))?;
128    }
129
130    for (source_lib_name, target_lib_name) in LIBS {
131        cp(
132            &cargo_target_location.join(source_lib_name),
133            &nvim_libs_path.join(target_lib_name),
134        )?;
135    }
136
137    Ok(())
138}
139
140#[cfg(test)]
141mod tests {
142    use rstest::rstest;
143
144    use super::*;
145
146    #[test]
147    fn drop_element_returns_true_and_removes_the_element_from_the_vec() {
148        let mut input = vec![42, 7];
149        assert!(drop_element(&mut input, &7));
150        assert_eq!(input, vec![42]);
151    }
152
153    #[test]
154    fn drop_element_returns_false_and_does_nothing_to_a_non_empty_vec() {
155        let mut input = vec![42, 7];
156        assert!(!drop_element(&mut input, &3));
157        assert_eq!(input, vec![42, 7]);
158    }
159
160    #[test]
161    fn drop_element_returns_false_and_does_nothing_to_an_empty_vec() {
162        let mut input: Vec<usize> = vec![];
163        assert!(!drop_element(&mut input, &3));
164        assert!(input.is_empty());
165    }
166
167    #[rstest]
168    #[case::no_dirs_removed(PathBuf::from("/home/user/docs"), 0, PathBuf::from("/home/user/docs"))]
169    #[case::remove_one_dir(PathBuf::from("/home/user/docs"), 1, PathBuf::from("/home/user"))]
170    #[case::remove_more_than_exist(PathBuf::from("/home/user"), 5, PathBuf::from("/"))]
171    #[case::root_path(PathBuf::from("/"), 1, PathBuf::from("/"))]
172    #[case::empty_path(PathBuf::new(), 1, PathBuf::new())]
173    fn remove_last_n_dirs_works(#[case] mut initial: PathBuf, #[case] n: usize, #[case] expected: PathBuf) {
174        remove_last_n_dirs(&mut initial, n);
175        pretty_assertions::assert_eq!(initial, expected);
176    }
177}