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 let mut scope_mapping = ScopeMapping::default();
704 super::pool::pool_files(
705 v_staging.path(),
706 &pool_dir,
707 &mut pool,
708 &mut scope_mapping,
709 fakeroot
710 )?;
711
712 sub_mapping.scopes.insert(*scope, scope_mapping);
713
714 if *scope == pkg_for_meta.scope {
715 if let Ok(verify_fn) =
717 lua.globals().get::<mlua::Function>("verify")
718 {
719 let verification_passed: bool = match verify_fn
720 .call::<mlua::Value>(args.clone())
721 {
722 Ok(mlua::Value::Boolean(b)) => b,
723 Ok(_) => true, Err(e) => {
725 return Err(anyhow!("Verification failed: {e}"));
726 }
727 };
728 if !verification_passed {
729 return Err(anyhow!("Package verification failed."));
730 }
731 }
732 }
733 }
734 mappings.insert(sub_package, sub_mapping);
735 }
736
737 if platform.starts_with("linux")
738 && let Err(e) = super::relocate::relocate_elfs(&pool_dir, quiet)
739 {
740 eprintln!(
741 "{} Failed to relocate ELF binaries in pool: {e}",
742 "Warning:".yellow(),
743 );
744 }
745
746 let pooled_manifest = PooledZpaManifest {
747 version: "2".to_string(),
748 pool,
749 mappings
750 };
751
752 let manifest_json = serde_json::to_string_pretty(&pooled_manifest)?;
753 fs::write(staging_dir.join("manifest.json"), manifest_json)?;
754
755 fs::copy(
756 package_file,
757 staging_dir.join(
758 package_file
759 .file_name()
760 .ok_or_else(|| anyhow!("package_file should have a name"))?
761 )
762 )?;
763
764 let output_filename =
765 format!("{}-{version}-{platform}.zpa", pkg_for_meta.name);
766 let output_base = if let Some(dir) = output_dir {
767 dir.to_path_buf()
768 } else {
769 package_file
770 .parent()
771 .ok_or_else(|| {
772 anyhow!("package_file should have a parent directory")
773 })?
774 .to_path_buf()
775 };
776 let output_path = output_base.join(output_filename);
777
778 {
779 let file = File::create(&output_path)?;
780 let encoder = ZstdEncoder::new(file, 0)?.auto_finish();
781 let mut tar_builder = TarBuilder::new(encoder);
782
783 if fakeroot {
784 if !quiet {
785 println!(
786 "{} Applying fakeroot (UID/GID 0) to archive...",
787 "::".bold().blue()
788 );
789 }
790 for entry in WalkDir::new(&staging_dir).min_depth(1) {
791 let entry = entry?;
792 let path = entry.path();
793 let rel_path = path.strip_prefix(&staging_dir)?;
794
795 let mut header = tar::Header::new_gnu();
796 let metadata = fs::symlink_metadata(path)?;
797
798 header.set_metadata(&metadata);
799 header.set_uid(0);
800 header.set_gid(0);
801 header.set_username("root")?;
802 header.set_groupname("root")?;
803
804 if metadata.is_dir() {
805 tar_builder.append_data(
806 &mut header,
807 rel_path,
808 std::io::empty()
809 )?;
810 } else if metadata.is_symlink() {
811 let target = fs::read_link(path)?;
812 tar_builder.append_link(&mut header, rel_path, target)?;
813 } else {
814 let mut file = File::open(path)?;
815 tar_builder.append_data(
816 &mut header,
817 rel_path,
818 &mut file
819 )?;
820 }
821 }
822 } else {
823 tar_builder.append_dir_all(".", &staging_dir)?;
824 }
825 tar_builder.finish()?;
826 }
827
828 let mut files_list = std::collections::HashSet::new();
830
831 for sub_pkg in pooled_manifest.mappings.values() {
833 for scope_mapping in sub_pkg.scopes.values() {
834 for f in &scope_mapping.files {
835 files_list.insert(f.dest.clone());
836 }
837 for s in &scope_mapping.symlinks {
838 files_list.insert(s.link.clone());
839 }
840 for d in &scope_mapping.dirs {
841 let mut dir_path = d.path.clone();
843 if !dir_path.ends_with('/') {
844 dir_path.push('/');
845 }
846 files_list.insert(dir_path);
847 }
848 }
849 }
850
851 let mut sorted_files: Vec<_> = files_list.into_iter().collect();
852 sorted_files.sort();
853
854 let files_manifest_path =
855 PathBuf::from(format!("{}.files", output_path.display()));
856 fs::write(&files_manifest_path, sorted_files.join("\n"))?;
857
858 let hash_path = PathBuf::from(format!("{}.hash", output_path.display()));
859 let output_path_str = output_path.to_str().ok_or_else(|| {
860 anyhow!(
861 "Output path contains invalid UTF-8: {}",
862 output_path.display()
863 )
864 })?;
865 let hash = zoi_core::hash::calculate_file_hash(
866 Path::new(output_path_str),
867 zoi_core::hash::HashAlgorithm::Sha512
868 )?;
869 fs::write(
870 &hash_path,
871 format!(
872 "{hash} {}\n",
873 output_path
874 .file_name()
875 .ok_or_else(|| anyhow!("output_path should have a name"))?
876 .to_str()
877 .ok_or_else(|| anyhow!(
878 "output_filename should be valid UTF-8"
879 ))?
880 )
881 )?;
882
883 let size_path = PathBuf::from(format!("{}.size", output_path.display()));
884 let compressed_size = fs::metadata(&output_path)?.len();
885 let uncompressed_size: u64 = WalkDir::new(&staging_dir)
886 .into_iter()
887 .filter_map(Result::ok)
888 .filter(|e| e.file_type().is_file())
889 .map(|e| e.metadata().map_or(0, |m| m.len()))
890 .sum();
891 fs::write(
892 &size_path,
893 format!("down: {compressed_size}\ninstall: {uncompressed_size}\n")
894 )?;
895
896 if !quiet {
897 println!(
898 "{}",
899 format!("Successfully built package: {}", output_path.display())
900 .green()
901 );
902 }
903
904 if let Some(key_id) = sign_key {
905 if !quiet {
906 println!("Signing package with key '{}'...", key_id.cyan());
907 }
908 let signature_path =
909 PathBuf::from(format!("{}.sig", output_path.display()));
910 if signature_path.exists() {
911 fs::remove_file(&signature_path)?;
912 }
913 zoi_core::pgp::sign_detached(&output_path, &signature_path, key_id)?;
914 if !quiet {
915 println!(
916 "{}",
917 format!(
918 "Successfully created signature: {}",
919 signature_path.display()
920 )
921 .green()
922 );
923 }
924 }
925
926 Ok(())
927}
928
929pub fn run(
939 package_file: &Path,
940 build_type: Option<&str>,
941 platforms: &[String],
942 sign_key: Option<String>,
943 output_dir: Option<&Path>,
944 version_override: Option<&str>,
945 sub_packages: Option<Vec<String>>,
946 quiet: bool,
947 method: &str,
948 image: Option<&str>,
949 fakeroot: bool,
950 install_deps: bool,
951 test: bool
952) -> Result<()> {
953 let mut _temp_zsa_dir = None;
954 let mut actual_package_file = package_file.to_path_buf();
955 let mut default_output_dir = None;
956
957 if package_file.to_string_lossy().ends_with(".zsa") {
958 if !quiet {
959 println!(
960 "{} Extracting source bundle: {}",
961 "::".bold().blue(),
962 package_file.display()
963 );
964 }
965
966 if output_dir.is_none() {
967 default_output_dir = package_file.parent().map(Path::to_path_buf);
968 }
969
970 let temp_dir = Builder::new().prefix("zoi-zsa-extract-").tempdir()?;
971 let file = File::open(package_file)?;
972 let decoder = ZstdDecoder::new(file)?;
973 let mut archive = Archive::new(decoder);
974 archive.unpack(temp_dir.path())?;
975
976 let mut pkg_lua = None;
978 for entry in WalkDir::new(temp_dir.path())
979 .into_iter()
980 .filter_map(Result::ok)
981 {
982 if entry.file_name().to_string_lossy().ends_with(".pkg.lua") {
983 pkg_lua = Some(entry.path().to_path_buf());
984 break;
985 }
986 }
987
988 actual_package_file = pkg_lua.ok_or_else(|| {
989 anyhow!("Could not find .pkg.lua file inside the .zsa bundle.")
990 })?;
991 _temp_zsa_dir = Some(temp_dir);
992 }
993
994 let package_file = actual_package_file.as_path();
995 let output_dir = output_dir.or(default_output_dir.as_deref());
996
997 if method == "docker" {
998 let docker_image = image.ok_or_else(|| {
999 anyhow!(
1000 "An image must be specified when using the 'docker' build \
1001 method."
1002 )
1003 })?;
1004 return super::docker::run(
1005 package_file,
1006 build_type,
1007 platforms,
1008 sign_key,
1009 output_dir,
1010 version_override,
1011 sub_packages,
1012 docker_image,
1013 fakeroot,
1014 install_deps,
1015 test
1016 );
1017 }
1018
1019 if method == "bwrap" {
1020 return super::bwrap::run(
1021 package_file,
1022 build_type,
1023 platforms,
1024 sign_key,
1025 output_dir,
1026 version_override,
1027 sub_packages,
1028 fakeroot,
1029 install_deps,
1030 test
1031 );
1032 }
1033
1034 if !quiet {
1035 println!("Building package from: {}", package_file.display());
1036 }
1037
1038 let platforms_to_build: Vec<String> =
1039 if platforms.contains(&"current".to_string()) {
1040 let mut p = platforms.to_vec();
1041 p.retain(|x| x != "current");
1042 p.push(utils::get_platform()?);
1043 p
1044 } else {
1045 platforms.to_vec()
1046 };
1047
1048 if platforms.contains(&"all".to_string()) {
1049 return Err(anyhow!(
1050 "Building for 'all' platforms is not supported in this flow yet. \
1051 Please specify platforms explicitly."
1052 ));
1053 }
1054
1055 let mut any_failed = false;
1056
1057 for platform in &platforms_to_build {
1058 if !quiet {
1059 println!(
1060 "{} Building for platform: {}",
1061 "::".bold().blue(),
1062 platform.cyan()
1063 );
1064 }
1065 if let Err(e) = build_for_platform(
1066 package_file,
1067 build_type,
1068 platform,
1069 sign_key.as_ref(),
1070 output_dir,
1071 version_override,
1072 sub_packages.as_ref(),
1073 quiet,
1074 fakeroot,
1075 install_deps,
1076 test
1077 ) {
1078 eprintln!(
1079 "{}: Failed to build for platform {}: {e}",
1080 "Error".red().bold(),
1081 platform.red(),
1082 );
1083 any_failed = true;
1084 }
1085 }
1086
1087 if any_failed {
1088 return Err(anyhow!("One or more platform builds failed"));
1089 }
1090
1091 Ok(())
1092}