1use bon::Builder;
6
7use crate::{
8 config::Config,
9 lua_installation::{LuaInstallation, LuaInstallationError},
10 lua_version::LuaVersion,
11 tree::InstallTree,
12};
13
14use std::{
15 io,
16 path::{Path, PathBuf},
17 process::Stdio,
18};
19
20use miette::Diagnostic;
21use thiserror::Error;
22use tokio::process::Command;
23
24use crate::{
25 lua_installation::{LuaBinary, LuaBinaryError},
26 path::{LuaEnvironment, Paths, PathsError},
27 tree::Tree,
28 tree::TreeError,
29};
30
31#[derive(Error, Debug, Diagnostic)]
32pub enum RunLuaError {
33 #[error("error running lua")]
34 #[diagnostic(forward(0))]
35 LuaBinary(#[from] LuaBinaryError),
36 #[error("failed to detect or install the lua runtime")]
37 #[diagnostic(forward(0))]
38 LuaInstallation(#[from] LuaInstallationError),
39 #[error("failed to run {lua_cmd}: {source}")]
40 LuaCommandFailed {
41 lua_cmd: String,
42 #[source]
43 source: io::Error,
44 },
45 #[error("{lua_cmd} exited with non-zero exit code: {}", exit_code.map(|code| code.to_string()).unwrap_or("unknown".into()))]
46 LuaCommandNonZeroExitCode {
47 lua_cmd: String,
48 exit_code: Option<i32>,
49 },
50 #[error(transparent)]
51 #[diagnostic(transparent)]
52 Paths(#[from] PathsError),
53
54 #[error(transparent)]
55 #[diagnostic(transparent)]
56 Tree(#[from] TreeError),
57}
58
59#[derive(Builder)]
60#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
61pub struct RunLua<'a> {
62 root: &'a Path,
63 tree: &'a Tree,
64 config: &'a Config,
65 lua_cmd: LuaBinary,
66 args: &'a Vec<String>,
67 prepend_test_paths: Option<bool>,
68 prepend_build_paths: Option<bool>,
69 disable_loader: Option<bool>,
70 lua_init: Option<String>,
71 welcome_message: Option<String>,
72 environment: LuaEnvironment,
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::for_environment(args.tree, args.config, args.environment)?;
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, args.environment).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 .envs(paths.lua_env_vars(args.environment, &lua_init))
127 .status()
128 .await
129 {
130 Ok(status) => Ok(status),
131 Err(err) => Err(RunLuaError::LuaCommandFailed {
132 lua_cmd: lua_cmd.to_string_lossy().to_string(),
133 source: err,
134 }),
135 }?;
136 if status.success() {
137 Ok(())
138 } else {
139 Err(RunLuaError::LuaCommandNonZeroExitCode {
140 lua_cmd: lua_cmd.to_string_lossy().to_string(),
141 exit_code: status.code(),
142 })
143 }
144 }
145}
146
147async fn detect_lux_lua(lua_cmd: &Path, paths: &Paths, environment: LuaEnvironment) -> bool {
153 detect_lua_module(lua_cmd, "lux", paths, environment).await
154}
155
156async fn detect_lua_module(
157 lua_cmd: &Path,
158 module: &str,
159 paths: &Paths,
160 environment: LuaEnvironment,
161) -> bool {
162 Command::new(lua_cmd)
163 .arg("-e")
164 .arg(format!(
165 "if pcall(require, '{}') then os.exit(0) else os.exit(1) end",
166 module
167 ))
168 .stderr(Stdio::null())
169 .stdout(Stdio::null())
170 .envs(paths.lua_env_vars(environment, ""))
171 .env("PATH", paths.path_prepended().joined())
172 .status()
173 .await
174 .is_ok_and(|status| status.success())
175}
176
177pub(crate) async fn resolve_lua_runtime(
182 version: &LuaVersion,
183 config: &Config,
184) -> Result<PathBuf, RunLuaError> {
185 match <LuaBinary as TryInto<PathBuf>>::try_into(LuaBinary::new(version.clone(), config)) {
186 Ok(lua_cmd) => Ok(lua_cmd),
187 Err(err) => {
188 let lua_installation = LuaInstallation::new(version, config).await?;
189 lua_installation
190 .bin()
191 .clone()
192 .ok_or(RunLuaError::LuaBinary(err))
193 }
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use std::str::FromStr;
200
201 use super::*;
202 use crate::fs;
203 use crate::path::PackagePath;
204 use assert_fs::prelude::{PathChild, PathCreateDir};
205 use assert_fs::TempDir;
206 use path_slash::PathBufExt;
207 use which::which;
208
209 #[tokio::test]
210 async fn test_detect_lua_module() {
211 let temp_dir = TempDir::new().unwrap();
212 let lua_dir = temp_dir.child("lua");
213 lua_dir.create_dir_all().unwrap();
214 let lux_file = lua_dir.child("lux.lua").to_path_buf();
215 let lux_path_expr = lua_dir.child("?.lua").to_path_buf();
216 fs::tokio::write(&lux_file, "return true").await.unwrap();
217 let package_path =
218 PackagePath::from_str(lux_path_expr.to_slash_lossy().to_string().as_str()).unwrap();
219 let package_cpath = PackagePath::default();
220 let paths = Paths::test(LuaVersion::Lua54, package_path, package_cpath);
221 let lua_cmd = which("lua")
222 .ok()
223 .or(which("luajit").ok())
224 .expect("lua not found");
225 let result = detect_lua_module(&lua_cmd, "lux", &paths, LuaEnvironment::Pure).await;
226 assert!(result, "detects module on the LUA_PATH");
227 let result = detect_lua_module(&lua_cmd, "lhflasdlkas", &paths, LuaEnvironment::Pure).await;
228 assert!(!result, "does not detect non-existing module");
229 }
230}