Skip to main content

lux_lib/
path.rs

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/// Whether Lua should only see the current tree's packages (pure mode)
18/// or also user-installed packages and system paths (impure).
19#[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    /// Paths for Lua libraries
29    src: PackagePath,
30    /// Paths for native Lua libraries
31    lib: PackagePath,
32    /// Paths for executables
33    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    /// Get the `package.path`
100    pub fn package_path(&self) -> &PackagePath {
101        &self.src
102    }
103
104    /// Get the `package.cpath`
105    pub fn package_cpath(&self) -> &PackagePath {
106        &self.lib
107    }
108
109    /// Get the `package.path`, prepended to `LUA_PATH`
110    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    /// Get the `package.cpath`, prepended to `LUA_CPATH`
118    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    /// The Lua version these paths belong to.
152    pub fn version(&self) -> &LuaVersion {
153        &self.version
154    }
155
156    /// Get the `$PATH`
157    pub fn path(&self) -> &BinPath {
158        &self.bin
159    }
160
161    /// Get `$LUA_INIT`
162    pub fn init(&self) -> String {
163        format!("if _VERSION:find('{}') then {LUA_INIT} end", self.version)
164    }
165
166    /// Get the `$PATH`, prepended to the existing `$PATH` environment.
167    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    /// Adds a `PathBuf` to the path.
279    /// Drops it silently if the `PathBuf` is invalid and cannot be used to create a `PATH` variable
280    /// (e.g. if it contains `PATH` separator characters).
281    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    /// Joins the path to create a PATH expression.
293    /// If a path contains invalid characters (e.g. the PATH separator),
294    /// this returns an empty string.
295    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}