Skip to main content

lux_lib/operations/
run_lua.rs

1//! Run the `lua` binary with some given arguments.
2//!
3//! The interfaces exposed here ensure that the correct version of Lua is being used.
4
5use bon::Builder;
6
7use crate::{
8    config::Config,
9    path::{BinPath, PackagePath},
10    tree::InstallTree,
11};
12
13use std::{
14    io,
15    path::{Path, PathBuf},
16    process::Stdio,
17};
18
19use thiserror::Error;
20use tokio::process::Command;
21
22use crate::{
23    lua_installation::{LuaBinary, LuaBinaryError},
24    path::{Paths, PathsError},
25    tree::Tree,
26    tree::TreeError,
27};
28
29#[derive(Error, Debug)]
30pub enum RunLuaError {
31    #[error("error running lua: {0}")]
32    LuaBinary(#[from] LuaBinaryError),
33    #[error("failed to run {lua_cmd}: {source}")]
34    LuaCommandFailed {
35        lua_cmd: String,
36        #[source]
37        source: io::Error,
38    },
39    #[error("{lua_cmd} exited with non-zero exit code: {}", exit_code.map(|code| code.to_string()).unwrap_or("unknown".into()))]
40    LuaCommandNonZeroExitCode {
41        lua_cmd: String,
42        exit_code: Option<i32>,
43    },
44    #[error(transparent)]
45    Paths(#[from] PathsError),
46
47    #[error(transparent)]
48    Tree(#[from] TreeError),
49}
50
51#[derive(Builder)]
52#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
53pub struct RunLua<'a> {
54    root: &'a Path,
55    tree: &'a Tree,
56    config: &'a Config,
57    lua_cmd: LuaBinary,
58    args: &'a Vec<String>,
59    prepend_test_paths: Option<bool>,
60    prepend_build_paths: Option<bool>,
61    disable_loader: Option<bool>,
62    lua_init: Option<String>,
63    welcome_message: Option<String>,
64}
65
66impl<State> RunLuaBuilder<'_, State>
67where
68    State: run_lua_builder::State + run_lua_builder::IsComplete,
69{
70    pub async fn run_lua(self) -> Result<(), RunLuaError> {
71        let args = self._build();
72        let mut paths = Paths::new(args.tree)?;
73
74        if args.prepend_test_paths.unwrap_or(false) {
75            let test_tree_path = args.tree.test_tree(args.config)?;
76
77            let test_path = Paths::new(&test_tree_path)?;
78
79            paths.prepend(&test_path);
80        }
81
82        if args.prepend_build_paths.unwrap_or(false) {
83            let build_tree_path = args.tree.build_tree(args.config)?;
84
85            let build_path = Paths::new(&build_tree_path)?;
86
87            paths.prepend(&build_path);
88        }
89
90        let lua_cmd: PathBuf = args.lua_cmd.try_into()?;
91
92        let is_lux_lua_available = detect_lux_lua(&lua_cmd, &paths).await;
93
94        let loader_init = if args.disable_loader.unwrap_or(false) {
95            "".to_string()
96        } else if !is_lux_lua_available && args.tree.version().lux_lib_dir().is_none() {
97            eprintln!(
98                "⚠️ WARNING: lux-lua library not found.
99Cannot use the `lux.loader`.
100To suppress this warning, set the `--no-loader` option.
101                "
102            );
103            "".to_string()
104        } else {
105            paths.init()
106        };
107        let lua_init = format!(
108            r#"print([==[{}]==])
109{}
110{}
111        "#,
112            args.welcome_message.unwrap_or_default(),
113            args.lua_init.unwrap_or_default(),
114            loader_init
115        );
116
117        let status = match Command::new(&lua_cmd)
118            .current_dir(args.root)
119            .args(args.args)
120            .env("PATH", paths.path_prepended().joined())
121            .env("LUA_PATH", paths.package_path().joined())
122            .env("LUA_CPATH", paths.package_cpath().joined())
123            .env("LUA_INIT", lua_init)
124            .status()
125            .await
126        {
127            Ok(status) => Ok(status),
128            Err(err) => Err(RunLuaError::LuaCommandFailed {
129                lua_cmd: lua_cmd.to_string_lossy().to_string(),
130                source: err,
131            }),
132        }?;
133        if status.success() {
134            Ok(())
135        } else {
136            Err(RunLuaError::LuaCommandNonZeroExitCode {
137                lua_cmd: lua_cmd.to_string_lossy().to_string(),
138                exit_code: status.code(),
139            })
140        }
141    }
142}
143
144/// Attempts to detect lux-lua by invoking a Lua command
145/// in case it's a Lua wrapper, like the one created
146/// in nixpkgs using `lua.withPackages (ps: [ps.lux-lua])`.
147/// If the command fails for any reason (including not being able to find the 'lux' module),
148/// this function evaluates to `false`.
149async fn detect_lux_lua(lua_cmd: &Path, paths: &Paths) -> bool {
150    detect_lua_module(
151        lua_cmd,
152        &paths.package_path_prepended(),
153        &paths.package_cpath_prepended(),
154        &paths.path_prepended(),
155        "lux",
156    )
157    .await
158}
159
160async fn detect_lua_module(
161    lua_cmd: &Path,
162    lua_path: &PackagePath,
163    lua_cpath: &PackagePath,
164    path: &BinPath,
165    module: &str,
166) -> bool {
167    Command::new(lua_cmd)
168        .arg("-e")
169        .arg(format!(
170            "if pcall(require, '{}') then os.exit(0) else os.exit(1) end",
171            module
172        ))
173        .stderr(Stdio::null())
174        .stdout(Stdio::null())
175        .env("LUA_PATH", lua_path.joined())
176        .env("LUA_CPATH", lua_cpath.joined())
177        .env("PATH", path.joined())
178        .status()
179        .await
180        .is_ok_and(|status| status.success())
181}
182
183#[cfg(test)]
184mod tests {
185    use std::str::FromStr;
186
187    use super::*;
188    use assert_fs::prelude::{PathChild, PathCreateDir};
189    use assert_fs::TempDir;
190    use path_slash::PathBufExt;
191    use which::which;
192
193    #[tokio::test]
194    async fn test_detect_lua_module() {
195        let temp_dir = TempDir::new().unwrap();
196        let lua_dir = temp_dir.child("lua");
197        lua_dir.create_dir_all().unwrap();
198        let lux_file = lua_dir.child("lux.lua").to_path_buf();
199        let lux_path_expr = lua_dir.child("?.lua").to_path_buf();
200        tokio::fs::write(&lux_file, "return true").await.unwrap();
201        let package_path =
202            PackagePath::from_str(lux_path_expr.to_slash_lossy().to_string().as_str()).unwrap();
203        let package_cpath = PackagePath::default();
204        let path = BinPath::default();
205        let lua_cmd = which("lua")
206            .ok()
207            .or(which("luajit").ok())
208            .expect("lua not found");
209        let result = detect_lua_module(&lua_cmd, &package_path, &package_cpath, &path, "lux").await;
210        assert!(result, "detects module on the LUA_PATH");
211        let result = detect_lua_module(
212            &lua_cmd,
213            &package_path,
214            &package_cpath,
215            &path,
216            "lhflasdlkas",
217        )
218        .await;
219        assert!(!result, "does not detect non-existing module");
220    }
221}