1use 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
149async 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
188pub(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}