lux_lib/operations/
run_lua.rs1use 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
143async 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}