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