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 mut retried = false;
174
175 let (stdout_accum, stderr, exit_code) =
176 loop {
177 let session_is_dead =
178 if let Some(session) = lua.app_data_ref::<ShellSession>() {
179 let mut inner =
180 session.inner.lock().expect("lock poisoned");
181 inner.child.try_wait().map_or(true, |s| s.is_some())
182 } else {
183 true
184 };
185
186 if retried || session_is_dead {
187 let session =
188 ShellSession::new(lua, &build_dir).map_err(|e| {
189 mlua::Error::RuntimeError(e.to_string())
190 })?;
191 lua.set_app_data(session);
192 }
193
194 let session = lua
195 .app_data_ref::<ShellSession>()
196 .expect("ShellSession missing from app_data");
197 let mut inner = session.inner.lock().expect("lock poisoned");
198
199 if !quiet {
200 println!("Executing: {command}");
201 }
202
203 if let Ok(mut buf) = inner.stderr_buffer.lock() {
205 buf.clear();
206 }
207
208 let sentinel = inner.sentinel.clone();
209
210 if cfg!(target_os = "windows") {
211 let cmd_text = format!(
212 "$ErrorActionPreference = 'Continue'; & {{ {command} \
213 }}; \"{sentinel} $LASTEXITCODE\"\n"
214 );
215 inner.stdin.write_all(cmd_text.as_bytes()).map_err(
216 |e| mlua::Error::RuntimeError(e.to_string())
217 )?;
218 inner.stdin.flush().map_err(|e| {
219 mlua::Error::RuntimeError(e.to_string())
220 })?;
221 } else {
222 let cmd_text = format!(
223 "{{ {command} ; }} ; printf \"\\n%s %d\\n\" \
224 {sentinel:?} $?\n"
225 );
226 inner.stdin.write_all(cmd_text.as_bytes()).map_err(
227 |e| mlua::Error::RuntimeError(e.to_string())
228 )?;
229 inner.stdin.flush().map_err(|e| {
230 mlua::Error::RuntimeError(e.to_string())
231 })?;
232 }
233
234 let mut stdout_accum = String::new();
235 let mut line = String::new();
236 let mut exit_code = 0;
237
238 let mut session_died = false;
239 loop {
240 line.clear();
241 let n = inner.stdout.read_line(&mut line).map_err(|e| {
242 mlua::Error::RuntimeError(e.to_string())
243 })?;
244 if n == 0 {
245 session_died = true;
246 break;
247 }
248
249 if let Some(idx) = line.find(&sentinel) {
250 let out_part = &line[..idx];
251 stdout_accum.push_str(out_part);
252
253 let rest = line[idx..]
254 .strip_prefix(&sentinel)
255 .expect("sentinel missing")
256 .trim();
257 exit_code = rest.parse::<i32>().unwrap_or(0);
258 break;
259 }
260 stdout_accum.push_str(&line);
261 }
262
263 if session_died && !retried {
264 retried = true;
265 continue;
266 }
267 if session_died {
268 return Err(mlua::Error::RuntimeError(
269 "Shell session ended unexpectedly".to_string()
270 ));
271 }
272
273 let stderr = inner
275 .stderr_buffer
276 .lock()
277 .map(|b| b.clone())
278 .unwrap_or_default();
279
280 if exit_code != 0 && !quiet {
281 eprintln!("[cmd] {stderr}");
282 }
283
284 break (stdout_accum.trim_end().to_string(), stderr, exit_code);
285 };
286
287 Ok((stdout_accum, stderr, exit_code))
288 })?;
289 lua.globals().set("cmd", cmd_fn)?;
290 Ok(())
291}
292
293pub fn add_zpatch(lua: &Lua, quiet: bool) -> Result<(), mlua::Error> {
300 let zpatch_fn = lua.create_function(
301 move |lua, (patch_file, strip): (String, Option<u32>)| {
302 let build_dir: String = lua.globals().get("BUILD_DIR")?;
303 let strip_level = strip.unwrap_or(1);
304
305 if !quiet {
306 println!("Applying patch: {patch_file}");
307 }
308
309 let output = std::process::Command::new("patch")
310 .arg(format!("-p{strip_level}"))
311 .arg("-i")
312 .arg(&patch_file)
313 .current_dir(&build_dir)
314 .output();
315
316 match output {
317 Ok(out) => {
318 if !out.status.success() {
319 let stderr =
320 String::from_utf8_lossy(&out.stderr).to_string();
321 return Err(mlua::Error::RuntimeError(format!(
322 "patch failed: {stderr}"
323 )));
324 }
325 if !quiet {
326 println!("Successfully applied patch {patch_file}");
327 }
328 Ok(())
329 }
330 Err(e) => Err(mlua::Error::RuntimeError(format!(
331 "Failed to execute patch command: {e}"
332 )))
333 }
334 }
335 )?;
336 lua.globals().set("zpatch", zpatch_fn)?;
337 Ok(())
338}