Skip to main content

evoke/
main.rs

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