1use std::collections::HashSet;
9use std::fs::{self, File};
10use std::path::{Path, PathBuf};
11
12use anyhow::{Result, anyhow};
13use colored::Colorize;
14use ignore::gitignore::GitignoreBuilder;
15use mlua::{Lua, LuaSerdeExt, Table, Value};
16use tar::Builder as TarBuilder;
17use tempfile::Builder;
18use walkdir::WalkDir;
19use zstd::stream::write::Encoder as ZstdEncoder;
20
21pub fn run(
30 package_file: &Path,
31 output_dir: Option<&Path>,
32 sign: Option<String>,
33 version_override: Option<&str>,
34 build_type: Option<&str>
35) -> Result<()> {
36 let pkg_dir = package_file
37 .parent()
38 .ok_or_else(|| anyhow!("Could not get parent directory"))?;
39
40 let mut ignore_builder = GitignoreBuilder::new(pkg_dir);
42 let zoiignore_path = pkg_dir.join(".zoiignore");
43 if zoiignore_path.exists()
44 && let Some(err) = ignore_builder.add(&zoiignore_path)
45 {
46 eprintln!("{}: Error parsing .zoiignore: {}", "Warning".yellow(), err);
47 }
48 let ignore = ignore_builder.build()?;
49
50 let is_ignored = |rel_path: &Path, is_dir: bool| -> bool {
51 ignore.matched(rel_path, is_dir).is_ignore()
52 };
53
54 println!(
55 "{} Bundling package: {}",
56 "::".bold().blue(),
57 package_file.display()
58 );
59
60 let lua = Lua::new();
61 let platform = zoi_core::utils::get_platform()?;
62
63 let refs_table = lua.create_table().map_err(|e| anyhow!(e.to_string()))?;
65 lua.globals()
66 .set("__ZoiReferencedFiles", refs_table)
67 .map_err(|e| anyhow!(e.to_string()))?;
68
69 let bundle_type = build_type.unwrap_or("source");
74
75 zoi_lua::functions::setup_lua_environment(
77 &lua,
78 &platform,
79 version_override,
80 package_file.to_str(),
81 None,
82 Some("/tmp/mock-build"),
83 Some("/tmp/mock-staging"),
84 None,
85 None,
86 Some(bundle_type),
87 true )
89 .map_err(|e| anyhow!(e.to_string()))?;
90
91 lua.globals()
92 .set("BUILD_TYPE", bundle_type)
93 .map_err(|e| anyhow!(e.to_string()))?;
94
95 if let Ok(utils) = lua.globals().get::<Table>("UTILS") {
98 let mock_extract = lua
99 .create_function(|_, (_source, _out_dir): (String, String)| Ok(()))
100 .map_err(|e| anyhow!(e.to_string()))?;
101 utils
102 .set("EXTRACT", mock_extract)
103 .map_err(|e| anyhow!(e.to_string()))?;
104 }
105
106 let mock_cmd = lua
108 .create_function(|_, _command: String| {
109 Ok((String::new(), String::new(), 0))
110 })
111 .map_err(|e| anyhow!(e.to_string()))?;
112 lua.globals()
113 .set("cmd", mock_cmd)
114 .map_err(|e| anyhow!(e.to_string()))?;
115
116 let lua_code = fs::read_to_string(package_file)?;
118 lua.load(&lua_code).exec().map_err(|e| {
119 anyhow!(
120 "Failed to execute Lua package file '{}' for bundling:\n{}",
121 package_file.display(),
122 e
123 )
124 })?;
125
126 let args = lua.create_table().map_err(|e| anyhow!(e.to_string()))?;
127
128 if let Ok(pkg_fn) = lua.globals().get::<mlua::Function>("package") {
130 let _ = pkg_fn.call::<()>(args.clone());
131 }
132
133 let mut files_to_include = HashSet::new();
134
135 let pkg_filename = package_file
137 .file_name()
138 .ok_or_else(|| anyhow!("Invalid package file"))?;
139 files_to_include.insert(pkg_filename.to_string_lossy().to_string());
140
141 if let Ok(refs) = lua.globals().get::<Table>("__ZoiReferencedFiles") {
143 for val in refs.sequence_values::<String>() {
144 files_to_include.insert(val.map_err(|e| anyhow!(e.to_string()))?);
145 }
146 }
147
148 if let Ok(ops) = lua.globals().get::<Table>("__ZoiBuildOperations") {
150 for op in ops.sequence_values::<Table>() {
151 let op = op.map_err(|e| anyhow!(e.to_string()))?;
152
153 if let Ok(source) = op.get::<String>("source")
155 && let Some(rel) = source.strip_prefix("${pkgluadir}/")
156 {
157 files_to_include.insert(rel.to_string());
158 }
159
160 if let Ok(target) = op.get::<String>("target")
162 && let Some(rel) = target.strip_prefix("${pkgluadir}/")
163 {
164 files_to_include.insert(rel.to_string());
165 }
166 }
167 }
168
169 println!("{} Fetching upstream sources...", "::".bold().blue());
171 let fetch_dir = Builder::new().prefix("zoi-bundle-fetch-").tempdir()?;
172
173 let lua_fetch = Lua::new();
175
176 let pkg_meta_table_f = lua_fetch
178 .create_table()
179 .map_err(|e| anyhow!(e.to_string()))?;
180 let pkg_deps_table_f = lua_fetch
181 .create_table()
182 .map_err(|e| anyhow!(e.to_string()))?;
183 let pkg_updates_table_f = lua_fetch
184 .create_table()
185 .map_err(|e| anyhow!(e.to_string()))?;
186 let pkg_hooks_table_f = lua_fetch
187 .create_table()
188 .map_err(|e| anyhow!(e.to_string()))?;
189 let pkg_service_table_f = lua_fetch
190 .create_table()
191 .map_err(|e| anyhow!(e.to_string()))?;
192 lua_fetch
193 .globals()
194 .set("__ZoiPackageMeta", pkg_meta_table_f)
195 .map_err(|e| anyhow!(e.to_string()))?;
196 lua_fetch
197 .globals()
198 .set("__ZoiPackageDeps", pkg_deps_table_f)
199 .map_err(|e| anyhow!(e.to_string()))?;
200 lua_fetch
201 .globals()
202 .set("__ZoiPackageUpdates", pkg_updates_table_f)
203 .map_err(|e| anyhow!(e.to_string()))?;
204 lua_fetch
205 .globals()
206 .set("__ZoiPackageHooks", pkg_hooks_table_f)
207 .map_err(|e| anyhow!(e.to_string()))?;
208 lua_fetch
209 .globals()
210 .set("__ZoiPackageService", pkg_service_table_f)
211 .map_err(|e| anyhow!(e.to_string()))?;
212
213 let pkg_global_table_f = lua_fetch
214 .create_table()
215 .map_err(|e| anyhow!(e.to_string()))?;
216 lua_fetch
217 .globals()
218 .set("PKG", pkg_global_table_f)
219 .map_err(|e| anyhow!(e.to_string()))?;
220
221 zoi_lua::functions::setup_lua_environment(
222 &lua_fetch,
223 &platform,
224 version_override,
225 package_file.to_str(),
226 None,
227 Some(fetch_dir.path().to_str().unwrap_or("")),
228 Some("/tmp/mock-staging"),
229 None,
230 None,
231 Some(bundle_type),
232 true )
234 .map_err(|e| anyhow!(e.to_string()))?;
235
236 lua_fetch
237 .globals()
238 .set("BUILD_TYPE", bundle_type)
239 .map_err(|e| anyhow!(e.to_string()))?;
240
241 lua_fetch
242 .globals()
243 .set(
244 "BUILD_DIR",
245 fetch_dir
246 .path()
247 .to_str()
248 .ok_or_else(|| anyhow!("Invalid fetch path"))?
249 )
250 .map_err(|e| anyhow!(e.to_string()))?;
251
252 zoi_lua::api::system::add_cmd_util(&lua_fetch, true)
254 .map_err(|e| anyhow!(e.to_string()))?;
255
256 lua_fetch.load(&lua_code).exec().map_err(|e| {
258 anyhow!(
259 "Failed to execute Lua package file '{}' during fetch:\n{}",
260 package_file.display(),
261 e
262 )
263 })?;
264
265 if let Ok(prep_fn) = lua_fetch.globals().get::<mlua::Function>("prepare") {
266 println!(" Running prepare()...");
267 let args_fetch = lua_fetch
268 .create_table()
269 .map_err(|e| anyhow!(e.to_string()))?;
270 prep_fn.call::<()>(args_fetch).map_err(|e| {
271 anyhow!(
272 "The 'prepare' function in '{}' failed during bundling:\n{}",
273 package_file.display(),
274 e
275 )
276 })?;
277 }
278
279 let pkg_dir = package_file
281 .parent()
282 .ok_or_else(|| anyhow!("Could not get parent directory"))?;
283
284 let final_pkg_meta: Table = lua
285 .globals()
286 .get("__ZoiPackageMeta")
287 .map_err(|e| anyhow!(e.to_string()))?;
288 let pkg_meta: zoi_core::types::Package = lua
289 .from_value(Value::Table(final_pkg_meta))
290 .map_err(|e| anyhow!(e.to_string()))?;
291
292 let version = version_override
293 .map(ToString::to_string)
294 .or(pkg_meta.version)
295 .unwrap_or_else(|| "unknown".to_string());
296 let output_filename = format!("{}-{}.zsa", pkg_meta.name, version);
297 let output_base =
298 output_dir.map_or_else(|| pkg_dir.to_path_buf(), Path::to_path_buf);
299 let output_path = output_base.join(output_filename);
300
301 let file = File::create(&output_path)?;
302 let encoder = ZstdEncoder::new(file, 0)?.auto_finish();
303 let mut tar_builder = TarBuilder::new(encoder);
304
305 let mut sorted_files: Vec<_> = files_to_include.into_iter().collect();
307 sorted_files.sort();
308
309 for rel_path_str in sorted_files {
310 let rel_path = Path::new(&rel_path_str);
311 let abs_path = pkg_dir.join(rel_path);
312 let is_dir = abs_path.is_dir();
313
314 if is_ignored(rel_path, is_dir) {
315 println!(" Ignored: {rel_path_str}");
316 continue;
317 }
318
319 if abs_path.exists() {
320 if is_dir {
321 let mut it = WalkDir::new(&abs_path).into_iter();
324 loop {
325 let entry = match it.next() {
326 None => break,
327 Some(Err(e)) => return Err(e.into()),
328 Some(Ok(e)) => e
329 };
330 let entry_rel = entry.path().strip_prefix(pkg_dir)?;
331 let entry_is_dir = entry.file_type().is_dir();
332 if is_ignored(entry_rel, entry_is_dir) {
333 if entry_is_dir {
334 it.skip_current_dir();
335 }
336 continue;
337 }
338
339 if entry.file_type().is_file() {
340 tar_builder
341 .append_path_with_name(entry.path(), entry_rel)?;
342 println!(" Included local: {}", entry_rel.display());
343 }
344 }
345 } else {
346 tar_builder.append_path_with_name(&abs_path, &rel_path_str)?;
347 println!(" Included local: {rel_path_str}");
348 }
349 }
350 }
351
352 let mut it = WalkDir::new(fetch_dir.path()).into_iter();
354 loop {
355 let entry = match it.next() {
356 None => break,
357 Some(Err(e)) => return Err(e.into()),
358 Some(Ok(e)) => e
359 };
360
361 if entry.depth() == 0 {
362 continue;
363 }
364
365 let rel_path = entry.path().strip_prefix(fetch_dir.path())?;
366 let rel_path_str = rel_path.to_string_lossy();
367 let is_dir = entry.file_type().is_dir();
368
369 if is_ignored(rel_path, is_dir) {
370 if is_dir {
371 it.skip_current_dir();
372 }
373 println!(" Ignored fetch: {rel_path_str}");
374 continue;
375 }
376
377 if entry.file_type().is_dir() {
378 continue;
380 }
381
382 tar_builder.append_path_with_name(entry.path(), rel_path)?;
383 println!(" Included fetch: {rel_path_str}");
384 }
385
386 let mut header = tar::Header::new_gnu();
388 header.set_path(".zoi-prepared")?;
389 header.set_size(0);
390 header.set_cksum();
391 tar_builder.append(&header, &[][..])?;
392
393 tar_builder.finish()?;
394 println!(
395 "{} Successfully created bundle: {}",
396 "::".bold().green(),
397 output_path.display()
398 );
399
400 if let Some(key_id) = sign {
401 println!(
402 "{} Signing bundle with key '{}'...",
403 "::".bold().blue(),
404 key_id.cyan()
405 );
406 let signature_path =
407 PathBuf::from(format!("{}.sig", output_path.display()));
408 if signature_path.exists() {
409 fs::remove_file(&signature_path)?;
410 }
411 zoi_core::pgp::sign_detached(&output_path, &signature_path, &key_id)?;
412 println!(
413 "{} Successfully created signature: {}",
414 "::".bold().green(),
415 signature_path.display()
416 );
417 }
418
419 Ok(())
420}