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