Skip to main content

lux_lib/which/
mod.rs

1use std::{io, path::PathBuf};
2
3use crate::{
4    config::Config,
5    lua_rockspec::LuaModule,
6    lua_version::{LuaVersion, LuaVersionUnset},
7    package::PackageReq,
8    tree::{InstallTree, TreeError},
9};
10use bon::Builder;
11use itertools::Itertools;
12use miette::Diagnostic;
13use thiserror::Error;
14
15/// A rocks module finder.
16#[derive(Builder)]
17#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
18pub struct Which<'a> {
19    #[builder(start_fn)]
20    module: LuaModule,
21    #[builder(start_fn)]
22    config: &'a Config,
23    #[builder(field)]
24    packages: Vec<PackageReq>,
25}
26
27impl<State> WhichBuilder<'_, State>
28where
29    State: which_builder::State,
30{
31    pub fn package(mut self, package: PackageReq) -> Self {
32        self.packages.push(package);
33        self
34    }
35
36    pub fn packages(mut self, packages: impl IntoIterator<Item = PackageReq>) -> Self {
37        self.packages.extend(packages);
38        self
39    }
40
41    pub fn search(self) -> Result<PathBuf, WhichError>
42    where
43        State: which_builder::IsComplete,
44    {
45        do_search(self._build())
46    }
47}
48
49#[derive(Error, Debug, Diagnostic)]
50pub enum WhichError {
51    #[error(transparent)]
52    Io(#[from] io::Error),
53    #[error(transparent)]
54    #[diagnostic(transparent)]
55    Tree(#[from] TreeError),
56    #[error(transparent)]
57    #[diagnostic(transparent)]
58    LuaVersionUnset(#[from] LuaVersionUnset),
59    #[error("lua module {0} not found.")]
60    ModuleNotFound(LuaModule),
61}
62
63fn do_search(which: Which<'_>) -> Result<PathBuf, WhichError> {
64    let config = which.config;
65    let lua_version = LuaVersion::from(config)?;
66    let tree = config.user_tree(lua_version.clone())?;
67    let lockfile = tree.lockfile()?;
68    let local_packages = if which.packages.is_empty() {
69        lockfile.list().into_values().flatten().collect_vec()
70    } else {
71        which
72            .packages
73            .iter()
74            .flat_map(|req| {
75                lockfile
76                    .find_rocks(req)
77                    .into_iter()
78                    .filter_map(|id| lockfile.get(&id))
79                    .cloned()
80                    .collect_vec()
81            })
82            .collect_vec()
83    };
84    local_packages
85        .into_iter()
86        .filter_map(|pkg| {
87            let rock_layout = tree.installed_rock_layout(&pkg).ok()?;
88            let lib_path = rock_layout.lib.join(which.module.to_lib_path());
89            if lib_path.is_file() {
90                return Some(lib_path);
91            }
92            let lua_path = rock_layout.src.join(which.module.to_lua_path());
93            if lua_path.is_file() {
94                return Some(lua_path);
95            }
96            let lua_path = rock_layout.src.join(which.module.to_lua_init_path());
97            if lua_path.is_file() {
98                return Some(lua_path);
99            }
100            None
101        })
102        .next()
103        .ok_or(WhichError::ModuleNotFound(which.module))
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::config::ConfigBuilder;
110    use assert_fs::prelude::PathCopy;
111    use std::{path::PathBuf, str::FromStr};
112
113    #[tokio::test]
114    async fn test_which() {
115        let tree_path =
116            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
117        let temp = assert_fs::TempDir::new().unwrap();
118        temp.copy_from(&tree_path, &["**"]).unwrap();
119        let tree_path = temp.to_path_buf();
120        let config = ConfigBuilder::new()
121            .unwrap()
122            .user_tree(Some(tree_path.clone()))
123            .lua_version(Some(LuaVersion::Lua51))
124            .build()
125            .unwrap();
126
127        let result = Which::new(LuaModule::from_str("foo.bar").unwrap(), &config)
128            .search()
129            .unwrap();
130        assert_eq!(result.file_name().unwrap().to_string_lossy(), "bar.lua");
131        assert_eq!(
132            result
133                .parent()
134                .unwrap()
135                .file_name()
136                .unwrap()
137                .to_string_lossy(),
138            "foo"
139        );
140        let result = Which::new(LuaModule::from_str("bat.baz").unwrap(), &config)
141            .search()
142            .unwrap();
143        assert_eq!(result.file_name().unwrap().to_string_lossy(), "baz.so");
144        assert_eq!(
145            result
146                .parent()
147                .unwrap()
148                .file_name()
149                .unwrap()
150                .to_string_lossy(),
151            "bat"
152        );
153        let result = Which::new(LuaModule::from_str("foo.bar").unwrap(), &config)
154            .package("lua-cjson".parse().unwrap())
155            .search();
156        assert!(matches!(result, Err(WhichError::ModuleNotFound(_))));
157        let result = Which::new(LuaModule::from_str("foo.bar").unwrap(), &config)
158            .package("neorg@8.1.1-1".parse().unwrap())
159            .search();
160        assert!(matches!(result, Err(WhichError::ModuleNotFound(_))));
161    }
162}