1use std::collections::HashSet;
2use std::fs::{self, File};
3use std::io::{Read, Seek};
4use std::path::{Path, PathBuf};
5
6use anyhow::{Result, anyhow};
7use colored::Colorize;
8use tar::Archive;
9use tempfile::Builder;
10use walkdir::WalkDir;
11use zoi_core::types;
12use zoi_core::utils::{self, copy_dir_all};
13use zoi_resolver::local;
14use zstd::stream::read::Decoder as ZstdDecoder;
15
16fn get_bin_root(scope: types::Scope) -> Result<PathBuf> {
19 match scope {
20 types::Scope::User => {
21 let home_dir = zoi_core::utils::get_user_home()
22 .ok_or_else(|| anyhow!("Could not find home directory."))?;
23 Ok(zoi_core::sysroot::apply_sysroot(
24 home_dir.join(".zoi/pkgs/bin")
25 ))
26 }
27 types::Scope::System => {
28 if cfg!(target_os = "windows") {
29 Ok(zoi_core::sysroot::apply_sysroot(PathBuf::from(
30 "C:\\ProgramData\\zoi\\pkgs\\bin"
31 )))
32 } else if zoi_core::utils::is_zoios() {
33 Ok(zoi_core::sysroot::apply_sysroot(PathBuf::from("/usr/bin")))
34 } else {
35 Ok(zoi_core::sysroot::apply_sysroot(PathBuf::from(
36 "/usr/local/bin"
37 )))
38 }
39 }
40 types::Scope::Project => {
41 let current_dir = std::env::current_dir()?;
42 Ok(current_dir.join(".zoi").join("pkgs").join("bin"))
43 }
44 }
45}
46
47fn get_completions_root(scope: types::Scope, shell: &str) -> Result<PathBuf> {
50 match scope {
51 types::Scope::User => {
52 let home_dir = zoi_core::utils::get_user_home()
53 .ok_or_else(|| anyhow!("Could not find home directory."))?;
54 Ok(zoi_core::sysroot::apply_sysroot(
55 home_dir.join(".zoi/pkgs/shell").join(shell)
56 ))
57 }
58 types::Scope::System => {
59 if cfg!(target_os = "windows") {
60 Ok(zoi_core::sysroot::apply_sysroot(PathBuf::from(format!(
61 "C:\\ProgramData\\zoi\\pkgs\\shell\\{shell}"
62 ))))
63 } else if zoi_core::utils::is_zoios() {
64 let base = match shell {
65 "bash" => "/usr/share/bash-completion/completions",
66 "zsh" => "/usr/share/zsh/site-functions",
67 "fish" => "/usr/share/fish/vendor_completions.d",
68 "elvish" => "/usr/share/elvish/lib",
69 _ => "/usr/share/zoi/completions"
70 };
71 Ok(zoi_core::sysroot::apply_sysroot(PathBuf::from(base)))
72 } else {
73 let base = match shell {
74 "bash" => "/usr/share/bash-completion/completions",
75 "zsh" => "/usr/share/zsh/site-functions",
76 "fish" => "/usr/share/fish/vendor_completions.d",
77 "elvish" => "/usr/share/elvish/lib",
78 _ => "/usr/local/share/zoi/completions"
79 };
80 Ok(zoi_core::sysroot::apply_sysroot(PathBuf::from(base)))
81 }
82 }
83 types::Scope::Project => {
84 let current_dir = std::env::current_dir()?;
85 Ok(current_dir
86 .join(".zoi")
87 .join("pkgs")
88 .join("shell")
89 .join(shell))
90 }
91 }
92}
93
94fn create_completion_symlink(source: &Path, link: &Path) -> Result<()> {
97 if link.exists() || link.is_symlink() {
98 fs::remove_file(link)?;
99 }
100 if let Some(parent) = link.parent() {
101 fs::create_dir_all(parent)?;
102 }
103 #[cfg(unix)]
104 {
105 std::os::unix::fs::symlink(source, link)
106 .map_err(|e| anyhow!("Failed to create completion symlink: {e}"))?;
107 }
108 #[cfg(windows)]
109 {
110 std::os::windows::fs::symlink_file(source, link).map_err(|e| {
111 anyhow!("Failed to create completion symlink: {}", e)
112 })?;
113 }
114 Ok(())
115}
116
117fn check_and_handle_file_conflicts(
120 source_dir: &Path,
121 dest_dir: &Path,
122 owned_files: &HashSet<String>,
123 yes: bool
124) -> Result<()> {
125 let mut conflicting_files = Vec::new();
126
127 for entry in WalkDir::new(source_dir)
128 .into_iter()
129 .filter_map(std::result::Result::ok)
130 .skip(1)
131 {
132 if entry.file_type().is_file() {
133 let relative_path = entry.path().strip_prefix(source_dir)?;
134 let dest_path = dest_dir.join(relative_path);
135 if dest_path.exists()
136 && !owned_files
137 .contains(&dest_path.to_string_lossy().to_string())
138 {
139 conflicting_files.push(dest_path);
140 }
141 }
142 }
143
144 if !conflicting_files.is_empty() {
145 println!();
146 println!("{}", "File Conflict Detected:".red().bold());
147 println!(
148 "The following files that this package wants to install already \
149 exist on your system:"
150 );
151 for file in &conflicting_files {
152 println!("- {}", file.display());
153 }
154 println!();
155
156 if !utils::ask_for_confirmation(
157 "Do you want to overwrite these files and continue with the \
158 installation?",
159 yes
160 ) {
161 return Err(anyhow!(
162 "Installation aborted by user due to file conflicts."
163 ));
164 }
165 }
166
167 Ok(())
168}
169
170pub fn run(
192 package_file: &Path,
193 scope_override: Option<types::Scope>,
194 registry_handle: &str,
195 version_override: Option<&str>,
196 yes: bool,
197 sub_packages: Option<Vec<String>>,
198 link_bins: bool,
199 pb: Option<&indicatif::ProgressBar>
200) -> Result<Vec<String>> {
201 let scope = scope_override.unwrap_or(types::Scope::User);
202
203 if package_file.as_os_str().is_empty() {
205 if pb.is_none() {
206 println!("Initializing meta-package...");
207 }
208 return Ok(Vec::new());
209 }
210
211 if pb.is_none() {
212 println!(
213 "Installing from package archive: {}",
214 package_file.display()
215 );
216 }
217
218 let file_metadata = fs::metadata(package_file)
219 .map_err(|e| anyhow!("Failed to get archive metadata: {e}"))?;
220 let file_size = file_metadata.len();
221
222 if pb.is_none() {
223 println!("Archive size: {}", zoi_core::utils::format_bytes(file_size));
224 }
225
226 let mut file = File::open(package_file)
227 .map_err(|e| anyhow!("Failed to open package archive: {e}"))?;
228
229 let mut magic = [0u8; 4];
230 if file.read_exact(&mut magic).is_ok() && magic != [0x28, 0xB5, 0x2F, 0xFD]
231 {
232 return Err(anyhow!(
233 "Invalid archive format: expected zstd magic number 28 B5 2F FD, \
234 but found {magic:02X?}. This file is likely not a valid .zst \
235 archive."
236 ));
237 }
238
239 file.rewind()
240 .map_err(|e| anyhow!("Failed to rewind archive file: {e}"))?;
241
242 let decoder = ZstdDecoder::new(file)
243 .map_err(|e| anyhow!("Failed to initialize zstd decoder: {e}"))?;
244 let mut archive = Archive::new(decoder);
245
246 #[cfg(target_os = "linux")]
247 archive.set_unpack_xattrs(true);
248
249 let temp_dir = Builder::new().prefix("zoi-install-").tempdir()?;
250 let unpack_path = temp_dir.path().to_path_buf();
251
252 for entry_res in archive
253 .entries()
254 .map_err(|e| anyhow!("Failed to read archive entries: {e}"))?
255 {
256 let mut entry = entry_res.map_err(|e| {
257 anyhow!(
258 "Failed to process archive entry: {e}. The archive may be \
259 truncated or corrupted."
260 )
261 })?;
262 let path = entry
263 .path()
264 .map_err(|e| anyhow!("Failed to get entry path: {e}"))?
265 .to_path_buf();
266 entry.unpack_in(&unpack_path).map_err(|e| {
267 anyhow!("Failed to unpack file '{}': {}", path.display(), e)
268 })?;
269 }
270
271 let mut pkg_lua_path = None;
272 for entry in WalkDir::new(temp_dir.path())
273 .into_iter()
274 .filter_map(std::result::Result::ok)
275 {
276 if entry.file_name().to_string_lossy().ends_with(".pkg.lua") {
277 pkg_lua_path = Some(entry.path().to_path_buf());
278 break;
279 }
280 }
281 let pkg_lua_path = pkg_lua_path.ok_or_else(|| {
282 anyhow!(
283 "Could not find .pkg.lua file in archive '{}'",
284 package_file.display()
285 )
286 })?;
287
288 let platform = utils::get_platform()?;
289 let metadata = zoi_lua::parser::parse_lua_package_for_platform(
290 pkg_lua_path.to_str().ok_or_else(|| {
291 anyhow!(
292 "Path contains invalid UTF-8 characters: {}",
293 pkg_lua_path.display()
294 )
295 })?,
296 &platform,
297 version_override,
298 Some(scope),
299 true
300 )?;
301
302 let pooled_manifest_path = unpack_path.join("manifest.json");
303 if pooled_manifest_path.exists() {
304 let content = fs::read_to_string(&pooled_manifest_path)?;
305 if let Ok(pooled_manifest) =
306 serde_json::from_str::<types::PooledZpaManifest>(&content)
307 {
308 return extract_pooled_zpa(
309 &pooled_manifest,
310 &unpack_path,
311 scope,
312 &metadata,
313 sub_packages,
314 link_bins,
315 pb,
316 yes,
317 registry_handle
318 );
319 }
320 }
321
322 let version = metadata.version.as_ref().ok_or_else(|| {
323 anyhow!(
324 "Package '{}' is missing version field in its metadata.",
325 metadata.name
326 )
327 })?;
328
329 if pb.is_none() {
330 println!(
331 "Installing package: {} v{}",
332 metadata.name.cyan(),
333 version.yellow()
334 );
335 }
336
337 let package_dir = local::get_package_dir(
338 scope,
339 registry_handle,
340 &metadata.repo,
341 &metadata.name
342 )?;
343 fs::create_dir_all(&package_dir)?;
344
345 let staging_dir = tempfile::Builder::new()
346 .prefix(".tmp-install-")
347 .tempdir_in(&package_dir)?;
348
349 let mut installed_files: Vec<String> = Vec::new();
350 let version_dir = package_dir.join(version);
351
352 let data_dir = temp_dir.path().join("data");
353 if data_dir.exists() {
354 if let Some(p) = pb {
355 p.set_message(format!("Installing {}...", metadata.name.cyan()));
356 } else {
357 println!("Installing {}...", metadata.name.cyan());
358 }
359
360 let subs_to_install = if let Some(subs) = sub_packages {
361 subs
362 } else if let Some(subs) = &metadata.sub_packages {
363 if let Some(main_subs) = &metadata.main_subs {
364 main_subs.clone()
365 } else {
366 let mut all = vec![String::new()];
367 all.extend(subs.clone());
368 all
369 }
370 } else {
371 vec![String::new()]
372 };
373
374 for sub in subs_to_install {
375 let sub_data_dir = if sub.is_empty() {
376 data_dir.clone()
377 } else {
378 if pb.is_none() {
379 println!("Installing sub-package: {}", sub.bold());
380 }
381 data_dir.join(&sub)
382 };
383
384 if !sub_data_dir.exists() {
385 if pb.is_none() {
386 eprintln!(
387 "Warning: sub-package '{sub}' not found in archive, \
388 skipping."
389 );
390 }
391 continue;
392 }
393
394 let mut owned_files = HashSet::new();
395 let sub_opt = if sub.is_empty() {
396 None
397 } else {
398 Some(sub.as_str())
399 };
400 if let Ok(Some(manifest)) =
401 local::is_package_installed(&metadata.name, sub_opt, scope)
402 {
403 owned_files.extend(manifest.installed_files);
404 }
405
406 let pkgstore_src = sub_data_dir.join("pkgstore");
407 if pkgstore_src.exists() {
408 copy_dir_all(&pkgstore_src, staging_dir.path())?;
409 }
410
411 let usrroot_src = sub_data_dir.join("usrroot");
412 if usrroot_src.exists() {
413 if !utils::is_admin() {
414 return Err(anyhow!(
415 "Administrator privileges required to install \
416 system-wide files. Please run with sudo or as an \
417 administrator."
418 ));
419 }
420 let root_dest =
421 zoi_core::sysroot::apply_sysroot(PathBuf::from("/"));
422 check_and_handle_file_conflicts(
423 &usrroot_src,
424 &root_dest,
425 &owned_files,
426 yes
427 )?;
428 copy_dir_all(&usrroot_src, &root_dest)?;
429 for entry in WalkDir::new(&usrroot_src)
430 .into_iter()
431 .filter_map(std::result::Result::ok)
432 {
433 if entry.file_type().is_file() {
434 let rel_to_root =
435 entry.path().strip_prefix(&usrroot_src)?;
436 installed_files.push(format!(
437 "${{usrroot}}/{}",
438 rel_to_root.to_string_lossy().replace('\\', "/")
439 ));
440 }
441 }
442 }
443
444 let usrhome_src = sub_data_dir.join("usrhome");
445 if usrhome_src.exists() {
446 let home_dest = zoi_core::utils::get_user_home()
447 .ok_or_else(|| anyhow!("Could not find home directory"))?;
448 check_and_handle_file_conflicts(
449 &usrhome_src,
450 &home_dest,
451 &owned_files,
452 yes
453 )?;
454 copy_dir_all(&usrhome_src, &home_dest)?;
455 for entry in WalkDir::new(&usrhome_src)
456 .into_iter()
457 .filter_map(std::result::Result::ok)
458 {
459 if entry.file_type().is_file() {
460 let rel_to_home =
461 entry.path().strip_prefix(&usrhome_src)?;
462 installed_files.push(format!(
463 "${{usrhome}}/{}",
464 rel_to_home.to_string_lossy().replace('\\', "/")
465 ));
466 }
467 }
468 }
469 }
470 }
471
472 if let Some(p) = pb {
473 p.set_position(60);
474 }
475
476 for entry in WalkDir::new(staging_dir.path())
477 .into_iter()
478 .filter_map(std::result::Result::ok)
479 {
480 if entry.file_type().is_file() {
481 let rel_path = entry.path().strip_prefix(staging_dir.path())?;
482 installed_files.push(format!(
483 "${{pkgstore}}/{}",
484 rel_path.to_string_lossy().replace('\\', "/")
485 ));
486 }
487 }
488
489 copy_dir_all(staging_dir.path(), &version_dir)?;
490
491 finalize_installation(
492 &version_dir,
493 &metadata,
494 scope,
495 link_bins,
496 pb,
497 &mut installed_files
498 )?;
499
500 if let Some(p) = pb {
501 p.set_position(100);
502 } else {
503 println!("{} Installation complete.", "Success:".green());
504 }
505 Ok(installed_files)
506}
507
508fn finalize_installation(
511 version_dir: &Path,
512 metadata: &types::Package,
513 scope: types::Scope,
514 link_bins: bool,
515 pb: Option<&indicatif::ProgressBar>,
516 _installed_files: &mut Vec<String>
517) -> Result<()> {
518 if let Some(backup_files) = &metadata.backup {
520 for backup_file_rel in backup_files {
521 let expanded_path = utils::expand_placeholders(
522 backup_file_rel,
523 version_dir,
524 scope
525 )?;
526 let backup_src = PathBuf::from(expanded_path);
527
528 if backup_src.exists() && backup_src.is_file() {
529 let mut orig_path = backup_src.clone();
530 let ext =
531 orig_path.extension().and_then(|s| s.to_str()).map_or_else(
532 || "zoiorig".to_string(),
533 |s| format!("{s}.zoiorig")
534 );
535 orig_path.set_extension(ext);
536
537 if let Err(e) = fs::copy(&backup_src, &orig_path)
538 && pb.is_none()
539 {
540 eprintln!(
541 "Warning: failed to create .zoiorig for {}: {}",
542 backup_src.display(),
543 e
544 );
545 }
546 }
547 }
548 }
549
550 if link_bins && let Some(bins) = &metadata.bins {
551 let bin_root = get_bin_root(scope)?;
552 fs::create_dir_all(&bin_root)?;
553
554 let mut created_shims = Vec::new();
555 let mut link_error: Option<String> = None;
556
557 for bin_name in bins {
558 let mut found_bin = false;
559 for entry in WalkDir::new(version_dir)
560 .into_iter()
561 .filter_map(std::result::Result::ok)
562 {
563 if entry.file_type().is_file()
564 && entry.file_name().to_string_lossy() == *bin_name
565 {
566 let link_path = bin_root.join(bin_name);
567
568 let zoi_exe = std::env::current_exe()?;
569 if let Err(e) =
570 zoi_core::utils::symlink_file(&zoi_exe, &link_path)
571 {
572 link_error = Some(e.to_string());
573 break;
574 }
575 created_shims.push(link_path);
576
577 if pb.is_none() {
578 println!("Created shim for: {}", bin_name.green());
579 }
580 found_bin = true;
581 break;
582 }
583 }
584 if link_error.is_some() {
585 break;
586 }
587 if !found_bin && pb.is_none() {
588 eprintln!(
589 "Warning: could not find binary '{}' to link.",
590 bin_name.yellow()
591 );
592 }
593 }
594
595 if let Some(e) = link_error {
596 for shim in created_shims {
597 let _ = fs::remove_file(shim);
598 }
599 return Err(anyhow!("Failed to create shims: {e}"));
600 }
601 }
602
603 let shell_dir = version_dir.join("shell");
604 if shell_dir.exists() {
605 for shell_entry in fs::read_dir(&shell_dir).map_err(|e| {
606 anyhow!("Failed to read shell completions directory: {e}")
607 })? {
608 let shell_entry = shell_entry?;
609 if !shell_entry.file_type()?.is_dir() {
610 continue;
611 }
612 let shell_name =
613 shell_entry.file_name().to_string_lossy().to_string();
614 let completions_root = get_completions_root(scope, &shell_name)?;
615 let pkg_completions_dir = completions_root.join(&metadata.name);
616 fs::create_dir_all(&pkg_completions_dir)?;
617
618 for file_entry in fs::read_dir(shell_entry.path()).map_err(|e| {
619 anyhow!("Failed to read shell/{shell_name}/ directory: {e}")
620 })? {
621 let file_entry = file_entry?;
622 if !file_entry.file_type()?.is_file() {
623 continue;
624 }
625 let filename =
626 file_entry.file_name().to_string_lossy().to_string();
627 let store_path = file_entry.path();
628 let link_path = pkg_completions_dir.join(&filename);
629 create_completion_symlink(&store_path, &link_path)?;
630 if pb.is_none() {
631 println!(
632 "Linked {} completion: {}",
633 shell_name.green(),
634 filename.cyan()
635 );
636 }
637 }
638 }
639 }
640
641 #[cfg(target_os = "macos")]
642 {
643 let applications_dir = match scope {
644 types::Scope::System => PathBuf::from("/Applications"),
645 types::Scope::User => {
646 let home_dir = zoi_core::utils::get_user_home()
647 .ok_or_else(|| anyhow!("Could not find home directory."))?;
648 home_dir.join("Applications")
649 }
650 types::Scope::Project => {
651 std::env::current_dir()?.join("Applications")
652 }
653 };
654
655 let mut app_bundles = Vec::new();
656 for entry in WalkDir::new(version_dir)
657 .max_depth(2)
658 .into_iter()
659 .filter_map(|e| e.ok())
660 {
661 if entry.file_type().is_dir()
662 && entry.file_name().to_string_lossy().ends_with(".app")
663 {
664 app_bundles.push(entry.path().to_path_buf());
665 }
666 }
667
668 if !app_bundles.is_empty() {
669 fs::create_dir_all(&applications_dir)?;
670 for app_path in app_bundles {
671 if zoi_core::utils::command_exists("xattr") {
672 let _ = std::process::Command::new("xattr")
673 .arg("-r")
674 .arg("-d")
675 .arg("com.apple.quarantine")
676 .arg(&app_path)
677 .status();
678 }
679
680 let app_name = app_path.file_name().ok_or_else(|| {
681 anyhow!("App path has no filename: {:?}", app_path)
682 })?;
683 let symlink_path = applications_dir.join(app_name);
684
685 if symlink_path.exists() {
686 let _ = fs::remove_file(&symlink_path);
687 let _ = fs::remove_dir_all(&symlink_path);
688 }
689
690 if std::os::unix::fs::symlink(&app_path, &symlink_path).is_ok()
691 {
692 _installed_files.push(format!(
693 "${{applications}}/{}",
694 app_name.to_string_lossy().replace('\\', "/")
695 ));
696 if pb.is_none() {
697 println!(
698 "Linked {} to {}",
699 app_name.to_string_lossy().green(),
700 applications_dir.display()
701 );
702 }
703 }
704 }
705 }
706 }
707
708 Ok(())
709}
710
711fn extract_pooled_zpa(
714 pooled_manifest: &types::PooledZpaManifest,
715 unpack_path: &Path,
716 scope: types::Scope,
717 metadata: &types::Package,
718 sub_packages: Option<Vec<String>>,
719 link_bins: bool,
720 pb: Option<&indicatif::ProgressBar>,
721 yes: bool,
722 registry_handle: &str
723) -> Result<Vec<String>> {
724 let version = metadata.version.as_ref().ok_or_else(|| {
725 anyhow!(
726 "Package '{}' is missing version field in its metadata.",
727 metadata.name
728 )
729 })?;
730
731 if pb.is_none() {
732 println!(
733 "Installing pooled package: {} v{} [{:?}]",
734 metadata.name.cyan(),
735 version.yellow(),
736 scope
737 );
738 }
739
740 let package_dir = local::get_package_dir(
741 scope,
742 registry_handle,
743 &metadata.repo,
744 &metadata.name
745 )?;
746 fs::create_dir_all(&package_dir)?;
747
748 let staging_dir = tempfile::Builder::new()
749 .prefix(".tmp-install-")
750 .tempdir_in(&package_dir)?;
751
752 let mut installed_files: Vec<String> = Vec::new();
753 let version_dir = package_dir.join(version);
754
755 let subs_to_install = if let Some(subs) = sub_packages {
756 subs
757 } else if let Some(subs) = &metadata.sub_packages {
758 if let Some(main_subs) = &metadata.main_subs {
759 main_subs.clone()
760 } else {
761 subs.clone()
762 }
763 } else {
764 vec![String::new()]
765 };
766
767 let pool_dir = unpack_path.join("pool");
768
769 let mut conflicts = Vec::new();
770 let mut owned_files = HashSet::new();
771
772 for sub in &subs_to_install {
773 let sub_opt = if sub.is_empty() {
774 None
775 } else {
776 Some(sub.as_str())
777 };
778 if let Ok(Some(manifest)) =
779 local::is_package_installed(&metadata.name, sub_opt, scope)
780 {
781 owned_files.extend(manifest.installed_files);
782 }
783
784 if let Some(sub_mapping) = pooled_manifest.mappings.get(sub)
785 && let Some(scope_mapping) = sub_mapping.scopes.get(&scope)
786 {
787 for mapped_file in &scope_mapping.files {
788 let dest_path = expand_pooled_path(
789 &mapped_file.dest,
790 staging_dir.path(),
791 scope
792 )?;
793 if !mapped_file.dest.starts_with("${pkgstore}")
796 && dest_path.exists()
797 && !owned_files.contains(&mapped_file.dest)
798 {
799 conflicts.push(dest_path);
800 }
801 }
802 }
803 }
804
805 if !conflicts.is_empty() {
806 println!();
807 println!("{}", "File Conflict Detected:".red().bold());
808 println!(
809 "The following files that this package wants to install already \
810 exist on your system:"
811 );
812 for file in &conflicts {
813 println!("- {}", file.display());
814 }
815 println!();
816
817 if !utils::ask_for_confirmation(
818 "Do you want to overwrite these files and continue with the \
819 installation?",
820 yes
821 ) {
822 return Err(anyhow!(
823 "Installation aborted by user due to file conflicts."
824 ));
825 }
826 }
827
828 for sub in subs_to_install {
829 let Some(sub_mapping) = pooled_manifest.mappings.get(&sub) else {
830 if pb.is_none() {
831 eprintln!(
832 "Warning: mapping for sub-package '{sub}' not found in \
833 archive, skipping."
834 );
835 }
836 continue;
837 };
838
839 let Some(scope_mapping) = sub_mapping.scopes.get(&scope) else {
840 if pb.is_none() {
841 eprintln!(
842 "Warning: mapping for scope {scope:?} not found for \
843 sub-package '{sub}', skipping."
844 );
845 }
846 continue;
847 };
848
849 for mapped_dir in &scope_mapping.dirs {
851 let dest_path = expand_pooled_path(
852 &mapped_dir.path,
853 staging_dir.path(),
854 scope
855 )?;
856 fs::create_dir_all(&dest_path)?;
857
858 #[cfg(unix)]
859 {
860 if let Some(mode) = mapped_dir.mode {
861 use std::os::unix::fs::PermissionsExt;
862 fs::set_permissions(
863 &dest_path,
864 fs::Permissions::from_mode(mode)
865 )?;
866 }
867 if let (Some(owner), Some(group)) =
868 (&mapped_dir.owner, &mapped_dir.group)
869 {
870 let _ = utils::set_path_owner(&dest_path, owner, group);
871 }
872 }
873 }
874
875 for mapped_file in &scope_mapping.files {
877 let pool_file = pool_dir.join(&mapped_file.hash);
878 if !pool_file.exists() {
879 return Err(anyhow!("Pool file missing: {}", mapped_file.hash));
880 }
881
882 let dest_path = expand_pooled_path(
883 &mapped_file.dest,
884 staging_dir.path(),
885 scope
886 )?;
887
888 if let Some(parent) = dest_path.parent() {
889 fs::create_dir_all(parent)?;
890 }
891
892 fs::copy(&pool_file, &dest_path)?;
893
894 #[cfg(unix)]
895 {
896 use std::os::unix::fs::PermissionsExt;
897 fs::set_permissions(
898 &dest_path,
899 fs::Permissions::from_mode(mapped_file.mode)
900 )?;
901 if let (Some(owner), Some(group)) =
902 (&mapped_file.owner, &mapped_file.group)
903 {
904 let _ = utils::set_path_owner(&dest_path, owner, group);
905 }
906 }
907
908 installed_files.push(mapped_file.dest.clone());
909 }
910
911 for mapped_link in &scope_mapping.symlinks {
913 let dest_path = expand_pooled_path(
914 &mapped_link.link,
915 staging_dir.path(),
916 scope
917 )?;
918
919 if let Some(parent) = dest_path.parent() {
920 fs::create_dir_all(parent)?;
921 }
922
923 if dest_path.exists() || dest_path.is_symlink() {
924 fs::remove_file(&dest_path).ok();
925 }
926
927 let target = mapped_link.target.clone();
929
930 utils::symlink_file(Path::new(&target), &dest_path)?;
931 installed_files.push(mapped_link.link.clone());
932 }
933 }
934
935 fs::create_dir_all(&version_dir)?;
937 copy_dir_all(staging_dir.path(), &version_dir)?;
938
939 finalize_installation(
940 &version_dir,
941 metadata,
942 scope,
943 link_bins,
944 pb,
945 &mut installed_files
946 )?;
947
948 if let Some(p) = pb {
949 p.set_position(100);
950 }
951
952 Ok(installed_files)
953}
954
955fn expand_pooled_path(
957 path: &str,
958 staging_path: &Path,
959 _scope: types::Scope
960) -> Result<PathBuf> {
961 if let Some(rel) = path.strip_prefix("${pkgstore}/") {
962 Ok(staging_path.join(rel))
963 } else if let Some(rel) = path.strip_prefix("${usrroot}/") {
964 Ok(zoi_core::sysroot::apply_sysroot(
965 PathBuf::from("/").join(rel)
966 ))
967 } else if let Some(rel) = path.strip_prefix("${usrhome}/") {
968 let home_dir = zoi_core::utils::get_user_home()
969 .ok_or_else(|| anyhow!("Home dir not found"))?;
970 Ok(home_dir.join(rel))
971 } else if let Some(rel) = path.strip_prefix("${createpkgdir}/") {
972 Ok(std::env::current_dir()?.join(rel))
973 } else {
974 Err(anyhow!("Invalid pooled path placeholder: {path}"))
975 }
976}