diff options
Diffstat (limited to 'src/pkgtoolkit/install.rs')
| -rw-r--r-- | src/pkgtoolkit/install.rs | 343 |
1 files changed, 343 insertions, 0 deletions
diff --git a/src/pkgtoolkit/install.rs b/src/pkgtoolkit/install.rs new file mode 100644 index 0000000..65476d9 --- /dev/null +++ b/src/pkgtoolkit/install.rs @@ -0,0 +1,343 @@ +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<Vec<String>, std::io::Error>; + fn install(&mut self) -> Result<bool, std::io::Error>; + fn uninstall(&self) -> Result<bool, std::io::Error>; + fn load_manifest(&self) -> Result<PackageManifest, std::io::Error>; + fn list_installed_packages() -> Result<Vec<PackageManifest>, std::io::Error>; + fn is_installed(&self) -> Result<bool, std::io::Error>; +} + +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<Vec<String>, 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<bool, std::io::Error> { + 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<String> = 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 staging_dir = Path::new(&config.paths.cache_dir).join("staging"); + create_dir_all(&staging_dir)?; + all_files = Self::collect_files_from_dir(&staging_dir, &staging_dir)? + .iter() + .map(|rel| format!("/{}", rel)) // Преобразуйте в абсолютные пути (предполагаем root=/) + .collect(); + + for file in &all_files { + let src = staging_dir.join(file.trim_start_matches('/')); + let dest = Path::new(file); + + // Check if source file exists + 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 + )) + })?; + // TODO: Set proper permissions + } + + fs::remove_dir_all(&staging_dir)?; + } else { + log::info!( + "No BUILD file or it's empty. Treating as binary package. Installing via INSTALL config or custom script." + ); + + // Validate custom script before execution + 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 + ); + let source_file_name = &self.name; + let build_dir = Path::new(&config.paths.cache_dir) + .join(format!("{}-{}", self.name, self.version)); + let src_path = build_dir.join(source_file_name); + let dest_path = Path::new(&install_meta.install.path); + + // 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<PackageManifest, 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); + 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<bool, std::io::Error> { + 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<Vec<PackageManifest>, 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<bool, 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); + let manifest_path = installed_db.join(format!("{}-{}.toml", self.name, self.version)); + + Ok(manifest_path.exists()) + } +} |
