Skip to main content

evoke/
local.rs

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