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