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