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