1use std::fmt::Write as _;
2use std::io::{BufRead, BufReader, Write as _};
3use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
4use std::sync::{Arc, Mutex};
5use std::thread;
6
7use mlua::{self, Lua, Table, Value};
8
9struct InnerSession {
11 child: Child,
13 stdin: ChildStdin,
15 stdout: BufReader<ChildStdout>,
17 stderr_buffer: Arc<Mutex<String>>,
19 sentinel: String
21}
22
23impl Drop for InnerSession {
24 fn drop(&mut self) {
25 let _ = self.child.kill();
26 let _ = self.child.wait();
27 }
28}
29
30struct ShellSession {
32 inner: Mutex<InnerSession>
34}
35
36impl ShellSession {
37 fn new(lua: &Lua, build_dir: &str) -> Result<Self, anyhow::Error> {
39 let sentinel =
40 format!("---ZOI_CMD_COMPLETE_{}---", uuid::Uuid::new_v4());
41
42 let mut child = if cfg!(target_os = "windows") {
43 Command::new("pwsh")
44 .arg("-NoProfile")
45 .arg("-NonInteractive")
46 .arg("-Command")
47 .arg("-")
48 .current_dir(build_dir)
49 .stdin(Stdio::piped())
50 .stdout(Stdio::piped())
51 .stderr(Stdio::piped())
52 .spawn()?
53 } else {
54 Command::new("bash")
55 .arg("--noprofile")
56 .arg("--norc")
57 .current_dir(build_dir)
58 .stdin(Stdio::piped())
59 .stdout(Stdio::piped())
60 .stderr(Stdio::piped())
61 .spawn()?
62 };
63
64 let mut stdin = child.stdin.take().expect("Failed to open stdin");
65 let stdout =
66 BufReader::new(child.stdout.take().expect("Failed to open stdout"));
67 let stderr = child.stderr.take().expect("Failed to open stderr");
68
69 let stderr_buffer = Arc::new(Mutex::new(String::new()));
70 let buffer_clone = Arc::clone(&stderr_buffer);
71
72 thread::spawn(move || {
74 let mut reader = BufReader::new(stderr);
75 let mut line = String::new();
76 while reader.read_line(&mut line).unwrap_or(0) > 0 {
77 if let Ok(mut buf) = buffer_clone.lock() {
78 buf.push_str(&line);
79 }
80 line.clear();
81 }
82 });
83
84 let mut env_cmds = String::new();
86 let globals = lua.globals();
87
88 let vars = [
89 ("BUILD_TYPE", "BUILD_TYPE"),
90 ("SUBPKG", "SUBPKG"),
91 ("BUILD_DIR", "BUILD_DIR"),
92 ("STAGING_DIR", "STAGING_DIR")
93 ];
94
95 for (lua_name, env_name) in vars {
96 if let Ok(val) = globals.get::<String>(lua_name) {
97 if cfg!(target_os = "windows") {
98 let _ = writeln!(
99 env_cmds,
100 "$env:{env_name} = '{}'",
101 val.replace('\'', "''")
102 );
103 } else {
104 let _ = writeln!(env_cmds, "export {env_name}={val:?}");
105 }
106 }
107 }
108
109 let tables = [("SYSTEM", "SYSTEM_"), ("ZOI", "ZOI_")];
111 for (table_name, prefix) in tables {
112 if let Ok(table) = globals.get::<Table>(table_name) {
113 for (k, v) in table.pairs::<String, Value>().flatten() {
114 let val_str = match v {
115 Value::String(s) => s
116 .to_str()
117 .map_err(|e| anyhow::anyhow!(e.to_string()))?
118 .to_string(),
119 Value::Integer(i) => i.to_string(),
120 Value::Number(n) => n.to_string(),
121 Value::Boolean(b) => b.to_string(),
122 _ => continue
123 };
124 let k_upper = k.to_uppercase();
125 if cfg!(target_os = "windows") {
126 let _ = writeln!(
127 env_cmds,
128 "$env:{prefix}{k_upper} = '{}'",
129 val_str.replace('\'', "''")
130 );
131 } else {
132 let _ = writeln!(
133 env_cmds,
134 "export {prefix}{k_upper}={val_str:?}"
135 );
136 }
137 }
138 }
139 }
140
141 stdin.write_all(env_cmds.as_bytes())?;
142 stdin.flush()?;
143
144 Ok(Self {
145 inner: Mutex::new(InnerSession {
146 child,
147 stdin,
148 stdout,
149 stderr_buffer,
150 sentinel
151 })
152 })
153 }
154}
155
156pub fn add_cmd_util(lua: &Lua, quiet: bool) -> Result<(), mlua::Error> {
167 let cmd_fn = lua.create_function(move |lua, command: String| {
168 let build_dir: String = lua.globals().get("BUILD_DIR")?;
169
170 let session_is_dead =
171 if let Some(session) = lua.app_data_ref::<ShellSession>() {
172 let mut inner = session.inner.lock().expect("lock poisoned");
173 inner.child.try_wait().map_or(true, |s| s.is_some())
174 } else {
175 true
176 };
177
178 if session_is_dead {
179 let session = ShellSession::new(lua, &build_dir)
180 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
181 lua.set_app_data(session);
182 }
183
184 let session = lua
185 .app_data_ref::<ShellSession>()
186 .expect("ShellSession missing from app_data");
187 let mut inner = session.inner.lock().expect("lock poisoned");
188
189 if !quiet {
190 println!("Executing: {command}");
191 }
192
193 if let Ok(mut buf) = inner.stderr_buffer.lock() {
195 buf.clear();
196 }
197
198 let sentinel = inner.sentinel.clone();
199
200 if cfg!(target_os = "windows") {
201 let cmd_text = format!(
202 "$ErrorActionPreference = 'Continue'; & {{ {command} }}; \
203 \"{sentinel} $LASTEXITCODE\"\n"
204 );
205 inner
206 .stdin
207 .write_all(cmd_text.as_bytes())
208 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
209 inner
210 .stdin
211 .flush()
212 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
213 } else {
214 let cmd_text = format!(
215 "{{ {command} ; }} ; printf \"\\n%s %d\\n\" {sentinel:?} $?\n"
216 );
217 inner
218 .stdin
219 .write_all(cmd_text.as_bytes())
220 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
221 inner
222 .stdin
223 .flush()
224 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
225 }
226
227 let mut stdout_accum = String::new();
228 let mut line = String::new();
229 let exit_code;
230
231 loop {
232 line.clear();
233 let n = inner
234 .stdout
235 .read_line(&mut line)
236 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
237 if n == 0 {
238 return Err(mlua::Error::RuntimeError(
239 "Shell session ended unexpectedly".to_string()
240 ));
241 }
242
243 if let Some(idx) = line.find(&sentinel) {
244 let out_part = &line[..idx];
245 stdout_accum.push_str(out_part);
246
247 let rest = line[idx..]
248 .strip_prefix(&sentinel)
249 .expect("sentinel missing")
250 .trim();
251 exit_code = rest.parse::<i32>().unwrap_or(0);
252 break;
253 }
254 stdout_accum.push_str(&line);
255 }
256
257 let stderr = inner
259 .stderr_buffer
260 .lock()
261 .map(|b| b.clone())
262 .unwrap_or_default();
263
264 if exit_code != 0 && !quiet {
265 eprintln!("[cmd] {stderr}");
266 }
267
268 Ok((stdout_accum.trim_end().to_string(), stderr, exit_code))
269 })?;
270 lua.globals().set("cmd", cmd_fn)?;
271 Ok(())
272}
273
274pub fn add_zpatch(lua: &Lua, quiet: bool) -> Result<(), mlua::Error> {
281 let zpatch_fn = lua.create_function(
282 move |lua, (patch_file, strip): (String, Option<u32>)| {
283 let build_dir: String = lua.globals().get("BUILD_DIR")?;
284 let strip_level = strip.unwrap_or(1);
285
286 if !quiet {
287 println!("Applying patch: {patch_file}");
288 }
289
290 let output = std::process::Command::new("patch")
291 .arg(format!("-p{strip_level}"))
292 .arg("-i")
293 .arg(&patch_file)
294 .current_dir(&build_dir)
295 .output();
296
297 match output {
298 Ok(out) => {
299 if !out.status.success() {
300 let stderr =
301 String::from_utf8_lossy(&out.stderr).to_string();
302 return Err(mlua::Error::RuntimeError(format!(
303 "patch failed: {stderr}"
304 )));
305 }
306 if !quiet {
307 println!("Successfully applied patch {patch_file}");
308 }
309 Ok(())
310 }
311 Err(e) => Err(mlua::Error::RuntimeError(format!(
312 "Failed to execute patch command: {e}"
313 )))
314 }
315 }
316 )?;
317 lua.globals().set("zpatch", zpatch_fn)?;
318 Ok(())
319}