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