lux_lib/operations/
run_lua.rs1use 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 thiserror::Error;
20use tokio::process::Command;
21
22use crate::{
23 lua_installation::{LuaBinary, LuaBinaryError},
24 path::{Paths, PathsError},
25 tree::Tree,
26 tree::TreeError,
27};
28
29#[derive(Error, Debug)]
30pub enum RunLuaError {
31 #[error("error running lua: {0}")]
32 LuaBinary(#[from] LuaBinaryError),
33 #[error("failed to run {lua_cmd}: {source}")]
34 LuaCommandFailed {
35 lua_cmd: String,
36 #[source]
37 source: io::Error,
38 },
39 #[error("{lua_cmd} exited with non-zero exit code: {}", exit_code.map(|code| code.to_string()).unwrap_or("unknown".into()))]
40 LuaCommandNonZeroExitCode {
41 lua_cmd: String,
42 exit_code: Option<i32>,
43 },
44 #[error(transparent)]
45 Paths(#[from] PathsError),
46
47 #[error(transparent)]
48 Tree(#[from] TreeError),
49}
50
51#[derive(Builder)]
52#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
53pub struct RunLua<'a> {
54 root: &'a Path,
55 tree: &'a Tree,
56 config: &'a Config,
57 lua_cmd: LuaBinary,
58 args: &'a Vec<String>,
59 prepend_test_paths: Option<bool>,
60 prepend_build_paths: Option<bool>,
61 disable_loader: Option<bool>,
62 lua_init: Option<String>,
63 welcome_message: Option<String>,
64}
65
66impl<State> RunLuaBuilder<'_, State>
67where
68 State: run_lua_builder::State + run_lua_builder::IsComplete,
69{
70 pub async fn run_lua(self) -> Result<(), RunLuaError> {
71 let args = self._build();
72 let mut paths = Paths::new(args.tree)?;
73
74 if args.prepend_test_paths.unwrap_or(false) {
75 let test_tree_path = args.tree.test_tree(args.config)?;
76
77 let test_path = Paths::new(&test_tree_path)?;
78
79 paths.prepend(&test_path);
80 }
81
82 if args.prepend_build_paths.unwrap_or(false) {
83 let build_tree_path = args.tree.build_tree(args.config)?;
84
85 let build_path = Paths::new(&build_tree_path)?;
86
87 paths.prepend(&build_path);
88 }
89
90 let lua_cmd: PathBuf = args.lua_cmd.try_into()?;
91
92 let is_lux_lua_available = detect_lux_lua(&lua_cmd, &paths).await;
93
94 let loader_init = if args.disable_loader.unwrap_or(false) {
95 "".to_string()
96 } else if !is_lux_lua_available && args.tree.version().lux_lib_dir().is_none() {
97 eprintln!(
98 "⚠️ WARNING: lux-lua library not found.
99Cannot use the `lux.loader`.
100To suppress this warning, set the `--no-loader` option.
101 "
102 );
103 "".to_string()
104 } else {
105 paths.init()
106 };
107 let lua_init = format!(
108 r#"print([==[{}]==])
109{}
110{}
111 "#,
112 args.welcome_message.unwrap_or_default(),
113 args.lua_init.unwrap_or_default(),
114 loader_init
115 );
116
117 let status = match Command::new(&lua_cmd)
118 .current_dir(args.root)
119 .args(args.args)
120 .env("PATH", paths.path_prepended().joined())
121 .env("LUA_PATH", paths.package_path().joined())
122 .env("LUA_CPATH", paths.package_cpath().joined())
123 .env("LUA_INIT", lua_init)
124 .status()
125 .await
126 {
127 Ok(status) => Ok(status),
128 Err(err) => Err(RunLuaError::LuaCommandFailed {
129 lua_cmd: lua_cmd.to_string_lossy().to_string(),
130 source: err,
131 }),
132 }?;
133 if status.success() {
134 Ok(())
135 } else {
136 Err(RunLuaError::LuaCommandNonZeroExitCode {
137 lua_cmd: lua_cmd.to_string_lossy().to_string(),
138 exit_code: status.code(),
139 })
140 }
141 }
142}
143
144async fn detect_lux_lua(lua_cmd: &Path, paths: &Paths) -> bool {
150 detect_lua_module(
151 lua_cmd,
152 &paths.package_path_prepended(),
153 &paths.package_cpath_prepended(),
154 &paths.path_prepended(),
155 "lux",
156 )
157 .await
158}
159
160async fn detect_lua_module(
161 lua_cmd: &Path,
162 lua_path: &PackagePath,
163 lua_cpath: &PackagePath,
164 path: &BinPath,
165 module: &str,
166) -> bool {
167 Command::new(lua_cmd)
168 .arg("-e")
169 .arg(format!(
170 "if pcall(require, '{}') then os.exit(0) else os.exit(1) end",
171 module
172 ))
173 .stderr(Stdio::null())
174 .stdout(Stdio::null())
175 .env("LUA_PATH", lua_path.joined())
176 .env("LUA_CPATH", lua_cpath.joined())
177 .env("PATH", path.joined())
178 .status()
179 .await
180 .is_ok_and(|status| status.success())
181}
182
183#[cfg(test)]
184mod tests {
185 use std::str::FromStr;
186
187 use super::*;
188 use assert_fs::prelude::{PathChild, PathCreateDir};
189 use assert_fs::TempDir;
190 use path_slash::PathBufExt;
191 use which::which;
192
193 #[tokio::test]
194 async fn test_detect_lua_module() {
195 let temp_dir = TempDir::new().unwrap();
196 let lua_dir = temp_dir.child("lua");
197 lua_dir.create_dir_all().unwrap();
198 let lux_file = lua_dir.child("lux.lua").to_path_buf();
199 let lux_path_expr = lua_dir.child("?.lua").to_path_buf();
200 tokio::fs::write(&lux_file, "return true").await.unwrap();
201 let package_path =
202 PackagePath::from_str(lux_path_expr.to_slash_lossy().to_string().as_str()).unwrap();
203 let package_cpath = PackagePath::default();
204 let path = BinPath::default();
205 let lua_cmd = which("lua")
206 .ok()
207 .or(which("luajit").ok())
208 .expect("lua not found");
209 let result = detect_lua_module(&lua_cmd, &package_path, &package_cpath, &path, "lux").await;
210 assert!(result, "detects module on the LUA_PATH");
211 let result = detect_lua_module(
212 &lua_cmd,
213 &package_path,
214 &package_cpath,
215 &path,
216 "lhflasdlkas",
217 )
218 .await;
219 assert!(!result, "does not detect non-existing module");
220 }
221}