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