1use 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
146async 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}