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