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