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#[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}