Skip to main content

lux_lib/
path.rs

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/// Whether Lua should only see the current tree's packages (pure mode)
24/// or also user-installed packages and system paths (impure).
25#[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    /// Paths for Lua libraries
35    src: PackagePath,
36    /// Paths for native Lua libraries
37    lib: PackagePath,
38    /// Paths for executables
39    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    /// Get the `package.path`
117    pub fn package_path(&self) -> &PackagePath {
118        &self.src
119    }
120
121    /// Get the `package.cpath`
122    pub fn package_cpath(&self) -> &PackagePath {
123        &self.lib
124    }
125
126    /// Get the `package.path`, prepended to `LUA_PATH`
127    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    /// Get the `package.cpath`, prepended to `LUA_CPATH`
135    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    /// The Lua version these paths belong to.
169    pub fn version(&self) -> &LuaVersion {
170        &self.version
171    }
172
173    /// Get the `$PATH`
174    pub fn path(&self) -> &BinPath {
175        &self.bin
176    }
177
178    /// Get `$LUA_INIT`
179    pub fn init(&self) -> String {
180        format!("if _VERSION:find('{}') then {LUA_INIT} end", self.version)
181    }
182
183    /// Get the `$PATH`, prepended to the existing `$PATH` environment.
184    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    /// Adds a `PathBuf` to the path.
296    /// Drops it silently if the `PathBuf` is invalid and cannot be used to create a `PATH` variable
297    /// (e.g. if it contains `PATH` separator characters).
298    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    /// Joins the path to create a PATH expression.
310    /// If a path contains invalid characters (e.g. the PATH separator),
311    /// this returns an empty string.
312    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}