1use crate::{
2 build::utils::c_dylib_extension,
3 config::Config,
4 lua_version::LuaVersion,
5 tree::{InstallTree, TreeError},
6};
7use itertools::Itertools;
8use miette::Diagnostic;
9use path_slash::PathBufExt;
10use serde::Serialize;
11use std::{env, fmt::Display, path::PathBuf, str::FromStr};
12use thiserror::Error;
13
14const LUA_PATH_SEPARATOR: &str = ";";
15const LUA_INIT: &str = "require('lux').loader()";
16
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
20pub enum LuaEnvironment {
21 #[default]
22 Pure,
23 Impure,
24}
25
26#[derive(PartialEq, Eq, Debug, Serialize)]
27pub struct Paths {
28 src: PackagePath,
30 lib: PackagePath,
32 bin: BinPath,
34
35 version: LuaVersion,
36}
37
38#[derive(Debug, Error, Diagnostic)]
39pub enum PathsError {
40 #[error(transparent)]
41 #[diagnostic(transparent)]
42 Tree(#[from] TreeError),
43}
44
45impl Paths {
46 fn default(tree: &impl InstallTree) -> Self {
47 Self {
48 src: <_>::default(),
49 lib: <_>::default(),
50 bin: <_>::default(),
51 version: tree.version().clone(),
52 }
53 }
54
55 pub fn new(tree: &impl InstallTree) -> Result<Self, PathsError> {
56 let mut paths = Self::default(tree);
57 for package in tree.list()?.values().flatten() {
58 let layout = tree.layout_for(&package.spec);
59 paths.src.0.push(layout.src.join("?.lua"));
60 paths.src.0.push(layout.src.join("?").join("init.lua"));
61 paths
62 .lib
63 .0
64 .push(layout.lib.join(format!("?.{}", c_dylib_extension())));
65 paths.bin.add_path(tree.bin());
66 }
67
68 if let Some(lib_path) = tree.version().lux_lib_dir() {
69 paths.prepend(&Paths {
70 version: tree.version().clone(),
71 src: <_>::default(),
72 bin: <_>::default(),
73 lib: PackagePath(vec![lib_path.join(format!("?.{}", c_dylib_extension()))]),
74 });
75 }
76
77 Ok(paths)
78 }
79
80 pub fn for_environment<T: InstallTree>(
81 tree: &T,
82 config: &Config,
83 environment: LuaEnvironment,
84 ) -> Result<Self, PathsError> {
85 let tree_paths = Self::new(tree)?;
86 match environment {
87 LuaEnvironment::Pure => Ok(tree_paths),
88 LuaEnvironment::Impure => {
89 let user_tree = config
90 .user_tree(tree.version().clone())
91 .map_err(PathsError::from)?;
92 let mut paths = Self::new(&user_tree)?;
93 paths.prepend(&tree_paths);
94 Ok(paths)
95 }
96 }
97 }
98
99 pub fn package_path(&self) -> &PackagePath {
101 &self.src
102 }
103
104 pub fn package_cpath(&self) -> &PackagePath {
106 &self.lib
107 }
108
109 pub fn package_path_prepended(&self) -> PackagePath {
111 let mut lua_path = PackagePath::from_str(env::var("LUA_PATH").unwrap_or_default().as_str())
112 .unwrap_or_default();
113 lua_path.prepend(self.package_path());
114 lua_path
115 }
116
117 pub fn package_cpath_prepended(&self) -> PackagePath {
119 let mut lua_cpath =
120 PackagePath::from_str(env::var("LUA_CPATH").unwrap_or_default().as_str())
121 .unwrap_or_default();
122 lua_cpath.prepend(self.package_cpath());
123 lua_cpath
124 }
125
126 pub fn lua_path(&self, environment: LuaEnvironment) -> String {
127 self.lua_package_path(self.package_path(), environment, "LUA_PATH")
128 }
129
130 pub fn lua_cpath(&self, environment: LuaEnvironment) -> String {
131 self.lua_package_path(self.package_cpath(), environment, "LUA_CPATH")
132 }
133
134 pub fn lua_env_vars(
135 &self,
136 environment: LuaEnvironment,
137 lua_init: &str,
138 ) -> Vec<(String, String)> {
139 let lua_path = self.lua_path(environment);
140 let lua_cpath = self.lua_cpath(environment);
141 vec![
142 ("LUA_PATH".into(), lua_path.clone()),
143 (self.versioned_env_var("LUA_PATH"), lua_path),
144 ("LUA_CPATH".into(), lua_cpath.clone()),
145 (self.versioned_env_var("LUA_CPATH"), lua_cpath),
146 ("LUA_INIT".into(), lua_init.to_string()),
147 (self.versioned_env_var("LUA_INIT"), lua_init.to_string()),
148 ]
149 }
150
151 pub fn version(&self) -> &LuaVersion {
153 &self.version
154 }
155
156 pub fn path(&self) -> &BinPath {
158 &self.bin
159 }
160
161 pub fn init(&self) -> String {
163 format!("if _VERSION:find('{}') then {LUA_INIT} end", self.version)
164 }
165
166 pub fn path_prepended(&self) -> BinPath {
168 let mut path = BinPath::from_env();
169 path.prepend(self.path());
170 path
171 }
172
173 pub fn prepend(&mut self, other: &Self) {
174 self.src.prepend(&other.src);
175 self.lib.prepend(&other.lib);
176 self.bin.prepend(&other.bin);
177 }
178}
179
180impl Paths {
181 fn lua_package_path(
182 &self,
183 tree: &PackagePath,
184 environment: LuaEnvironment,
185 var: &str,
186 ) -> String {
187 match environment {
188 LuaEnvironment::Pure => tree.joined(),
189 LuaEnvironment::Impure => {
190 let mut paths = self.inherited_package_path(var);
191 paths.prepend(tree);
192 format!("{};;", paths.joined())
193 }
194 }
195 }
196
197 fn inherited_package_path(&self, var: &str) -> PackagePath {
198 env::var(self.versioned_env_var(var))
199 .or_else(|_| env::var(var))
200 .ok()
201 .and_then(|value| PackagePath::from_str(&value).ok())
202 .unwrap_or_default()
203 }
204
205 fn versioned_env_var(&self, var: &str) -> String {
206 format!(
207 "{var}_{}",
208 self.version.version_compatibility_str().replace('.', "_")
209 )
210 }
211}
212
213#[cfg(test)]
214impl Paths {
215 pub(crate) fn test(version: LuaVersion, src: PackagePath, lib: PackagePath) -> Self {
216 Self {
217 src,
218 lib,
219 bin: BinPath::default(),
220 version,
221 }
222 }
223}
224
225#[derive(PartialEq, Eq, Debug, Default, Serialize, Clone)]
226pub struct PackagePath(Vec<PathBuf>);
227
228impl PackagePath {
229 fn prepend(&mut self, other: &Self) {
230 let mut new_vec = other.0.to_owned();
231 new_vec.append(&mut self.0);
232 self.0 = new_vec;
233 }
234 pub fn is_empty(&self) -> bool {
235 self.0.is_empty()
236 }
237 pub fn joined(&self) -> String {
238 self.0
239 .iter()
240 .unique()
241 .map(|path| path.to_slash_lossy())
242 .join(LUA_PATH_SEPARATOR)
243 }
244}
245
246impl FromStr for PackagePath {
247 type Err = &'static str;
248
249 fn from_str(s: &str) -> Result<Self, Self::Err> {
250 let paths = s
251 .trim_start_matches(LUA_PATH_SEPARATOR)
252 .trim_end_matches(LUA_PATH_SEPARATOR)
253 .split(LUA_PATH_SEPARATOR)
254 .map(PathBuf::from)
255 .collect();
256 Ok(PackagePath(paths))
257 }
258}
259
260impl Display for PackagePath {
261 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262 self.joined().fmt(f)
263 }
264}
265
266#[derive(PartialEq, Eq, Debug, Default, Serialize)]
267pub struct BinPath(Vec<PathBuf>);
268
269impl BinPath {
270 pub fn from_env() -> Self {
271 Self::from_str(env::var("PATH").unwrap_or_default().as_str()).unwrap_or_default()
272 }
273 pub fn prepend(&mut self, other: &Self) {
274 let mut new_vec = other.0.to_owned();
275 new_vec.append(&mut self.0);
276 self.0 = new_vec;
277 }
278 pub fn add_path(&mut self, path: PathBuf) {
282 if env::join_paths(vec![&path]).is_ok() {
283 let mut new_vec = Vec::new();
284 new_vec.push(path);
285 new_vec.append(&mut self.0);
286 self.0 = new_vec;
287 }
288 }
289 pub fn is_empty(&self) -> bool {
290 self.0.is_empty()
291 }
292 pub fn joined(&self) -> String {
296 env::join_paths(self.0.iter().unique())
297 .unwrap_or_default()
298 .to_string_lossy()
299 .to_string()
300 }
301}
302
303impl FromStr for BinPath {
304 type Err = &'static str;
305
306 fn from_str(s: &str) -> Result<Self, Self::Err> {
307 let paths = env::split_paths(s).collect();
308 Ok(BinPath(paths))
309 }
310}
311
312impl Display for BinPath {
313 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314 self.joined().fmt(f)
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321 use serial_test::serial;
322 use std::ffi::OsString;
323
324 struct EnvGuard {
325 key: &'static str,
326 previous: Option<OsString>,
327 }
328
329 impl EnvGuard {
330 fn set(key: &'static str, value: &str) -> Self {
331 let previous = env::var_os(key);
332 env::set_var(key, value);
333 Self { key, previous }
334 }
335 }
336
337 impl Drop for EnvGuard {
338 fn drop(&mut self) {
339 match &self.previous {
340 Some(value) => env::set_var(self.key, value),
341 None => env::remove_var(self.key),
342 }
343 }
344 }
345
346 fn test_paths(version: LuaVersion, src: &[&str], lib: &[&str]) -> Paths {
347 Paths {
348 src: PackagePath(src.iter().map(|path| PathBuf::from(*path)).collect()),
349 lib: PackagePath(lib.iter().map(|path| PathBuf::from(*path)).collect()),
350 bin: BinPath::default(),
351 version,
352 }
353 }
354
355 #[test]
356 fn package_path_leading_trailing_delimiters() {
357 let path = PackagePath::from_str(
358 ";;/path/to/some/lib/lua/5.1/?.so;/path/to/another/lib/lua/5.1/?.so;;;",
359 )
360 .unwrap();
361 assert_eq!(
362 path,
363 PackagePath(vec![
364 "/path/to/some/lib/lua/5.1/?.so".into(),
365 "/path/to/another/lib/lua/5.1/?.so".into(),
366 ])
367 );
368 assert_eq!(
369 format!("{path}"),
370 "/path/to/some/lib/lua/5.1/?.so;/path/to/another/lib/lua/5.1/?.so"
371 );
372 }
373
374 #[test]
375 #[serial]
376 fn pure_lua_path_ignores_inherited_paths() {
377 let _base = EnvGuard::set("LUA_PATH", "/system/?.lua");
378 let _versioned = EnvGuard::set("LUA_PATH_5_4", "/system54/?.lua");
379 let paths = test_paths(LuaVersion::Lua54, &["/project/?.lua"], &[]);
380
381 assert_eq!(paths.lua_path(LuaEnvironment::Pure), "/project/?.lua");
382 }
383
384 #[test]
385 #[serial]
386 fn impure_lua_path_prefers_versioned_env_and_appends_defaults() {
387 let _base = EnvGuard::set("LUA_PATH", "/system/?.lua");
388 let _versioned = EnvGuard::set("LUA_PATH_5_4", "/system54/?.lua");
389 let paths = test_paths(LuaVersion::Lua54, &["/project/?.lua"], &[]);
390
391 assert_eq!(
392 paths.lua_path(LuaEnvironment::Impure),
393 "/project/?.lua;/system54/?.lua;;"
394 );
395 }
396
397 #[test]
398 #[serial]
399 fn impure_lua_path_falls_back_to_unversioned_env() {
400 let _base = EnvGuard::set("LUA_PATH", "/system/?.lua");
401 let _versioned = EnvGuard::set("LUA_PATH_5_3", "/system53/?.lua");
402 let paths = test_paths(LuaVersion::Lua54, &["/project/?.lua"], &[]);
403
404 assert_eq!(
405 paths.lua_path(LuaEnvironment::Impure),
406 "/project/?.lua;/system/?.lua;;"
407 );
408 }
409
410 #[test]
411 #[serial]
412 fn impure_lua_cpath_merges_inherited_paths() {
413 let _versioned = EnvGuard::set("LUA_CPATH_5_4", "/system54/?.so");
414 let paths = test_paths(LuaVersion::Lua54, &[], &["/project/?.so"]);
415
416 assert_eq!(
417 paths.lua_cpath(LuaEnvironment::Impure),
418 "/project/?.so;/system54/?.so;;"
419 );
420 }
421}