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    #[tracing::instrument(level = "trace", skip_all)]
42    pub fn search(self) -> Result<PathBuf, WhichError>
43    where
44        State: which_builder::IsComplete,
45    {
46        do_search(self._build())
47    }
48}
49
50#[derive(Error, Debug, Diagnostic)]
51#[non_exhaustive]
52pub enum WhichError {
53    #[error(transparent)]
54    Io(#[from] io::Error),
55    #[error(transparent)]
56    #[diagnostic(transparent)]
57    Tree(#[from] TreeError),
58    #[error(transparent)]
59    #[diagnostic(transparent)]
60    LuaVersionUnset(#[from] LuaVersionUnset),
61    #[error("lua module {0} not found.")]
62    ModuleNotFound(LuaModule),
63}
64
65fn do_search(which: Which<'_>) -> Result<PathBuf, WhichError> {
66    let config = which.config;
67    let lua_version = LuaVersion::from(config)?;
68    let tree = config.user_tree(lua_version.clone())?;
69    let lockfile = tree.lockfile()?;
70    let local_packages = if which.packages.is_empty() {
71        lockfile.list().into_values().flatten().collect_vec()
72    } else {
73        which
74            .packages
75            .iter()
76            .flat_map(|req| {
77                lockfile
78                    .find_rocks(req)
79                    .into_iter()
80                    .filter_map(|id| lockfile.get(&id))
81                    .cloned()
82                    .collect_vec()
83            })
84            .collect_vec()
85    };
86    local_packages
87        .into_iter()
88        .filter_map(|pkg| {
89            let layout = tree.layout_for(&pkg.spec);
90            let lib_path = layout.lib.join(which.module.to_lib_path());
91            if lib_path.is_file() {
92                return Some(lib_path);
93            }
94            let lua_path = layout.src.join(which.module.to_lua_path());
95            if lua_path.is_file() {
96                return Some(lua_path);
97            }
98            let lua_path = layout.src.join(which.module.to_lua_init_path());
99            if lua_path.is_file() {
100                return Some(lua_path);
101            }
102            let luau_path = layout.src.join(which.module.to_luau_path());
103            if luau_path.is_file() {
104                return Some(luau_path);
105            }
106            let luau_init_path = layout.src.join(which.module.to_luau_init_path());
107            if luau_init_path.is_file() {
108                return Some(luau_init_path);
109            }
110            None
111        })
112        .next()
113        .ok_or(WhichError::ModuleNotFound(which.module))
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::config::ConfigBuilder;
120    use assert_fs::prelude::PathCopy;
121    use std::{path::PathBuf, str::FromStr};
122
123    #[tokio::test]
124    async fn test_which() {
125        let tree_path =
126            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
127        let temp = assert_fs::TempDir::new().unwrap();
128        temp.copy_from(&tree_path, &["**"]).unwrap();
129        let tree_path = temp.to_path_buf();
130        let config = ConfigBuilder::new()
131            .unwrap()
132            .user_tree(Some(tree_path.clone()))
133            .lua_version(Some(LuaVersion::Lua51))
134            .build()
135            .unwrap();
136
137        let result = Which::new(LuaModule::from_str("foo.bar").unwrap(), &config)
138            .search()
139            .unwrap();
140        assert_eq!(result.file_name().unwrap().to_string_lossy(), "bar.lua");
141        assert_eq!(
142            result
143                .parent()
144                .unwrap()
145                .file_name()
146                .unwrap()
147                .to_string_lossy(),
148            "foo"
149        );
150        let result = Which::new(LuaModule::from_str("bat.baz").unwrap(), &config)
151            .search()
152            .unwrap();
153        assert_eq!(result.file_name().unwrap().to_string_lossy(), "baz.so");
154        assert_eq!(
155            result
156                .parent()
157                .unwrap()
158                .file_name()
159                .unwrap()
160                .to_string_lossy(),
161            "bat"
162        );
163        let result = Which::new(LuaModule::from_str("hello").unwrap(), &config)
164            .search()
165            .unwrap();
166        assert_eq!(result.file_name().unwrap().to_string_lossy(), "hello.luau");
167        let result = Which::new(LuaModule::from_str("extra").unwrap(), &config)
168            .search()
169            .unwrap();
170        assert_eq!(result.file_name().unwrap().to_string_lossy(), "init.luau");
171        let result = Which::new(LuaModule::from_str("foo.bar").unwrap(), &config)
172            .package("lua-cjson".parse().unwrap())
173            .search();
174        assert!(matches!(result, Err(WhichError::ModuleNotFound(_))));
175        let result = Which::new(LuaModule::from_str("foo.bar").unwrap(), &config)
176            .package("neorg@8.1.1-1".parse().unwrap())
177            .search();
178        assert!(matches!(result, Err(WhichError::ModuleNotFound(_))));
179    }
180}