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};
11
12use std::{
13    io,
14    path::{Path, PathBuf},
15    process::Stdio,
16};
17
18use thiserror::Error;
19use tokio::process::Command;
20
21use crate::{
22    lua_installation::{LuaBinary, LuaBinaryError},
23    path::{Paths, PathsError},
24    tree::Tree,
25    tree::TreeError,
26};
27
28#[derive(Error, Debug)]
29pub enum RunLuaError {
30    #[error("error running lua: {0}")]
31    LuaBinary(#[from] LuaBinaryError),
32    #[error("failed to run {lua_cmd}: {source}")]
33    LuaCommandFailed {
34        lua_cmd: String,
35        #[source]
36        source: io::Error,
37    },
38    #[error("{lua_cmd} exited with non-zero exit code: {}", exit_code.map(|code| code.to_string()).unwrap_or("unknown".into()))]
39    LuaCommandNonZeroExitCode {
40        lua_cmd: String,
41        exit_code: Option<i32>,
42    },
43    #[error(transparent)]
44    Paths(#[from] PathsError),
45
46    #[error(transparent)]
47    Tree(#[from] TreeError),
48}
49
50#[derive(Builder)]
51#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
52pub struct RunLua<'a> {
53    root: &'a Path,
54    tree: &'a Tree,
55    config: &'a Config,
56    lua_cmd: LuaBinary,
57    args: &'a Vec<String>,
58    prepend_test_paths: Option<bool>,
59    prepend_build_paths: Option<bool>,
60    disable_loader: Option<bool>,
61    lua_init: Option<String>,
62    welcome_message: Option<String>,
63}
64
65impl<State> RunLuaBuilder<'_, State>
66where
67    State: run_lua_builder::State + run_lua_builder::IsComplete,
68{
69    pub async fn run_lua(self) -> Result<(), RunLuaError> {
70        let args = self._build();
71        let mut paths = Paths::new(args.tree)?;
72
73        if args.prepend_test_paths.unwrap_or(false) {
74            let test_tree_path = args.tree.test_tree(args.config)?;
75
76            let test_path = Paths::new(&test_tree_path)?;
77
78            paths.prepend(&test_path);
79        }
80
81        if args.prepend_build_paths.unwrap_or(false) {
82            let build_tree_path = args.tree.build_tree(args.config)?;
83
84            let build_path = Paths::new(&build_tree_path)?;
85
86            paths.prepend(&build_path);
87        }
88
89        let lua_cmd: PathBuf = args.lua_cmd.try_into()?;
90
91        let is_lux_lua_available = detect_lux_lua(&lua_cmd, &paths).await;
92
93        let loader_init = if args.disable_loader.unwrap_or(false) {
94            "".to_string()
95        } else if !is_lux_lua_available && args.tree.version().lux_lib_dir().is_none() {
96            eprintln!(
97                "⚠️ WARNING: lux-lua library not found.
98Cannot use the `lux.loader`.
99To suppress this warning, set the `--no-loader` option.
100                "
101            );
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 test {
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}