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