use crate::cfg::config::Config; use std::{ fs::{self, Permissions, create_dir_all, set_permissions}, os::unix::fs::PermissionsExt, path::{Path, StripPrefixError}, process::Command, }; use toml; use super::archive::ArchiveOperations; use super::build::BuildOperations; use super::types::{Package, PackageManifest}; pub trait InstallOperations { fn collect_files_from_dir(root: &Path, base: &Path) -> Result, std::io::Error>; fn install(&mut self) -> Result; fn uninstall(&self) -> Result; fn load_manifest(&self) -> Result; fn list_installed_packages() -> Result, std::io::Error>; fn is_installed(&self) -> Result; } impl InstallOperations for Package { /// Recursively collects all files from a directory and its subdirectories /// and returns them as a vector of strings. /// /// # Arguments /// /// * `root`: The root directory from which to collect files. /// * `base`: The base directory from which to strip the prefix from the file paths. /// /// # Returns /// /// A vector of strings containing the file paths relative to the `base` directory. fn collect_files_from_dir(root: &Path, base: &Path) -> Result, std::io::Error> { let mut files = Vec::new(); for entry in fs::read_dir(root)? { let entry = entry?; let path = entry.path(); if path.is_dir() { files.extend(Self::collect_files_from_dir(&path, base)?); } else { let rel_path = path .strip_prefix(base) .map_err(|e: StripPrefixError| { std::io::Error::new(std::io::ErrorKind::InvalidData, e) })? .to_string_lossy() .to_string(); files.push(rel_path); } } Ok(files) } /// Installs the package according to the INSTALL file in the archive. /// /// There are two strategies for installing the package. If the BUILD file is empty, the package is assumed to be a binary package and the default install hook is skipped. If the BUILD file is not empty, the package is assumed to be a source package and the default build hook is skipped. /// /// If the BUILD file is empty and the INSTALL file contains a custom script, the custom script is run instead of the default install hook. /// If the BUILD file is not empty and the INSTALL file contains a custom script, the custom script is ignored. /// /// # Errors /// /// Returns an error if the BUILD file is invalid or if the build or install hook fails. fn install(&mut self) -> Result { let config = Config::parse().map_err(|e| std::io::Error::other(e.to_string()))?; let (install_meta, _setts_meta, build_meta) = Self::loadmeta(self)?; let installed_db = Path::new(&config.paths.installed_db); create_dir_all(installed_db)?; let mut all_files: Vec = Vec::new(); let is_build_present_and_not_empty = build_meta.is_some(); if is_build_present_and_not_empty { log::info!( "Found BUILD file, preparing to build and install package: {}", self.name ); let build_meta_ref = build_meta.as_ref().unwrap(); if let Err(e) = self.execute_build(build_meta_ref) { return Err(std::io::Error::other(format!( "Build failed during installation: {}", e ))); } let build_dir = Path::new(&config.paths.cache_dir).join(format!("{}-{}", self.name, self.version)); let target_release = build_dir.join("target/release"); if !target_release.exists() { return Err(std::io::Error::other(format!( "target/release directory not found: {}", target_release.display() ))); } all_files = Self::collect_files_from_dir(&target_release, &target_release)? .iter() .map(|rel| format!("/{}", rel)) .collect(); for file in &all_files { let src = target_release.join(file.trim_start_matches('/')); let dest = Path::new(&install_meta.install.path).join(file.trim_start_matches('/')); if !src.exists() { log::warn!("Source file not found: {:?}, skipping", src); continue; } if let Some(parent) = dest.parent() { create_dir_all(parent)?; } fs::copy(&src, &dest).map_err(|e| { std::io::Error::other(format!( "Failed to copy {} to {}: {}", src.display(), dest.display(), e )) })?; // Set permissions let mode = u32::from_str_radix(&install_meta.install.mode, 8).map_err(|_| { std::io::Error::new( std::io::ErrorKind::InvalidData, "Invalid mode string in INSTALL", ) })?; let perms = Permissions::from_mode(mode); set_permissions(&dest, perms).map_err(|e| { std::io::Error::other(format!( "Failed to set permissions for {}: {}", dest.display(), e )) })?; let output = Command::new("chown") .arg(format!( "{}:{}", install_meta.install.user, install_meta.install.group )) .arg(&dest) .output() .map_err(|e| std::io::Error::other(format!("'chown' command failed: {}", e)))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); log::warn!( "Warning: 'chown' command failed for {} (requires root?):\n{}", dest.display(), stderr ); } } } else { log::info!( "No BUILD file or it's empty. Treating as binary package. Installing via INSTALL config or custom script." ); if let Some(ref script) = install_meta.install.custom_script { Self::validate_custom_script(script)?; } if let Some(ref script) = install_meta.install.custom_script { log::info!( "Executing custom install script for {}", install_meta.package.name ); let status = if script.starts_with("./") || script.contains('/') { Command::new("/bin/sh").arg("-c").arg(script).status() } else { Command::new(script).status() } .map_err(|e| { std::io::Error::other(format!("Failed to run custom script: {}", e)) })?; if !status.success() { return Err(std::io::Error::other("Custom install script failed")); } log::warn!( "Custom script used; file list may be incomplete. Add manual tracking if needed." ); } else { log::info!( "No custom script. Running default install hook for {}", install_meta.package.name ); log::info!("Starting default install process"); let build_dir = Path::new(&config.paths.cache_dir) .join(format!("{}-{}", self.name, self.version)); let dest_path = Path::new(&install_meta.install.path); log::info!( "Install path: {:?}, is_dir: {}, ends_with_slash: {}", dest_path, dest_path.is_dir(), dest_path.to_string_lossy().ends_with('/') ); if dest_path.is_dir() || dest_path.to_string_lossy().ends_with('/') { log::info!("Taking multi-binary package path"); let target_release = build_dir.join("target/release"); if !target_release.exists() { return Err(std::io::Error::other(format!( "target/release directory not found: {}", target_release.display() ))); } create_dir_all(dest_path).map_err(|e| { std::io::Error::other(format!("Failed to create dest dir: {}", e)) })?; let mut copied_files = Vec::new(); for entry in fs::read_dir(&target_release)? { let entry = entry?; let path = entry.path(); if path.is_file() && let Ok(metadata) = fs::metadata(&path) { let mode = metadata.permissions().mode(); log::debug!("Checking file: {:?}, mode: {:o}", path.file_name(), mode); if mode & 0o111 != 0 { let file_name = path.file_name().unwrap().to_string_lossy(); let dest_file = dest_path.join(&*file_name); log::info!( "Copying executable: {} to {}", file_name, dest_file.display() ); fs::copy(&path, &dest_file).map_err(|e| { std::io::Error::other(format!( "Failed to copy {}: {}", file_name, e )) })?; copied_files.push(dest_file); } else { log::debug!( "File {:?} is not executable (mode: {:o})", path.file_name(), mode ); } } } if copied_files.is_empty() { return Err(std::io::Error::other( "No executable files found in target/release", )); } // Set permissions and ownership for all copied files let mode = u32::from_str_radix(&install_meta.install.mode, 8).map_err(|_| { std::io::Error::new( std::io::ErrorKind::InvalidData, "Invalid mode string in INSTALL", ) })?; let perms = Permissions::from_mode(mode); for file_path in &copied_files { set_permissions(file_path, perms.clone()).map_err(|e| { std::io::Error::other(format!( "Failed to set permissions for {}: {}", file_path.display(), e )) })?; let output = Command::new("chown") .arg(format!( "{}:{}", install_meta.install.user, install_meta.install.group )) .arg(file_path) .output() .map_err(|e| { std::io::Error::other(format!("'chown' command failed: {}", e)) })?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); log::warn!( "Warning: 'chown' command failed for {} (requires root?):\n{}", file_path.display(), stderr ); } } } else { log::info!("Taking single binary package path"); // Single binary package - copy specific file let source_file_name = &self.name; let src_path = build_dir.join(source_file_name); // Check if source file exists if !src_path.exists() { return Err(std::io::Error::other(format!( "Source binary file not found: {}", src_path.display() ))); } if let Some(parent) = dest_path.parent() { create_dir_all(parent).map_err(|e| { std::io::Error::other(format!("Failed to create parent dir: {}", e)) })?; } fs::copy(&src_path, dest_path).map_err(|e| { std::io::Error::other(format!("Failed to copy file: {}", e)) })?; let mode = u32::from_str_radix(&install_meta.install.mode, 8).map_err(|_| { std::io::Error::new( std::io::ErrorKind::InvalidData, "Invalid mode string in INSTALL", ) })?; let perms = Permissions::from_mode(mode); set_permissions(dest_path, perms).map_err(|e| { std::io::Error::other(format!("Failed to set permissions: {}", e)) })?; let output = Command::new("chown") .arg(format!( "{}:{}", install_meta.install.user, install_meta.install.group )) .arg(dest_path) .output() .map_err(|e| { std::io::Error::other(format!("'chown' command failed: {}", e)) })?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); log::warn!( "Warning: 'chown' command failed (requires root?):\n{}", stderr ); } all_files = install_meta.files; } } } let manifest = PackageManifest { name: self.name.clone(), version: self.version.clone(), all_files, }; let manifest_path = installed_db.join(format!("{}-{}.toml", self.name, self.version)); let manifest_toml = toml::to_string_pretty(&manifest).map_err(|e| std::io::Error::other(e.to_string()))?; fs::write(&manifest_path, manifest_toml)?; log::info!( "Package {} installed successfully. Manifest generated at {:?} with {} files tracked", self.name, manifest_path, manifest.all_files.len() ); Ok(true) } /// Loads the package manifest from the installed database fn load_manifest(&self) -> Result { let config = Config::parse().map_err(|e| std::io::Error::other(e.to_string()))?; let installed_db = Path::new(&config.paths.installed_db); let manifest_path = installed_db.join(format!("{}-{}.toml", self.name, self.version)); if !manifest_path.exists() { return Err(std::io::Error::other(format!( "Package manifest not found: {:?}", manifest_path ))); } let manifest_content = fs::read_to_string(&manifest_path)?; let manifest: PackageManifest = toml::from_str(&manifest_content) .map_err(|e| std::io::Error::other(format!("Failed to parse manifest: {}", e)))?; Ok(manifest) } /// Uninstalls the package by removing all files listed in the manifest /// and then removing the manifest file itself fn uninstall(&self) -> Result { let manifest = self.load_manifest()?; log::info!( "Uninstalling package {}-{}", manifest.name, manifest.version ); let mut removed_files = 0; let mut failed_removals = 0; for file_path in &manifest.all_files { let path = Path::new(file_path); if path.exists() { match fs::remove_file(path) { Ok(()) => { removed_files += 1; log::debug!("Removed file: {:?}", path); } Err(e) => { failed_removals += 1; log::warn!("Failed to remove file {:?}: {}", path, e); } } } else { log::debug!("File not found, skipping: {:?}", path); } } // Remove the manifest file let config = Config::parse().map_err(|e| std::io::Error::other(e.to_string()))?; let installed_db = Path::new(&config.paths.installed_db); let manifest_path = installed_db.join(format!("{}-{}.toml", self.name, self.version)); if manifest_path.exists() { fs::remove_file(&manifest_path)?; log::info!("Removed manifest file: {:?}", manifest_path); } log::info!( "Package {}-{} uninstalled successfully. Removed {} files, {} failures", manifest.name, manifest.version, removed_files, failed_removals ); Ok(failed_removals == 0) } /// Lists all installed packages by reading manifest files from the installed database fn list_installed_packages() -> Result, std::io::Error> { let config = Config::parse().map_err(|e| std::io::Error::other(e.to_string()))?; let installed_db = Path::new(&config.paths.installed_db); if !installed_db.exists() { return Ok(Vec::new()); } let mut packages = Vec::new(); for entry in fs::read_dir(installed_db)? { let entry = entry?; let path = entry.path(); if path.extension().and_then(|s| s.to_str()) == Some("toml") { let manifest_content = fs::read_to_string(&path)?; let manifest: PackageManifest = toml::from_str(&manifest_content).map_err(|e| { std::io::Error::other(format!("Failed to parse manifest {:?}: {}", path, e)) })?; packages.push(manifest); } } Ok(packages) } /// Checks if the package is currently installed by looking for its manifest fn is_installed(&self) -> Result { let config = Config::parse().map_err(|e| std::io::Error::other(e.to_string()))?; let installed_db = Path::new(&config.paths.installed_db); let manifest_path = installed_db.join(format!("{}-{}.toml", self.name, self.version)); Ok(manifest_path.exists()) } }