1use std::{
2 env, fs,
3 io::Write,
4 path::{Path, PathBuf},
5 process::Command,
6 thread::sleep,
7 time::Duration,
8};
9
10use chrono::Utc;
11use serde_json::json;
12use soar_config::{
13 config::Config,
14 packages::{BinaryMapping, BuildConfig, PackageHooks, SandboxConfig},
15};
16use soar_db::repository::core::{
17 CoreRepository, InstalledPackageWithPortable, NewInstalledPackage,
18};
19use soar_dl::{
20 download::Download,
21 error::DownloadError,
22 filter::Filter,
23 oci::OciDownload,
24 types::{OverwriteMode, Progress},
25};
26use soar_utils::{
27 error::FileSystemResult,
28 fs::{safe_remove, walk_dir},
29 hash::calculate_checksum,
30};
31use tracing::{debug, trace, warn};
32
33use crate::{
34 constants::INSTALL_MARKER_FILE,
35 database::{connection::DieselDatabase, models::Package},
36 error::{ErrorContext, SoarError},
37 utils::get_extract_dir,
38 SoarResult,
39};
40
41fn validate_relative_path(relative_path: &str, path_type: &str) -> SoarResult<()> {
44 if Path::new(relative_path).is_absolute() {
45 return Err(SoarError::Custom(format!(
46 "{} '{}' must be a relative path, not absolute",
47 path_type, relative_path
48 )));
49 }
50
51 if relative_path.contains("..") {
52 return Err(SoarError::Custom(format!(
53 "{} '{}' contains path traversal components",
54 path_type, relative_path
55 )));
56 }
57
58 Ok(())
59}
60
61fn validate_path_containment(
64 base_dir: &Path,
65 relative_path: &str,
66 path_type: &str,
67) -> SoarResult<PathBuf> {
68 let joined_path = base_dir.join(relative_path);
69
70 let canonical_base = base_dir
71 .canonicalize()
72 .with_context(|| format!("canonicalizing base directory {}", base_dir.display()))?;
73
74 let canonical_path = joined_path.canonicalize().with_context(|| {
75 format!(
76 "canonicalizing {} path {}",
77 path_type,
78 joined_path.display()
79 )
80 })?;
81
82 if !canonical_path.starts_with(&canonical_base) {
83 return Err(SoarError::Custom(format!(
84 "{} '{}' escapes install directory (path traversal)",
85 path_type, relative_path
86 )));
87 }
88
89 Ok(canonical_path)
90}
91
92use crate::utils::substitute_placeholders;
93
94#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
96pub struct InstallMarker {
97 pub pkg_id: String,
98 pub version: String,
99 pub bsum: Option<String>,
100}
101
102impl InstallMarker {
103 pub fn read_from_dir(install_dir: &Path) -> Option<Self> {
104 let marker_path = install_dir.join(INSTALL_MARKER_FILE);
105 let content = fs::read_to_string(&marker_path).ok()?;
106 serde_json::from_str(&content).ok()
107 }
108
109 pub fn matches_package(&self, package: &Package) -> bool {
110 self.pkg_id == package.pkg_id
111 && self.version == package.version
112 && self.bsum == package.bsum
113 }
114}
115
116pub struct PackageInstaller {
117 package: Package,
118 install_dir: PathBuf,
119 progress_callback: Option<std::sync::Arc<dyn Fn(Progress) + Send + Sync>>,
120 db: DieselDatabase,
121 config: Config,
122 globs: Vec<String>,
123 nested_extract: Option<String>,
124 extract_root: Option<String>,
125 hooks: Option<PackageHooks>,
126 build: Option<BuildConfig>,
127 sandbox: Option<SandboxConfig>,
128 arch_map: Option<std::collections::HashMap<String, String>>,
129}
130
131#[derive(Clone, Default, Debug)]
132pub struct InstallTarget {
133 pub package: Package,
134 pub existing_install: Option<crate::database::models::InstalledPackage>,
135 pub pinned: bool,
136 pub profile: Option<String>,
137 pub portable: Option<String>,
138 pub portable_home: Option<String>,
139 pub portable_config: Option<String>,
140 pub portable_share: Option<String>,
141 pub portable_cache: Option<String>,
142 pub entrypoint: Option<String>,
143 pub binaries: Option<Vec<BinaryMapping>>,
144 pub nested_extract: Option<String>,
145 pub extract_root: Option<String>,
146 pub hooks: Option<PackageHooks>,
147 pub build: Option<BuildConfig>,
148 pub sandbox: Option<SandboxConfig>,
149 pub arch_map: Option<std::collections::HashMap<String, String>>,
150}
151
152impl PackageInstaller {
153 pub async fn new<P: AsRef<Path>>(
154 target: &InstallTarget,
155 install_dir: P,
156 progress_callback: Option<std::sync::Arc<dyn Fn(Progress) + Send + Sync>>,
157 db: DieselDatabase,
158 globs: Vec<String>,
159 config: Config,
160 ) -> SoarResult<Self> {
161 let install_dir = install_dir.as_ref().to_path_buf();
162 let package = &target.package;
163 trace!(
164 pkg_name = package.pkg_name,
165 pkg_id = package.pkg_id,
166 install_dir = %install_dir.display(),
167 "creating package installer"
168 );
169 let profile = config.default_profile.clone();
170
171 if let Some(ref extract_root) = target.extract_root {
173 validate_relative_path(extract_root, "extract_root")?;
174 }
175 if let Some(ref nested_extract) = target.nested_extract {
176 validate_relative_path(nested_extract, "nested_extract")?;
177 }
178
179 let has_pending = db.with_conn(|conn| {
181 CoreRepository::has_pending_install(
182 conn,
183 &package.pkg_id,
184 &package.pkg_name,
185 &package.repo_name,
186 &package.version,
187 )
188 })?;
189
190 trace!(
191 pkg_id = package.pkg_id,
192 pkg_name = package.pkg_name,
193 repo_name = package.repo_name,
194 version = package.version,
195 has_pending = has_pending,
196 "checking for pending install"
197 );
198
199 let needs_new_record = if has_pending {
200 trace!("resuming existing pending install");
201 false
202 } else {
203 match &target.existing_install {
204 None => true,
205 Some(existing) => existing.version != package.version || existing.is_installed,
206 }
207 };
208
209 if needs_new_record {
210 trace!(
211 "inserting new package record for version {}",
212 package.version
213 );
214 let repo_name = &package.repo_name;
215 let pkg_id = &package.pkg_id;
216 let pkg_name = &package.pkg_name;
217 let pkg_type = package.pkg_type.as_deref();
218 let version = &package.version;
219 let size = package.ghcr_size.unwrap_or(package.size.unwrap_or(0)) as i64;
220 let installed_path = install_dir.to_string_lossy();
221 let installed_date = Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
222
223 let orphaned_paths = db.with_conn(|conn| {
225 CoreRepository::delete_pending_installs(conn, pkg_id, pkg_name, repo_name)
226 })?;
227 for path in orphaned_paths {
228 let path = std::path::Path::new(&path);
229 if path.exists() {
230 fs::remove_dir_all(path).ok();
231 }
232 }
233
234 let new_package = NewInstalledPackage {
235 repo_name,
236 pkg_id,
237 pkg_name,
238 pkg_type,
239 version,
240 size,
241 checksum: None,
242 installed_path: &installed_path,
243 installed_date: &installed_date,
244 profile: &profile,
245 pinned: target.pinned,
246 is_installed: false,
247 detached: false,
248 unlinked: false,
249 provides: None,
250 install_patterns: Some(json!(globs)),
251 };
252
253 db.with_conn(|conn| CoreRepository::insert(conn, &new_package))?;
254 }
255
256 Ok(Self {
257 package: package.clone(),
258 install_dir,
259 progress_callback,
260 db,
261 config,
262 globs,
263 nested_extract: target.nested_extract.clone(),
264 extract_root: target.extract_root.clone(),
265 hooks: target.hooks.clone(),
266 build: target.build.clone(),
267 sandbox: target.sandbox.clone(),
268 arch_map: target.arch_map.clone(),
269 })
270 }
271
272 fn run_hook(&self, hook_name: &str, command: &str) -> SoarResult<()> {
274 use super::hooks::{run_hook, HookEnv};
275
276 let env = HookEnv {
277 install_dir: &self.install_dir,
278 pkg_name: &self.package.pkg_name,
279 pkg_id: &self.package.pkg_id,
280 pkg_version: &self.package.version,
281 };
282
283 run_hook(hook_name, command, &env, self.sandbox.as_ref())
284 }
285
286 pub fn run_post_download_hook(&self) -> SoarResult<()> {
288 if let Some(ref hooks) = self.hooks {
289 if let Some(ref cmd) = hooks.post_download {
290 self.run_hook("post_download", cmd)?;
291 }
292 }
293 Ok(())
294 }
295
296 pub fn run_post_extract_hook(&self) -> SoarResult<()> {
298 if let Some(ref hooks) = self.hooks {
299 if let Some(ref cmd) = hooks.post_extract {
300 self.run_hook("post_extract", cmd)?;
301 }
302 }
303 Ok(())
304 }
305
306 pub fn run_post_install_hook(&self) -> SoarResult<()> {
308 if let Some(ref hooks) = self.hooks {
309 if let Some(ref cmd) = hooks.post_install {
310 self.run_hook("post_install", cmd)?;
311 }
312 }
313 Ok(())
314 }
315
316 fn check_build_dependencies(&self, deps: &[String]) -> SoarResult<()> {
318 for dep in deps {
319 let result = Command::new("which").arg(dep).output();
320
321 match result {
322 Ok(output) if !output.status.success() => {
323 warn!("Build dependency '{}' not found in PATH", dep);
324 }
325 Err(_) => {
326 warn!("Could not check for build dependency '{}'", dep);
327 }
328 _ => {
329 trace!("Build dependency '{}' found", dep);
330 }
331 }
332 }
333 Ok(())
334 }
335
336 pub fn run_build(&self) -> SoarResult<()> {
338 use crate::sandbox;
339
340 let build_config = match &self.build {
341 Some(config) if !config.commands.is_empty() => config,
342 _ => return Ok(()),
343 };
344
345 debug!(
346 "building package {} with {} commands",
347 self.package.pkg_name,
348 build_config.commands.len()
349 );
350
351 if !build_config.dependencies.is_empty() {
352 self.check_build_dependencies(&build_config.dependencies)?;
353 }
354
355 let bin_dir = self.config.get_bin_path()?;
356 let nproc = std::thread::available_parallelism()
357 .map(|p| p.get().to_string())
358 .unwrap_or_else(|_| "1".to_string());
359
360 let use_sandbox = sandbox::is_landlock_supported();
361
362 if use_sandbox {
363 debug!("running build with Landlock sandbox");
364 } else {
365 if self.sandbox.as_ref().is_some_and(|s| s.require) {
366 return Err(SoarError::Custom(
367 "Build requires sandbox but Landlock is not available on this system. \
368 Either upgrade to Linux 5.13+ or set sandbox.require = false."
369 .into(),
370 ));
371 }
372 warn!(
373 "Landlock not supported, running build without sandbox ({} commands)",
374 build_config.commands.len()
375 );
376 }
377
378 for (i, cmd) in build_config.commands.iter().enumerate() {
379 debug!(
380 "running build command {}/{}: {}",
381 i + 1,
382 build_config.commands.len(),
383 cmd
384 );
385
386 let status = if use_sandbox {
387 let env_vars: Vec<(&str, String)> = vec![
388 (
389 "INSTALL_DIR",
390 self.install_dir.to_string_lossy().to_string(),
391 ),
392 ("BIN_DIR", bin_dir.to_string_lossy().to_string()),
393 ("PKG_NAME", self.package.pkg_name.clone()),
394 ("PKG_ID", self.package.pkg_id.clone()),
395 ("PKG_VERSION", self.package.version.clone()),
396 ("NPROC", nproc.clone()),
397 ];
398
399 let mut sandbox_cmd = sandbox::SandboxedCommand::new(cmd)
400 .working_dir(&self.install_dir)
401 .read_path(&bin_dir)
402 .envs(env_vars);
403
404 if let Some(s) = &self.sandbox {
405 let config = sandbox::SandboxConfig::new().with_network(if s.network {
406 sandbox::NetworkConfig::allow_all()
407 } else {
408 sandbox::NetworkConfig::default()
409 });
410 sandbox_cmd = sandbox_cmd.config(config);
411 for path in &s.fs_read {
412 sandbox_cmd = sandbox_cmd.read_path(path);
413 }
414 for path in &s.fs_write {
415 sandbox_cmd = sandbox_cmd.write_path(path);
416 }
417 }
418 sandbox_cmd.run()?
419 } else {
420 Command::new("sh")
421 .arg("-c")
422 .arg(cmd)
423 .env("INSTALL_DIR", &self.install_dir)
424 .env("BIN_DIR", &bin_dir)
425 .env("PKG_NAME", &self.package.pkg_name)
426 .env("PKG_ID", &self.package.pkg_id)
427 .env("PKG_VERSION", &self.package.version)
428 .env("NPROC", &nproc)
429 .current_dir(&self.install_dir)
430 .status()
431 .with_context(|| format!("executing build command {}", i + 1))?
432 };
433
434 if !status.success() {
435 return Err(SoarError::Custom(format!(
436 "Build command {} failed with exit code: {}",
437 i + 1,
438 status.code().unwrap_or(-1)
439 )));
440 }
441 }
442
443 debug!("build completed successfully");
444 Ok(())
445 }
446
447 fn write_marker(&self) -> SoarResult<()> {
448 fs::create_dir_all(&self.install_dir).with_context(|| {
449 format!("creating install directory {}", self.install_dir.display())
450 })?;
451
452 let marker = InstallMarker {
453 pkg_id: self.package.pkg_id.clone(),
454 version: self.package.version.clone(),
455 bsum: self.package.bsum.clone(),
456 };
457
458 let marker_path = self.install_dir.join(INSTALL_MARKER_FILE);
459 let mut file = fs::File::create(&marker_path)
460 .with_context(|| format!("creating marker file {}", marker_path.display()))?;
461 let content = serde_json::to_string(&marker)
462 .map_err(|e| SoarError::Custom(format!("Failed to serialize marker: {e}")))?;
463 file.write_all(content.as_bytes())
464 .with_context(|| format!("writing marker file {}", marker_path.display()))?;
465
466 Ok(())
467 }
468
469 fn remove_marker(&self) -> SoarResult<()> {
470 let marker_path = self.install_dir.join(INSTALL_MARKER_FILE);
471 if marker_path.exists() {
472 fs::remove_file(&marker_path)
473 .with_context(|| format!("removing marker file {}", marker_path.display()))?;
474 }
475 Ok(())
476 }
477
478 pub async fn download_package(&self) -> SoarResult<Option<String>> {
479 debug!(
480 pkg_name = self.package.pkg_name,
481 pkg_id = self.package.pkg_id,
482 "starting package download"
483 );
484 self.write_marker()?;
485
486 let package = &self.package;
487 let output_path = self.install_dir.join(&package.pkg_name);
488
489 let (url, output_path) = if let Some(ref ghcr_pkg) = self.package.ghcr_pkg {
491 debug!("source: {} (OCI)", ghcr_pkg);
492 (ghcr_pkg, &self.install_dir)
493 } else {
494 debug!("source: {}", self.package.download_url);
495 (&self.package.download_url, &output_path.to_path_buf())
496 };
497
498 if self.package.ghcr_pkg.is_some() {
499 trace!(url = url.as_str(), "using OCI/GHCR download");
500 let mut dl = OciDownload::new(url.as_str())
501 .output(output_path.to_string_lossy())
502 .parallel(self.config.ghcr_concurrency.unwrap_or(8))
503 .overwrite(OverwriteMode::Skip);
504
505 if let Some(ref cb) = self.progress_callback {
506 let cb = cb.clone();
507 dl = dl.progress(move |p| {
508 cb(p);
509 });
510 }
511
512 if !self.globs.is_empty() {
513 dl = dl.filter(Filter {
514 globs: self.globs.clone(),
515 ..Default::default()
516 });
517 }
518
519 let mut retries = 0;
520 let mut last_error: Option<DownloadError> = None;
521 loop {
522 if retries > 5 {
523 if let Some(ref callback) = self.progress_callback {
524 callback(Progress::Aborted);
525 }
526 return Err(last_error.unwrap_or_else(|| {
528 DownloadError::Multiple {
529 errors: vec!["Download failed after 5 retries".into()],
530 }
531 }))?;
532 }
533 match dl.clone().execute() {
534 Ok(_) => {
535 debug!("OCI download completed successfully");
536 break;
537 }
538 Err(err) => {
539 if matches!(
540 err,
541 DownloadError::HttpError {
542 status: 429,
543 ..
544 } | DownloadError::Network(_)
545 ) {
546 warn!(retry = retries, "download failed, retrying after delay");
547 sleep(Duration::from_secs(5));
548 retries += 1;
549 if retries > 1 {
550 if let Some(ref callback) = self.progress_callback {
551 callback(Progress::Error);
552 }
553 }
554 last_error = Some(err);
555 } else {
556 return Err(err)?;
557 }
558 }
559 }
560 }
561
562 self.run_post_download_hook()?;
565 self.run_post_extract_hook()?;
566 self.run_build()?;
567
568 Ok(None)
569 } else {
570 trace!(url = url.as_str(), "using direct download");
571 let extract_dir = get_extract_dir(&self.install_dir);
572
573 let should_extract = self
574 .package
575 .pkg_type
576 .as_deref()
577 .is_some_and(|t| t == "archive");
578
579 let mut dl = Download::new(url.as_str())
580 .output(output_path.to_string_lossy())
581 .overwrite(OverwriteMode::Skip)
582 .extract(should_extract)
583 .extract_to(&extract_dir);
584
585 if let Some(ref cb) = self.progress_callback {
586 let cb = cb.clone();
587 dl = dl.progress(move |p| {
588 cb(p);
589 });
590 }
591
592 let file_path = dl.execute()?;
593
594 self.run_post_download_hook()?;
595
596 let checksum = if PathBuf::from(&file_path).exists() {
597 Some(calculate_checksum(&file_path)?)
598 } else {
599 None
600 };
601
602 let extract_path = PathBuf::from(&extract_dir);
603 if extract_path.exists() {
604 fs::remove_file(file_path).ok();
605
606 for entry in fs::read_dir(&extract_path)
607 .with_context(|| format!("reading {} directory", extract_path.display()))?
608 {
609 let entry = entry.with_context(|| {
610 format!("reading entry from directory {}", extract_path.display())
611 })?;
612 let from = entry.path();
613 let to = self.install_dir.join(entry.file_name());
614 fs::rename(&from, &to).with_context(|| {
615 format!("renaming {} to {}", from.display(), to.display())
616 })?;
617 }
618
619 fs::remove_dir_all(&extract_path).ok();
620 }
621
622 if let Some(ref root_dir) = self.extract_root {
624 let root_dir = substitute_placeholders(
625 root_dir,
626 Some(&self.package.version),
627 self.arch_map.as_ref(),
628 );
629 let root_path =
630 validate_path_containment(&self.install_dir, &root_dir, "extract_root")?;
631
632 if root_path.is_dir() {
633 debug!(
634 "applying extract_root: moving contents from {} to {}",
635 root_path.display(),
636 self.install_dir.display()
637 );
638 for entry in fs::read_dir(&root_path).with_context(|| {
640 format!("reading extract_root directory {}", root_path.display())
641 })? {
642 let entry = entry.with_context(|| {
643 format!("reading entry from directory {}", root_path.display())
644 })?;
645 let from = entry.path();
646 let to = self.install_dir.join(entry.file_name());
647 if to.exists() {
648 if to.is_dir() {
649 fs::remove_dir_all(&to).ok();
650 } else {
651 fs::remove_file(&to).ok();
652 }
653 }
654 fs::rename(&from, &to).with_context(|| {
655 format!("moving {} to {}", from.display(), to.display())
656 })?;
657 }
658 fs::remove_dir_all(&root_path).ok();
659 } else {
660 warn!("extract_root '{}' not found in package", root_dir);
661 }
662 }
663
664 if let Some(ref nested_archive) = self.nested_extract {
666 let nested_archive = substitute_placeholders(
667 nested_archive,
668 Some(&self.package.version),
669 self.arch_map.as_ref(),
670 );
671 let archive_path = validate_path_containment(
672 &self.install_dir,
673 &nested_archive,
674 "nested_extract",
675 )?;
676
677 if archive_path.is_file() {
678 debug!("extracting nested archive: {}", archive_path.display());
679 let nested_extract_dir = get_extract_dir(&self.install_dir);
680
681 compak::extract_archive(&archive_path, &nested_extract_dir).map_err(|e| {
682 SoarError::Custom(format!(
683 "Failed to extract nested archive {}: {}",
684 archive_path.display(),
685 e
686 ))
687 })?;
688
689 fs::remove_file(&archive_path).ok();
690
691 let nested_extract_path = PathBuf::from(&nested_extract_dir);
693 if nested_extract_path.exists() {
694 for entry in fs::read_dir(&nested_extract_path).with_context(|| {
695 format!(
696 "reading nested extract directory {}",
697 nested_extract_path.display()
698 )
699 })? {
700 let entry = entry.with_context(|| {
701 format!(
702 "reading entry from directory {}",
703 nested_extract_path.display()
704 )
705 })?;
706 let from = entry.path();
707 let to = self.install_dir.join(entry.file_name());
708 if to.exists() {
709 if to.is_dir() {
710 fs::remove_dir_all(&to).ok();
711 } else {
712 fs::remove_file(&to).ok();
713 }
714 }
715 fs::rename(&from, &to).with_context(|| {
716 format!("moving {} to {}", from.display(), to.display())
717 })?;
718 }
719 fs::remove_dir_all(&nested_extract_path).ok();
720 }
721 } else {
722 warn!(
723 "nested_extract archive '{}' not found in package",
724 nested_archive
725 );
726 }
727 }
728
729 self.run_post_extract_hook()?;
730 self.run_build()?;
731
732 Ok(checksum)
733 }
734 }
735
736 pub async fn record(
737 &self,
738 unlinked: bool,
739 portable: Option<&str>,
740 portable_home: Option<&str>,
741 portable_config: Option<&str>,
742 portable_share: Option<&str>,
743 portable_cache: Option<&str>,
744 ) -> SoarResult<()> {
745 debug!(
746 pkg_name = self.package.pkg_name,
747 pkg_id = self.package.pkg_id,
748 unlinked = unlinked,
749 "recording installation"
750 );
751 let package = &self.package;
752 let repo_name = &package.repo_name;
753 let pkg_name = &package.pkg_name;
754 let pkg_id = &package.pkg_id;
755 let version = &package.version;
756 let size = package.ghcr_size.unwrap_or(package.size.unwrap_or(0)) as i64;
757 let checksum = package.bsum.as_deref();
758 let provides = package.provides.clone();
759
760 let installed_date = Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
761
762 let installed_path = self.install_dir.to_string_lossy();
763 let record_id: Option<i32> = self.db.with_conn(|conn| {
764 CoreRepository::record_installation(
765 conn,
766 repo_name,
767 pkg_name,
768 pkg_id,
769 version,
770 size,
771 provides,
772 checksum,
773 &installed_date,
774 &installed_path,
775 )
776 })?;
777
778 let record_id = record_id.ok_or_else(|| {
779 SoarError::Custom(format!(
780 "Failed to record installation for {}#{}: package not found in database",
781 pkg_name, pkg_id
782 ))
783 })?;
784
785 if portable.is_some()
786 || portable_home.is_some()
787 || portable_config.is_some()
788 || portable_share.is_some()
789 || portable_cache.is_some()
790 {
791 let base_dir = env::current_dir()
792 .map_err(|_| SoarError::Custom("Error retrieving current directory".into()))?;
793
794 let resolve_path = |opt: Option<&str>| -> Option<String> {
795 opt.map(|p| {
796 if p.is_empty() {
797 String::new()
798 } else {
799 let path = PathBuf::from(p);
800 let absolute = if path.is_absolute() {
801 path
802 } else {
803 base_dir.join(path)
804 };
805 absolute.to_string_lossy().into_owned()
806 }
807 })
808 };
809
810 let portable_path = resolve_path(portable);
811 let portable_home = resolve_path(portable_home);
812 let portable_config = resolve_path(portable_config);
813 let portable_share = resolve_path(portable_share);
814 let portable_cache = resolve_path(portable_cache);
815
816 self.db.with_conn(|conn| {
817 CoreRepository::upsert_portable(
818 conn,
819 record_id,
820 portable_path.as_deref(),
821 portable_home.as_deref(),
822 portable_config.as_deref(),
823 portable_share.as_deref(),
824 portable_cache.as_deref(),
825 )
826 })?;
827 }
828
829 if !unlinked {
830 self.db
831 .with_conn(|conn| CoreRepository::unlink_others(conn, pkg_name, pkg_id, version))?;
832
833 let alternate_packages: Vec<InstalledPackageWithPortable> =
834 self.db.with_conn(|conn| {
835 CoreRepository::find_alternates(conn, pkg_name, pkg_id, version)
836 })?;
837
838 for alt_pkg in alternate_packages {
839 let installed_path = PathBuf::from(&alt_pkg.installed_path);
840
841 let mut remove_action = |path: &Path| -> FileSystemResult<()> {
842 if let Ok(real_path) = fs::read_link(path) {
843 if real_path.parent() == Some(&installed_path) {
844 safe_remove(path)?;
845 }
846 }
847 Ok(())
848 };
849 walk_dir(&self.config.get_desktop_path()?, &mut remove_action)?;
850
851 let mut remove_action = |path: &Path| -> FileSystemResult<()> {
852 if let Ok(real_path) = fs::read_link(path) {
853 if real_path.parent() == Some(&installed_path) {
854 safe_remove(path)?;
855 }
856 }
857 Ok(())
858 };
859 walk_dir(self.config.get_icons_path(), &mut remove_action)?;
860
861 if let Some(ref provides) = alt_pkg.provides {
862 let bin_path = self.config.get_bin_path()?;
863 for provide in provides {
864 for name in provide.bin_symlink_names() {
865 let link = bin_path.join(name);
866 if link.is_symlink() || link.is_file() {
867 std::fs::remove_file(&link).with_context(|| {
868 format!("removing provide {}", link.display())
869 })?;
870 }
871 }
872 }
873 }
874 }
875 }
876
877 self.remove_marker()?;
878
879 Ok(())
880 }
881}