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            tracing::warn!("lux-lua library not found.\nCannot use the `lux.loader`.\nTo suppress this warning, set the `--no-loader` option.");
102            "".to_string()
103        } else {
104            paths.init()
105        };
106        let lua_init = format!(
107            r#"print([==[{}]==])
108{}
109{}
110        "#,
111            args.welcome_message.unwrap_or_default(),
112            args.lua_init.unwrap_or_default(),
113            loader_init
114        );
115
116        let status = match Command::new(&lua_cmd)
117            .current_dir(args.root)
118            .args(args.args)
119            .env("PATH", paths.path_prepended().joined())
120            .env("LUA_PATH", paths.package_path().joined())
121            .env("LUA_CPATH", paths.package_cpath().joined())
122            .env("LUA_INIT", lua_init)
123            .status()
124            .await
125        {
126            Ok(status) => Ok(status),
127            Err(err) => Err(RunLuaError::LuaCommandFailed {
128                lua_cmd: lua_cmd.to_string_lossy().to_string(),
129                source: err,
130            }),
131        }?;
132        if status.success() {
133            Ok(())
134        } else {
135            Err(RunLuaError::LuaCommandNonZeroExitCode {
136                lua_cmd: lua_cmd.to_string_lossy().to_string(),
137                exit_code: status.code(),
138            })
139        }
140    }
141}
142
143/// Attempts to detect lux-lua by invoking a Lua command
144/// in case it's a Lua wrapper, like the one created
145/// in nixpkgs using `lua.withPackages (ps: [ps.lux-lua])`.
146/// If the command fails for any reason (including not being able to find the 'lux' module),
147/// this function evaluates to `false`.
148async fn detect_lux_lua(lua_cmd: &Path, paths: &Paths) -> bool {
149    detect_lua_module(
150        lua_cmd,
151        &paths.package_path_prepended(),
152        &paths.package_cpath_prepended(),
153        &paths.path_prepended(),
154        "lux",
155    )
156    .await
157}
158
159async fn detect_lua_module(
160    lua_cmd: &Path,
161    lua_path: &PackagePath,
162    lua_cpath: &PackagePath,
163    path: &BinPath,
164    module: &str,
165) -> bool {
166    Command::new(lua_cmd)
167        .arg("-e")
168        .arg(format!(
169            "if pcall(require, '{}') then os.exit(0) else os.exit(1) end",
170            module
171        ))
172        .stderr(Stdio::null())
173        .stdout(Stdio::null())
174        .env("LUA_PATH", lua_path.joined())
175        .env("LUA_CPATH", lua_cpath.joined())
176        .env("PATH", path.joined())
177        .status()
178        .await
179        .is_ok_and(|status| status.success())
180}
181
182#[cfg(test)]
183mod tests {
184    use std::str::FromStr;
185
186    use super::*;
187    use assert_fs::prelude::{PathChild, PathCreateDir};
188    use assert_fs::TempDir;
189    use path_slash::PathBufExt;
190    use which::which;
191
192    #[tokio::test]
193    async fn test_detect_lua_module() {
194        let temp_dir = TempDir::new().unwrap();
195        let lua_dir = temp_dir.child("lua");
196        lua_dir.create_dir_all().unwrap();
197        let lux_file = lua_dir.child("lux.lua").to_path_buf();
198        let lux_path_expr = lua_dir.child("?.lua").to_path_buf();
199        tokio::fs::write(&lux_file, "return true").await.unwrap();
200        let package_path =
201            PackagePath::from_str(lux_path_expr.to_slash_lossy().to_string().as_str()).unwrap();
202        let package_cpath = PackagePath::default();
203        let path = BinPath::default();
204        let lua_cmd = which("lua")
205            .ok()
206            .or(which("luajit").ok())
207            .expect("lua not found");
208        let result = detect_lua_module(&lua_cmd, &package_path, &package_cpath, &path, "lux").await;
209        assert!(result, "detects module on the LUA_PATH");
210        let result = detect_lua_module(
211            &lua_cmd,
212            &package_path,
213            &package_cpath,
214            &path,
215            "lhflasdlkas",
216        )
217        .await;
218        assert!(!result, "does not detect non-existing module");
219    }
220}