Skip to main content

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