1use anyhow::{Result, anyhow};
11use colored::*;
12use mlua::{Lua, LuaSerdeExt, Table};
13use std::collections::BTreeMap;
14use std::fs::{self, File};
15use std::path::{Path, PathBuf};
16use tar::{Archive, Builder as TarBuilder};
17use tempfile::Builder;
18use walkdir::WalkDir;
19use zoi_core::types::{
20 self, PoolFileEntry, PooledZpaManifest, Scope, ScopeMapping, SubPackageMapping,
21};
22use zoi_core::utils;
23use zoi_lua;
24use zoi_resolver::resolve;
25use zstd::stream::read::Decoder as ZstdDecoder;
26use zstd::stream::write::Encoder as ZstdEncoder;
27
28pub fn resolve_build_type(
29 requested: Option<&str>,
30 supported: &[String],
31 pkg_name: &str,
32) -> Result<Option<String>> {
33 if let Some(t) = requested {
34 if !supported.iter().any(|s| s == t) {
35 return Err(anyhow!(
36 "Build type '{}' not supported by package '{}'. Supported types: {:?}",
37 t,
38 pkg_name,
39 supported
40 ));
41 }
42 return Ok(Some(t.to_string()));
43 }
44
45 if supported.iter().any(|t| t == "pre-compiled") {
46 Ok(Some("pre-compiled".to_string()))
47 } else if supported.iter().any(|t| t == "source") {
48 Ok(Some("source".to_string()))
49 } else if let Some(first) = supported.first() {
50 Ok(Some(first.clone()))
51 } else {
52 Ok(None)
53 }
54}
55
56pub fn get_build_dependencies(
57 package_file: &Path,
58 build_type: Option<&str>,
59 platform: &str,
60 version_override: Option<&str>,
61 quiet: bool,
62) -> Result<Option<Vec<String>>> {
63 let pkg_for_meta = zoi_lua::parser::parse_lua_package_for_platform(
64 package_file
65 .to_str()
66 .ok_or_else(|| anyhow!("Path contains invalid UTF-8 characters: {:?}", package_file))?,
67 platform,
68 version_override,
69 None,
70 quiet,
71 )?;
72
73 let resolved_build_type =
74 match resolve_build_type(build_type, &pkg_for_meta.types, &pkg_for_meta.name)? {
75 Some(t) => t,
76 None => return Ok(None),
77 };
78
79 if let Some(deps) = &pkg_for_meta.dependencies
80 && let Some(build_deps) = &deps.build
81 {
82 let group = match build_deps {
83 types::BuildDependencies::Group(g) => Some(g),
84 types::BuildDependencies::Typed(t) => t.types.get(&resolved_build_type),
85 };
86
87 if let Some(g) = group {
88 let mut all_deps = Vec::new();
89 collect_deps_from_group_no_prompt(g, &mut all_deps);
90 return Ok(Some(all_deps));
91 }
92 }
93
94 Ok(None)
95}
96
97pub fn get_test_dependencies(
98 package_file: &Path,
99 platform: &str,
100 version_override: Option<&str>,
101 quiet: bool,
102) -> Result<Option<Vec<String>>> {
103 let pkg_for_meta = zoi_lua::parser::parse_lua_package_for_platform(
104 package_file
105 .to_str()
106 .ok_or_else(|| anyhow!("Path contains invalid UTF-8 characters: {:?}", package_file))?,
107 platform,
108 version_override,
109 None,
110 quiet,
111 )?;
112
113 if let Some(deps) = &pkg_for_meta.dependencies
114 && let Some(test_deps) = &deps.test
115 {
116 let mut all_deps = Vec::new();
117 collect_deps_from_group_no_prompt(test_deps, &mut all_deps);
118 return Ok(Some(all_deps));
119 }
120
121 Ok(None)
122}
123
124fn collect_deps_from_group_no_prompt(group: &types::DependencyGroup, deps: &mut Vec<String>) {
125 match group {
126 types::DependencyGroup::Simple(d) => {
127 deps.extend(d.clone());
128 }
129 types::DependencyGroup::Complex(g) => {
130 deps.extend(g.required.clone());
131 deps.extend(g.optional.clone());
132 for option_group in &g.options {
133 if option_group.all {
134 deps.extend(option_group.depends.clone());
135 } else if let Some(dep) = option_group.depends.first() {
136 deps.push(dep.clone());
137 }
138 }
139 if let Some(sub_deps_map) = &g.sub_packages {
140 for sub_group in sub_deps_map.values() {
141 collect_deps_from_group_no_prompt(sub_group, deps);
142 }
143 }
144 }
145 }
146}
147
148fn process_build_operations(
149 lua: &Lua,
150 _sub_package: &str,
151 pkg_lua_dir_str: &str,
152 build_dir_path: &Path,
153 target_staging_dir: &Path,
154 quiet: bool,
155) -> Result<()> {
156 if let Ok(build_ops) = lua.globals().get::<Table>("__ZoiBuildOperations") {
157 for op in build_ops.sequence_values::<Table>() {
158 let op = op.map_err(|e| anyhow!(e.to_string()))?;
159 let op_type: String = op.get("op").map_err(|e| anyhow!(e.to_string()))?;
160
161 let resolve_dest = |dest: String| -> String {
162 dest.replace("${pkgstore}", "pkgstore")
163 .replace("${createpkgdir}", "createpkgdir")
164 .replace("${usrroot}", "usrroot")
165 .replace("${usrhome}", "usrhome")
166 };
167
168 match op_type.as_str() {
169 "zcp" => {
170 let source: String = op.get("source").map_err(|e| anyhow!(e.to_string()))?;
171 let destination: String =
172 op.get("destination").map_err(|e| anyhow!(e.to_string()))?;
173
174 let mut source_path = if source.contains("${pkgluadir}") {
175 Path::new(&source.replace("${pkgluadir}", pkg_lua_dir_str)).to_path_buf()
176 } else {
177 build_dir_path.join(&source)
178 };
179
180 if !source_path.exists() && !source.contains("${pkgluadir}") {
181 let fallback = Path::new(pkg_lua_dir_str).join(&source);
182 if fallback.exists() {
183 source_path = fallback;
184 }
185 }
186
187 let dest_rel = resolve_dest(destination);
188
189 if !utils::is_safe_path(target_staging_dir, Path::new(&dest_rel)) {
190 return Err(anyhow!(
191 "Path traversal detected in zcp destination: {}",
192 dest_rel
193 ));
194 }
195
196 let dest_path = target_staging_dir.join(&dest_rel);
197
198 let source_metadata = match source_path.symlink_metadata() {
199 Ok(m) => m,
200 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
201 if !quiet {
202 println!(
203 "{} Skipping missing zcp source: '{}' (not found in build dir or package dir)",
204 "::".bold().yellow(),
205 source
206 );
207 }
208 continue;
209 }
210 Err(e) => {
211 return Err(anyhow!(
212 "Failed to get metadata for '{}' (resolved from '{}'): {}",
213 source_path.display(),
214 source,
215 e
216 ));
217 }
218 };
219
220 if source_metadata.is_dir() {
221 for entry in WalkDir::new(&source_path)
222 .into_iter()
223 .filter_map(|e| e.ok())
224 {
225 let rel_entry = entry.path().strip_prefix(&source_path)?;
226 let target_path = dest_path.join(rel_entry);
227
228 let metadata = match entry.path().symlink_metadata() {
229 Ok(m) => m,
230 Err(_) => continue, };
232
233 if metadata.is_dir() {
234 fs::create_dir_all(&target_path)?;
235 } else if metadata.is_symlink() {
236 if let Some(p) = target_path.parent() {
237 fs::create_dir_all(p)?;
238 }
239 if let Ok(link_target) = fs::read_link(entry.path()) {
240 utils::symlink_file(&link_target, &target_path)?;
241 }
242 } else {
243 if let Some(p) = target_path.parent() {
244 fs::create_dir_all(p)?;
245 }
246 let _ = fs::copy(entry.path(), &target_path);
247 }
248 }
249 } else if source_metadata.is_symlink() {
250 if let Some(parent) = dest_path.parent() {
251 fs::create_dir_all(parent)?;
252 }
253 if let Ok(link_target) = fs::read_link(&source_path) {
254 utils::symlink_file(&link_target, &dest_path)?;
255 }
256 } else {
257 if let Some(parent) = dest_path.parent() {
258 fs::create_dir_all(parent)?;
259 }
260 fs::copy(&source_path, &dest_path)?;
261 }
262
263 if !quiet {
264 println!("Staged '{}' to '{}'", source, dest_rel);
265 }
266 }
267 "zln" => {
268 let mut target: String =
269 op.get("target").map_err(|e| anyhow!(e.to_string()))?;
270 let link: String = op.get("link").map_err(|e| anyhow!(e.to_string()))?;
271
272 let dest_rel = resolve_dest(link);
273
274 target = target.replace("${pkgstore}", "pkgstore");
275 target = target.replace("${createpkgdir}", "createpkgdir");
276 target = target.replace("${usrroot}", "usrroot");
277 target = target.replace("${usrhome}", "usrhome");
278
279 if !utils::is_safe_path(target_staging_dir, Path::new(&dest_rel)) {
280 return Err(anyhow!("Path traversal detected in zln link: {}", dest_rel));
281 }
282
283 let link_path = target_staging_dir.join(&dest_rel);
284 if let Some(parent) = link_path.parent() {
285 fs::create_dir_all(parent)?;
286 }
287
288 utils::symlink_file(Path::new(&target), &link_path)?;
289 if !quiet {
290 println!("Created symlink '{}' -> '{}'", dest_rel, target);
291 }
292 }
293 "zchmod" => {
294 let path: String = op.get("path").map_err(|e| anyhow!(e.to_string()))?;
295 let mode: u32 = op.get("mode").map_err(|e| anyhow!(e.to_string()))?;
296
297 let dest_rel = resolve_dest(path);
298
299 if !utils::is_safe_path(target_staging_dir, Path::new(&dest_rel)) {
300 return Err(anyhow!(
301 "Path traversal detected in zchmod path: {}",
302 dest_rel
303 ));
304 }
305
306 #[cfg(unix)]
307 {
308 let full_path = target_staging_dir.join(&dest_rel);
309 use std::os::unix::fs::PermissionsExt;
310 fs::set_permissions(full_path, fs::Permissions::from_mode(mode))?;
311 }
312 if !quiet {
313 println!("Set permissions {} on '{}'", mode, dest_rel);
314 }
315 }
316 "zchown" => {
317 let path: String = op.get("path").map_err(|e| anyhow!(e.to_string()))?;
318 let owner: String = op.get("owner").map_err(|e| anyhow!(e.to_string()))?;
319 let group: String = op.get("group").map_err(|e| anyhow!(e.to_string()))?;
320
321 let dest_rel = resolve_dest(path);
322
323 if !utils::is_safe_path(target_staging_dir, Path::new(&dest_rel)) {
324 return Err(anyhow!(
325 "Path traversal detected in zchown path: {}",
326 dest_rel
327 ));
328 }
329
330 #[cfg(unix)]
331 {
332 let full_path = target_staging_dir.join(&dest_rel);
333 utils::set_path_owner(&full_path, &owner, &group)?;
334 }
335 if !quiet {
336 println!("Set ownership {}:{} on '{}'", owner, group, dest_rel);
337 }
338 }
339 "zmkdir" => {
340 let path: String = op.get("path").map_err(|e| anyhow!(e.to_string()))?;
341
342 let dest_rel = resolve_dest(path);
343
344 if !utils::is_safe_path(target_staging_dir, Path::new(&dest_rel)) {
345 return Err(anyhow!(
346 "Path traversal detected in zmkdir path: {}",
347 dest_rel
348 ));
349 }
350
351 let full_path = target_staging_dir.join(&dest_rel);
352 fs::create_dir_all(full_path)?;
353 if !quiet {
354 println!("Created directory '{}'", dest_rel);
355 }
356 }
357 _ => {}
358 }
359 }
360 }
361 Ok(())
362}
363
364fn build_for_platform(
365 package_file: &Path,
366 build_type: Option<&str>,
367 platform: &str,
368 sign_key: &Option<String>,
369 output_dir: Option<&Path>,
370 version_override: Option<&str>,
371 sub_packages: Option<&Vec<String>>,
372 quiet: bool,
373 fakeroot: bool,
374 _install_deps: bool,
375 _test: bool,
376) -> Result<()> {
377 let pkg_lua_dir = package_file
378 .parent()
379 .filter(|p| !p.as_os_str().is_empty())
380 .unwrap_or(Path::new("."));
381 let pkg_lua_dir_str = pkg_lua_dir
382 .to_str()
383 .ok_or_else(|| anyhow!("Could not get parent directory of package file"))?;
384 let pkg_for_meta = zoi_lua::parser::parse_lua_package_for_platform(
385 package_file
386 .to_str()
387 .ok_or_else(|| anyhow!("Path contains invalid UTF-8 characters: {:?}", package_file))?,
388 platform,
389 version_override,
390 None,
391 quiet,
392 )?;
393
394 if let Some(allowed_platforms) = &pkg_for_meta.platforms
395 && !utils::is_platform_compatible(platform, allowed_platforms)
396 {
397 if !quiet {
398 println!(
399 "{} Skipping build for platform {}: package only supports {:?}",
400 "::".bold().yellow(),
401 platform.cyan(),
402 allowed_platforms
403 );
404 }
405 return Ok(());
406 }
407
408 let resolved_build_type = match resolve_build_type(
409 build_type,
410 &pkg_for_meta.types,
411 &pkg_for_meta.name,
412 )? {
413 Some(t) => t,
414 None => {
415 if !quiet {
416 println!(
417 "{} Skipping build for package '{}': no build types supported (likely a collection or template).",
418 "::".bold().yellow(),
419 pkg_for_meta.name
420 );
421 }
422 return Ok(());
423 }
424 };
425
426 let version = if let Some(v) = version_override {
427 v.to_string()
428 } else {
429 resolve::get_default_version(&pkg_for_meta, None)?
430 };
431
432 let build_dir = Builder::new()
433 .prefix(&format!("zoi-build-{}-{}", pkg_for_meta.name, platform))
434 .tempdir()?;
435 if !quiet {
436 println!("Using build directory: {}", build_dir.path().display());
437 }
438
439 let mut skip_prepare = false;
440 if let Some(parent) = package_file.parent()
441 && parent.join(".zoi-prepared").exists()
442 {
443 if !quiet {
444 println!(
445 "{} Detected pre-prepared source bundle, copying files...",
446 "::".bold().blue()
447 );
448 }
449 utils::copy_dir_all(parent, build_dir.path())?;
450 skip_prepare = true;
451 }
452
453 let staging_dir = build_dir.path().join("staging");
454 fs::create_dir_all(&staging_dir)?;
455
456 let pool_dir = staging_dir.join("pool");
457 fs::create_dir_all(&pool_dir)?;
458
459 let mut pool: BTreeMap<String, PoolFileEntry> = BTreeMap::new();
460 let mut mappings: BTreeMap<String, SubPackageMapping> = BTreeMap::new();
461
462 let subs_to_build = if let Some(subs) = sub_packages {
463 subs.clone()
464 } else if let Some(subs) = &pkg_for_meta.sub_packages {
465 if subs.contains(&"".to_string()) || subs.contains(&"main".to_string()) {
466 subs.clone()
467 } else {
468 let mut all_subs = vec!["".to_string()];
469 all_subs.extend(subs.clone());
470 all_subs
471 }
472 } else {
473 vec!["".to_string()]
474 };
475
476 let scopes_to_process =
477 pkg_for_meta
478 .scopes
479 .clone()
480 .unwrap_or(vec![Scope::User, Scope::System, Scope::Project]);
481
482 let lua_code = fs::read_to_string(package_file)?;
483
484 for sub_package in subs_to_build {
485 let sub_pkg_name = if sub_package.is_empty() {
486 None
487 } else {
488 Some(sub_package.as_str())
489 };
490
491 if !quiet && let Some(sub) = sub_pkg_name {
492 println!(
493 "{} Building sub-package: {}",
494 "::".bold().blue(),
495 sub.cyan()
496 );
497 }
498
499 {
501 let lua_sub = Lua::new();
502 zoi_lua::functions::setup_lua_environment(
503 &lua_sub,
504 platform,
505 Some(&version),
506 package_file.to_str(),
507 None,
508 Some(build_dir.path().to_str().unwrap_or("")),
509 None, sub_pkg_name,
511 Some(pkg_for_meta.scope),
512 Some(resolved_build_type.as_str()),
513 quiet,
514 )
515 .map_err(|e| anyhow!(e.to_string()))?;
516
517 lua_sub
518 .load(&lua_code)
519 .exec()
520 .map_err(|e| anyhow!(e.to_string()))?;
521
522 let args_sub = lua_sub.create_table().map_err(|e| anyhow!(e.to_string()))?;
523 if let Some(sub) = sub_pkg_name {
524 args_sub
525 .set("sub", sub)
526 .map_err(|e| anyhow!(e.to_string()))?;
527 }
528
529 if !skip_prepare
530 && let Ok(prepare_fn) = lua_sub.globals().get::<mlua::Function>("prepare")
531 {
532 if !quiet {
533 println!("Running prepare()...");
534 }
535 prepare_fn
536 .call::<()>(args_sub.clone())
537 .map_err(|e| anyhow!(e.to_string()))?;
538 }
539
540 if let Ok(build_fn) = lua_sub.globals().get::<mlua::Function>("build") {
541 if !quiet {
542 println!("Running build()...");
543 }
544 build_fn
545 .call::<()>(args_sub)
546 .map_err(|e| anyhow!(e.to_string()))?;
547 }
548 }
549
550 let mut sub_mapping = SubPackageMapping {
551 scopes: BTreeMap::new(),
552 };
553
554 for scope in &scopes_to_process {
555 if !quiet {
556 println!(" {} Staging for scope: {:?}", "::".bold().blue(), scope);
557 }
558
559 let lua = Lua::new();
560 let v_staging = Builder::new().prefix("zoi-vstage-").tempdir()?;
561
562 zoi_lua::functions::setup_lua_environment(
563 &lua,
564 platform,
565 Some(&version),
566 package_file.to_str(),
567 None,
568 Some(build_dir.path().to_str().unwrap_or("")),
569 Some(v_staging.path().to_str().unwrap_or("")),
570 sub_pkg_name,
571 Some(*scope),
572 Some(resolved_build_type.as_str()),
573 true, )
575 .map_err(|e| anyhow!(e.to_string()))?;
576
577 let pkg_table = lua
578 .to_value(&pkg_for_meta)
579 .map_err(|e| anyhow!(e.to_string()))?;
580 lua.globals()
581 .set("PKG", pkg_table)
582 .map_err(|e| anyhow!(e.to_string()))?;
583
584 lua.load(&lua_code)
585 .exec()
586 .map_err(|e| anyhow!(e.to_string()))?;
587
588 let args = lua.create_table().map_err(|e| anyhow!(e.to_string()))?;
589 if !sub_package.is_empty() {
590 args.set("sub", sub_package.clone())
591 .map_err(|e| anyhow!(e.to_string()))?;
592 }
593
594 if let Ok(package_fn) = lua.globals().get::<mlua::Function>("package") {
595 package_fn
596 .call::<()>(args.clone())
597 .map_err(|e| anyhow!(e.to_string()))?;
598 }
599
600 process_build_operations(
601 &lua,
602 &sub_package,
603 pkg_lua_dir_str,
604 build_dir.path(),
605 v_staging.path(),
606 true,
607 )?;
608
609 let mut scope_mapping = ScopeMapping::default();
610 super::pool::pool_files(
611 v_staging.path(),
612 &pool_dir,
613 &mut pool,
614 &mut scope_mapping,
615 fakeroot,
616 )?;
617
618 sub_mapping.scopes.insert(*scope, scope_mapping);
619
620 if *scope == pkg_for_meta.scope {
621 if let Ok(verify_fn) = lua.globals().get::<mlua::Function>("verify") {
623 let verification_passed: bool =
624 match verify_fn.call::<mlua::Value>(args.clone()) {
625 Ok(mlua::Value::Boolean(b)) => b,
626 Ok(_) => true, Err(e) => return Err(anyhow!("Verification failed: {}", e)),
628 };
629 if !verification_passed {
630 return Err(anyhow!("Package verification failed."));
631 }
632 }
633 }
634 }
635 mappings.insert(sub_package, sub_mapping);
636 }
637
638 if platform.starts_with("linux")
639 && let Err(e) = super::relocate::relocate_elfs(&pool_dir, quiet)
640 {
641 eprintln!(
642 "{} Failed to relocate ELF binaries in pool: {}",
643 "Warning:".yellow(),
644 e
645 );
646 }
647
648 let pooled_manifest = PooledZpaManifest {
649 version: "2".to_string(),
650 pool,
651 mappings,
652 };
653
654 let manifest_json = serde_json::to_string_pretty(&pooled_manifest)?;
655 fs::write(staging_dir.join("manifest.json"), manifest_json)?;
656
657 fs::copy(
658 package_file,
659 staging_dir.join(
660 package_file
661 .file_name()
662 .ok_or_else(|| anyhow!("package_file should have a name"))?,
663 ),
664 )?;
665
666 let output_filename = format!("{}-{}-{}.zpa", pkg_for_meta.name, version, platform);
667 let output_base = if let Some(dir) = output_dir {
668 dir.to_path_buf()
669 } else {
670 package_file
671 .parent()
672 .ok_or_else(|| anyhow!("package_file should have a parent directory"))?
673 .to_path_buf()
674 };
675 let output_path = output_base.join(output_filename);
676
677 {
678 let file = File::create(&output_path)?;
679 let encoder = ZstdEncoder::new(file, 0)?.auto_finish();
680 let mut tar_builder = TarBuilder::new(encoder);
681
682 if fakeroot {
683 if !quiet {
684 println!(
685 "{} Applying fakeroot (UID/GID 0) to archive...",
686 "::".bold().blue()
687 );
688 }
689 for entry in WalkDir::new(&staging_dir).min_depth(1) {
690 let entry = entry?;
691 let path = entry.path();
692 let rel_path = path.strip_prefix(&staging_dir)?;
693
694 let mut header = tar::Header::new_gnu();
695 let metadata = fs::symlink_metadata(path)?;
696
697 header.set_metadata(&metadata);
698 header.set_uid(0);
699 header.set_gid(0);
700 header.set_username("root")?;
701 header.set_groupname("root")?;
702
703 if metadata.is_dir() {
704 tar_builder.append_data(&mut header, rel_path, std::io::empty())?;
705 } else if metadata.is_symlink() {
706 let target = fs::read_link(path)?;
707 tar_builder.append_link(&mut header, rel_path, target)?;
708 } else {
709 let mut file = File::open(path)?;
710 tar_builder.append_data(&mut header, rel_path, &mut file)?;
711 }
712 }
713 } else {
714 tar_builder.append_dir_all(".", &staging_dir)?;
715 }
716 tar_builder.finish()?;
717 }
718
719 let mut files_list = std::collections::HashSet::new();
721
722 for sub_pkg in pooled_manifest.mappings.values() {
724 for scope_mapping in sub_pkg.scopes.values() {
725 for f in &scope_mapping.files {
726 files_list.insert(f.dest.clone());
727 }
728 for s in &scope_mapping.symlinks {
729 files_list.insert(s.link.clone());
730 }
731 for d in &scope_mapping.dirs {
732 let mut dir_path = d.path.clone();
734 if !dir_path.ends_with('/') {
735 dir_path.push('/');
736 }
737 files_list.insert(dir_path);
738 }
739 }
740 }
741
742 let mut sorted_files: Vec<_> = files_list.into_iter().collect();
743 sorted_files.sort();
744
745 let files_manifest_path = PathBuf::from(format!("{}.files", output_path.display()));
746 fs::write(&files_manifest_path, sorted_files.join("\n"))?;
747
748 let hash_path = PathBuf::from(format!("{}.hash", output_path.display()));
749 let output_path_str = output_path
750 .to_str()
751 .ok_or_else(|| anyhow!("Output path contains invalid UTF-8: {:?}", output_path))?;
752 let hash = zoi_core::hash::calculate_file_hash(
753 Path::new(output_path_str),
754 zoi_core::hash::HashAlgorithm::Sha512,
755 )?;
756 fs::write(
757 &hash_path,
758 format!(
759 "{} {}\n",
760 hash,
761 output_path
762 .file_name()
763 .ok_or_else(|| anyhow!("output_path should have a name"))?
764 .to_str()
765 .ok_or_else(|| anyhow!("output_filename should be valid UTF-8"))?
766 ),
767 )?;
768
769 let size_path = PathBuf::from(format!("{}.size", output_path.display()));
770 let compressed_size = fs::metadata(&output_path)?.len();
771 let uncompressed_size: u64 = WalkDir::new(&staging_dir)
772 .into_iter()
773 .filter_map(|e| e.ok())
774 .filter(|e| e.file_type().is_file())
775 .map(|e| e.metadata().map(|m| m.len()).unwrap_or(0))
776 .sum();
777 fs::write(
778 &size_path,
779 format!(
780 "down: {}\ninstall: {}\n",
781 compressed_size, uncompressed_size
782 ),
783 )?;
784
785 if !quiet {
786 println!(
787 "{}",
788 format!("Successfully built package: {}", output_path.display()).green()
789 );
790 }
791
792 if let Some(key_id) = sign_key {
793 if !quiet {
794 println!("Signing package with key '{}'...", key_id.cyan());
795 }
796 let signature_path = PathBuf::from(format!("{}.sig", output_path.display()));
797 if signature_path.exists() {
798 fs::remove_file(&signature_path)?;
799 }
800 zoi_core::pgp::sign_detached(&output_path, &signature_path, key_id)?;
801 if !quiet {
802 println!(
803 "{}",
804 format!(
805 "Successfully created signature: {}",
806 signature_path.display()
807 )
808 .green()
809 );
810 }
811 }
812
813 Ok(())
814}
815
816pub fn run(
817 package_file: &Path,
818 build_type: Option<&str>,
819 platforms: &[String],
820 sign_key: Option<String>,
821 output_dir: Option<&Path>,
822 version_override: Option<&str>,
823 sub_packages: Option<Vec<String>>,
824 quiet: bool,
825 method: &str,
826 image: Option<&str>,
827 fakeroot: bool,
828 install_deps: bool,
829 test: bool,
830) -> Result<()> {
831 let mut _temp_zsa_dir = None;
832 let mut actual_package_file = package_file.to_path_buf();
833 let mut default_output_dir = None;
834
835 if package_file.to_string_lossy().ends_with(".zsa") {
836 if !quiet {
837 println!(
838 "{} Extracting source bundle: {}",
839 "::".bold().blue(),
840 package_file.display()
841 );
842 }
843
844 if output_dir.is_none() {
845 default_output_dir = package_file.parent().map(|p| p.to_path_buf());
846 }
847
848 let temp_dir = Builder::new().prefix("zoi-zsa-extract-").tempdir()?;
849 let file = File::open(package_file)?;
850 let decoder = ZstdDecoder::new(file)?;
851 let mut archive = Archive::new(decoder);
852 archive.unpack(temp_dir.path())?;
853
854 let mut pkg_lua = None;
856 for entry in WalkDir::new(temp_dir.path())
857 .into_iter()
858 .filter_map(|e| e.ok())
859 {
860 if entry.file_name().to_string_lossy().ends_with(".pkg.lua") {
861 pkg_lua = Some(entry.path().to_path_buf());
862 break;
863 }
864 }
865
866 actual_package_file = pkg_lua
867 .ok_or_else(|| anyhow!("Could not find .pkg.lua file inside the .zsa bundle."))?;
868 _temp_zsa_dir = Some(temp_dir);
869 }
870
871 let package_file = actual_package_file.as_path();
872 let output_dir = output_dir.or(default_output_dir.as_deref());
873
874 if method == "docker" {
875 let docker_image = image.ok_or_else(|| {
876 anyhow!("An image must be specified when using the 'docker' build method.")
877 })?;
878 return super::docker::run(
879 package_file,
880 build_type,
881 platforms,
882 sign_key,
883 output_dir,
884 version_override,
885 sub_packages,
886 docker_image,
887 fakeroot,
888 install_deps,
889 test,
890 );
891 }
892
893 if method == "bwrap" {
894 return super::bwrap::run(
895 package_file,
896 build_type,
897 platforms,
898 sign_key,
899 output_dir,
900 version_override,
901 sub_packages,
902 fakeroot,
903 install_deps,
904 test,
905 );
906 }
907
908 if !quiet {
909 println!("Building package from: {}", package_file.display());
910 }
911
912 let platforms_to_build: Vec<String> = if platforms.contains(&"current".to_string()) {
913 let mut p = platforms.to_vec();
914 p.retain(|x| x != "current");
915 p.push(utils::get_platform()?);
916 p
917 } else {
918 platforms.to_vec()
919 };
920
921 if platforms.contains(&"all".to_string()) {
922 return Err(anyhow!(
923 "Building for 'all' platforms is not supported in this flow yet. Please specify platforms explicitly."
924 ));
925 }
926
927 let mut any_failed = false;
928
929 for platform in &platforms_to_build {
930 if !quiet {
931 println!(
932 "{} Building for platform: {}",
933 "::".bold().blue(),
934 platform.cyan()
935 );
936 }
937 if let Err(e) = build_for_platform(
938 package_file,
939 build_type,
940 platform,
941 &sign_key,
942 output_dir,
943 version_override,
944 sub_packages.as_ref(),
945 quiet,
946 fakeroot,
947 install_deps,
948 test,
949 ) {
950 eprintln!(
951 "{}: Failed to build for platform {}: {}",
952 "Error".red().bold(),
953 platform.red(),
954 e
955 );
956 any_failed = true;
957 }
958 }
959
960 if any_failed {
961 return Err(anyhow!("One or more platform builds failed"));
962 }
963
964 Ok(())
965}