diff options
| author | Namilskyy <alive6863@gmail.com> | 2025-12-06 16:36:44 +0300 |
|---|---|---|
| committer | Namilskyy <alive6863@gmail.com> | 2025-12-06 16:36:44 +0300 |
| commit | def46ae74c3f5974ed448e9877b0e0067a8e67d2 (patch) | |
| tree | 4eda9fe8b6da96ea8f1824a14235286fe6e49c72 /src/pkgtoolkit | |
| parent | 79c8ecb6bf4d2fc2df5c90007e7c26b456ddc33f (diff) | |
Big code-cleaning in pkgtoolkit, implemented much functions and fixed logical mistakes
Diffstat (limited to 'src/pkgtoolkit')
| -rw-r--r-- | src/pkgtoolkit/archive.rs | 377 | ||||
| -rw-r--r-- | src/pkgtoolkit/build.rs | 349 | ||||
| -rw-r--r-- | src/pkgtoolkit/index.rs | 135 | ||||
| -rw-r--r-- | src/pkgtoolkit/install.rs | 343 | ||||
| -rw-r--r-- | src/pkgtoolkit/mod.rs | 55 | ||||
| -rw-r--r-- | src/pkgtoolkit/pkgtools.rs | 615 | ||||
| -rw-r--r-- | src/pkgtoolkit/types.rs | 89 |
7 files changed, 1347 insertions, 616 deletions
diff --git a/src/pkgtoolkit/archive.rs b/src/pkgtoolkit/archive.rs new file mode 100644 index 0000000..9d9a7d1 --- /dev/null +++ b/src/pkgtoolkit/archive.rs @@ -0,0 +1,377 @@ +use crate::cfg::config::Config; +use std::{ + fs::{self, File, create_dir_all}, + io, + os::unix::fs::PermissionsExt, + path::Path, +}; + +use flate2::read::GzDecoder; +use tar::Archive; +use toml; + +use super::types::{Build, Install, Package, Setts}; + +pub trait ArchiveOperations { + fn extract_archive(path_to_archive: &str) -> Result<(), std::io::Error>; + fn check(path_to_archive: String) -> Result<bool, std::io::Error>; + fn loadmeta( + minimal_package_meta: &mut Package, + ) -> Result<(Install, Option<Setts>, Option<Build>), std::io::Error>; + fn validate_custom_script(script: &str) -> Result<(), std::io::Error>; + fn validate_path(path: &str, base_dir: &Path) -> Result<std::path::PathBuf, std::io::Error>; +} + +impl ArchiveOperations for Package { + /// Extracts a .tar.gz archive to the cache directory specified in Config. + /// + /// This function handles opening the archive file, decompressing it with GzDecoder, + /// and unpacking the contents into the configured cache directory. + /// + /// # Arguments + /// * `path_to_archive` - A string representing the path to the .tar.gz file. + /// + /// # Returns + /// * `Ok(())` if the archive is successfully unpacked. + /// * `Err(std::io::Error)` if there's an issue opening, reading, or unpacking the archive. + fn extract_archive(path_to_archive: &str) -> Result<(), std::io::Error> { + let config = Config::parse().map_err(|e| std::io::Error::other(e.to_string()))?; + let cache_dir = &config.paths.cache_dir; + create_dir_all(cache_dir)?; + + for meta_name in ["INSTALL", "SETTS", "BUILD"] { + let meta_path = Path::new(cache_dir).join(meta_name); + if meta_path.exists() { + fs::remove_file(&meta_path)?; + } + } + let file = File::open(path_to_archive)?; + let gz = GzDecoder::new(file); + let mut archive = Archive::new(gz); + // Unpack directly into the cache directory. Игнорируем AlreadyExists, чтобы не мешать валидации. + match archive.unpack(cache_dir) { + Ok(()) => Ok(()), + Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(()), + Err(e) => Err(e), + } + } + + /// Load meta information from the .mesk archive. + /// + /// This function parses the meta information from the .mesk archive, + /// which includes the package name, version, architecture, description, + /// installation path, user, group, mode, and custom installation script + /// and deserializing this information. Returns (Install, Option<Setts>, Option<Build>) + /// + /// The function expects the 'INSTALL', 'SETTS', and 'BUILD' files to be present + /// in the `config.paths.cache_dir`. It specifically requires the 'INSTALL' file. + /// + /// # Errors + /// + /// Returns an error if the `cache_dir` cannot be created, if the required 'INSTALL' file + /// is not found, or if the 'INSTALL' file is empty. + #[allow(clippy::type_complexity)] + fn loadmeta( + minimal_package_meta: &mut Self, + ) -> Result<(Install, Option<Setts>, Option<Build>), std::io::Error> { + // Changed return type for more flexibility + /* + Example INSTALL format: + [package] + name = "my-package" + version = "1.0.0" + arch = "X86_64" + descr = "Just example INSTALL script" + + [install] + path = "/usr/bin/my-package" + user = "root" + group = "root" + mode = "755" + + # Also [install] can be + # path = "/usr/bin/my-package" + # user = "root" + # group = "root" + # mode = "755" + # custom_script = "./install.sh" OR + # custom_script = """ + # echo "Installing my-package" + # sudo apt-get install my-package + # """ + */ + + let config = Config::parse().map_err(|e| std::io::Error::other(e.to_string()))?; + + fs::create_dir_all(&config.paths.cache_dir)?; + + let cache_dir = &config.paths.cache_dir; + let install_path = + Path::new(cache_dir).join(format!("{}/INSTALL", minimal_package_meta.name)); + let setts_path = Path::new(cache_dir).join(format!("{}/SETTS", minimal_package_meta.name)); + let build_path = Path::new(cache_dir).join(format!("{}/BUILD", minimal_package_meta.name)); + + if !install_path.exists() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + "File INSTALL not found in cache directory", + )); + } + + let install_content = fs::read_to_string(&install_path)?; + let install_meta: Install = toml::from_str(&install_content) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + let mut setts_meta: Option<Setts> = None; + let mut build_meta: Option<Build> = None; + + if setts_path.exists() { + let setts_content = fs::read_to_string(&setts_path)?; + setts_meta = Some( + toml::from_str(&setts_content) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, + ); + } + + if build_path.exists() { + let build_content = fs::read_to_string(&build_path)?; + build_meta = Some( + toml::from_str(&build_content) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, + ); + } + + if let Some(ref _script) = install_meta.install.custom_script { + log::warn!( + "Custom script found for package: {}", + install_meta.package.name + ); + } else { + log::info!( + "No custom script for package: {}", + install_meta.package.name + ); + } + + Ok((install_meta, setts_meta, build_meta)) + } + + /// Checks if the archive contains INSTALL, SETTS and BUILD files. + /// + /// Checks if INSTALL file exists and is not empty. If it does not exist or is empty, returns an error. + /// Checks SETTS and BUILD (if present) and logs предупреждения, если они пустые или отсутствуют. + /// Ожидаем структуру: <cachedir>/<packagename>/{INSTALL, SETTS, BUILD, ...}. + /// + /// # Errors + /// * Returns an error if INSTALL file does not exist or is empty. + /// * Returns an error if INSTALL file is empty. + fn check(path_to_archive: String) -> Result<bool, std::io::Error> { + Self::extract_archive(&path_to_archive)?; + + let config = Config::parse().map_err(|e| std::io::Error::other(e.to_string()))?; + let cache_root = Path::new(&config.paths.cache_dir); + + // Рекурсивно находим все файлы INSTALL под cache_root + fn find_install(root: &Path) -> Result<Vec<std::path::PathBuf>, std::io::Error> { + let mut installs = Vec::new(); + for entry in fs::read_dir(root)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + installs.extend(find_install(&path)?); + } else if path.file_name().and_then(|n| n.to_str()) == Some("INSTALL") { + installs.push(path); + } + } + Ok(installs) + } + + let installs = find_install(cache_root)?; + if installs.is_empty() { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "INSTALL file not found in archive", + )); + } + if installs.len() > 1 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Multiple INSTALL files found in archive; expected exactly one", + )); + } + + let install_path = &installs[0]; + let pkg_dir = install_path.parent().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + "INSTALL has no parent directory", + ) + })?; + + // Проверим, что структура именно <cachedir>/<packagename>/INSTALL + if pkg_dir.parent() != Some(cache_root) { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid meta-files layout: expected <cachedir>/<packagename>/INSTALL", + )); + } + + let setts_path = pkg_dir.join("SETTS"); + let build_path = pkg_dir.join("BUILD"); + + let install_content = std::fs::read_to_string(install_path)?; + if install_content.trim().is_empty() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "INSTALL file is empty", + )); + } + + if !setts_path.exists() { + log::warn!("SETTS file not found in archive. Make sure you dont need this."); + } else { + let setts_content = std::fs::read_to_string(&setts_path)?; + if setts_content.trim().is_empty() { + log::warn!("SETTS file is empty. Make sure you dont need this."); + } + } + + if !build_path.exists() { + log::warn!("BUILD file not found in archive. Make sure you dont need this."); + } else { + let build_content = std::fs::read_to_string(&build_path)?; + if build_content.trim().is_empty() { + log::warn!("BUILD file is empty. Make sure you dont need this."); + } + } + + let content = std::fs::read_to_string(install_path).map_err(|e| { + log::warn!("Failed to read file: {}", e); + e + })?; + let install_content: Result<Install, toml::de::Error> = toml::from_str(&content); + + log::info!("Validating arch..."); + let install_parsed = match install_content { + Ok(v) => v, + Err(e) => { + log::error!("Arch mismatch while parsing INSTALL: {}", e); + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Arch mismatch: {}", e), + )); + } + }; + if std::env::consts::ARCH != install_parsed.package.arch.as_str() { + let pkg_arch = &install_parsed.package.arch; + log::error!( + "Arch mismatch. Package arch: {:?}, Host arch: {}", + pkg_arch, + std::env::consts::ARCH + ); + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Arch mismatch", + )); + } + + Ok(true) + } + + /// Validates a custom script for security and basic syntax + /// Now only minimal validation is performed + fn validate_custom_script(script: &str) -> Result<(), std::io::Error> { + // Check for dangerous commands + let dangerous_patterns = [ + "rm -rf /", + "sudo rm", + "chmod 777", + "chown root", + "dd if=", + "mkfs", + "fdisk", + "format", + ":(){ :|:& };:", + ]; + + for pattern in &dangerous_patterns { + if script.contains(pattern) { + return Err(std::io::Error::other(format!( + "Dangerous command detected in script: {}", + pattern + ))); + } + } + + // Check for script path existence if it's a file path + if script.starts_with("./") { + let script_path = Path::new(script); + if !script_path.exists() { + return Err(std::io::Error::other(format!( + "Script file not found: {}", + script + ))); + } + + // Check if it's executable + let metadata = fs::metadata(script_path)?; + if !metadata.permissions().mode() & 0o111 != 0 { + log::warn!("Script {} is not executable", script); + } + } + + Ok(()) + } + + /// Validates and sanitizes a file path for security + fn validate_path(path: &str, base_dir: &Path) -> Result<std::path::PathBuf, std::io::Error> { + let path = Path::new(path); + + // Convert to absolute path + let absolute_path = if path.is_absolute() { + path.to_path_buf() + } else { + base_dir.join(path) + }; + + // Normalize the path + let normalized = match absolute_path.canonicalize() { + Ok(p) => p, + Err(_) => { + // If canonicalization fails, at least clean up the path + let mut cleaned = std::path::PathBuf::new(); + for component in absolute_path.components() { + match component { + std::path::Component::ParentDir => { + // Don't go above base directory + if cleaned.pop() { + // Successfully removed parent + } else { + return Err(std::io::Error::other( + "Path traversal attempt detected", + )); + } + } + std::path::Component::CurDir => { + // Skip current directory + } + _ => cleaned.push(component), + } + } + cleaned + } + }; + + // Ensure the path is within the base directory (for relative paths) + if !path.is_absolute() + && let Ok(base_absolute) = base_dir.canonicalize() + && !normalized.starts_with(&base_absolute) + { + return Err(std::io::Error::other( + "Path is outside of allowed directory", + )); + } + + Ok(normalized) + } +} diff --git a/src/pkgtoolkit/build.rs b/src/pkgtoolkit/build.rs new file mode 100644 index 0000000..a323521 --- /dev/null +++ b/src/pkgtoolkit/build.rs @@ -0,0 +1,349 @@ +use crate::cfg::config::Config; +use std::{fs::create_dir_all, path::Path, process::Command}; + +use num_cpus; + +use super::archive::ArchiveOperations; +use super::types::{Build, BuildSystems, Package}; + +pub trait BuildOperations { + fn execute_build(&self, build_meta: &Build) -> Result<(), std::io::Error>; + fn build(&mut self) -> Result<bool, std::io::Error>; +} + +#[allow(dead_code)] +impl BuildOperations for Package { + /// Execute the build script for the package. + /// + /// This function takes the `Build` meta information as an argument and + /// executes the build script accordingly. It also handles the different + /// build systems supported (Make, CMake, Meson, Cargo). + /// + /// # Errors + /// + /// Returns an error if the build command fails. + fn execute_build(&self, build_meta: &Build) -> Result<(), std::io::Error> { + let config = Config::parse().map_err(|e| std::io::Error::other(e.to_string()))?; + let build_dir = + Path::new(&config.paths.cache_dir).join(format!("{}-{}", self.name, self.version)); + // Check if build directory exists + if !build_dir.exists() { + return Err(std::io::Error::other(format!( + "Build directory not found: {}", + build_dir.display() + ))); + } + + // Prepare environment variables + let mut cmd_envs: Vec<(String, String)> = Vec::new(); + if let Some(ref env_vars) = build_meta.env { + for env_line in env_vars.lines() { + if let Some((key, value)) = env_line.split_once('=') { + cmd_envs.push((key.trim().to_string(), value.trim().to_string())); + } + } + } + + // Handle custom build script if provided + if let Some(ref script) = build_meta.script { + log::info!("Executing custom build script: {}", script); + Self::validate_custom_script(script)?; + let mut cmd = if script.starts_with("./") || script.contains('/') { + // Assume it's a file path + let script_path = build_dir.join(script); + if !script_path.exists() { + return Err(std::io::Error::other(format!( + "Custom script file not found: {}", + script_path.display() + ))); + } + let mut inner_cmd = Command::new("/bin/sh"); + inner_cmd.arg("-c"); + inner_cmd.arg(script_path.to_str().unwrap()); + inner_cmd + } else { + // Inline script + let mut inner_cmd = Command::new("/bin/sh"); + inner_cmd.arg("-c"); + inner_cmd.arg(script); + inner_cmd + }; + cmd.current_dir(&build_dir); + for (key, value) in &cmd_envs { + cmd.env(key, value); + } + let output = cmd + .output() + .map_err(|e| std::io::Error::other(format!("Custom build script failed: {}", e)))?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + return Err(std::io::Error::other(format!( + "Custom build script failed:\nStdout: {}\nStderr: {}", + stdout, stderr + ))); + } + log::info!( + "Custom build script completed successfully for package: {}", + self.name + ); + return Ok(()); + } + + // No custom script, proceed with build system + match build_meta.build_system { + BuildSystems::Make => { + // Check for Makefile + let makefile_path = build_dir.join("Makefile"); + if !makefile_path.exists() { + return Err(std::io::Error::other(format!( + "Makefile not found: {}", + makefile_path.display() + ))); + } + let mut cmd = Command::new("make"); + cmd.current_dir(&build_dir); + cmd.arg("all"); + for (key, value) in &cmd_envs { + cmd.env(key, value); + } + log::info!("Running Make build: {:?}", cmd); + let output = cmd + .output() + .map_err(|e| std::io::Error::other(format!("Make build failed: {}", e)))?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + return Err(std::io::Error::other(format!( + "Make build failed:\nStdout: {}\nStderr: {}", + stdout, stderr + ))); + } + } + BuildSystems::CMake => { + // Check for CMakeLists.txt + let cmake_file = build_dir.join("CMakeLists.txt"); + if !cmake_file.exists() { + return Err(std::io::Error::other(format!( + "CMakeLists.txt not found: {}", + cmake_file.display() + ))); + } + let build_dir_build = build_dir.join("build"); + create_dir_all(&build_dir_build)?; + let mut config_cmd = Command::new("cmake"); + config_cmd + .arg("-S") + .arg(&build_dir) + .arg("-B") + .arg(&build_dir_build) + .current_dir(&build_dir); + for (key, value) in &cmd_envs { + config_cmd.env(key, value); + } + log::info!("Running CMake configuration: {:?}", config_cmd); + let config_output = config_cmd + .output() + .map_err(|e| std::io::Error::other(format!("CMake config failed: {}", e)))?; + if !config_output.status.success() { + let stderr = String::from_utf8_lossy(&config_output.stderr); + return Err(std::io::Error::other(format!( + "CMake config failed:\n{}", + stderr + ))); + } + + // Now build + let mut build_cmd = Command::new("make"); + build_cmd.current_dir(&build_dir_build); + build_cmd.arg("-j").arg(num_cpus::get().to_string()); // Parallel build + for (key, value) in &cmd_envs { + build_cmd.env(key, value); + } + log::info!("Running CMake build: {:?}", build_cmd); + let build_output = build_cmd + .output() + .map_err(|e| std::io::Error::other(format!("CMake build failed: {}", e)))?; + if !build_output.status.success() { + let stderr = String::from_utf8_lossy(&build_output.stderr); + let stdout = String::from_utf8_lossy(&build_output.stdout); + return Err(std::io::Error::other(format!( + "CMake build failed:\nStdout: {}\nStderr: {}", + stdout, stderr + ))); + } + } + BuildSystems::Meson => { + // Check for meson.build + let meson_file = build_dir.join("meson.build"); + if !meson_file.exists() { + return Err(std::io::Error::other(format!( + "meson.build not found: {}", + meson_file.display() + ))); + } + let build_dir_build = build_dir.join("build"); + create_dir_all(&build_dir_build)?; + let mut config_cmd = Command::new("meson"); + config_cmd + .arg("setup") + .arg(&build_dir_build) + .current_dir(&build_dir); + for (key, value) in &cmd_envs { + config_cmd.env(key, value); + } + log::info!("Running Meson configuration: {:?}", config_cmd); + let config_output = config_cmd + .output() + .map_err(|e| std::io::Error::other(format!("Meson config failed: {}", e)))?; + if !config_output.status.success() { + let stderr = String::from_utf8_lossy(&config_output.stderr); + return Err(std::io::Error::other(format!( + "Meson config failed:\n{}", + stderr + ))); + } + + // Now build + let mut build_cmd = Command::new("ninja"); + build_cmd.current_dir(&build_dir_build); + build_cmd.arg("-j").arg(num_cpus::get().to_string()); // Parallel build + for (key, value) in &cmd_envs { + build_cmd.env(key, value); + } + log::info!("Running Meson build: {:?}", build_cmd); + let build_output = build_cmd + .output() + .map_err(|e| std::io::Error::other(format!("Meson build failed: {}", e)))?; + if !build_output.status.success() { + let stderr = String::from_utf8_lossy(&build_output.stderr); + let stdout = String::from_utf8_lossy(&build_output.stdout); + return Err(std::io::Error::other(format!( + "Meson build failed:\nStdout: {}\nStderr: {}", + stdout, stderr + ))); + } + } + BuildSystems::Cargo => { + // Check for Cargo.toml + let cargo_file = build_dir.join("Cargo.toml"); + if !cargo_file.exists() { + return Err(std::io::Error::other(format!( + "Cargo.toml not found: {}", + cargo_file.display() + ))); + } + let mut cmd = Command::new("cargo"); + cmd.arg("build").arg("--release").current_dir(&build_dir); + for (key, value) in &cmd_envs { + cmd.env(key, value); + } + log::info!("Running Cargo build: {:?}", cmd); + let output = cmd + .output() + .map_err(|e| std::io::Error::other(format!("Cargo build failed: {}", e)))?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + return Err(std::io::Error::other(format!( + "Cargo build failed:\nStdout: {}\nStderr: {}", + stdout, stderr + ))); + } + } + } + + log::info!("Build completed successfully for package: {}", self.name); + Ok(()) + } + + /// Builds the package according to the BUILD file in the archive. + /// + /// There are two strategies for building 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 build(&mut self) -> Result<bool, std::io::Error> { + let meta = Self::loadmeta(self)?; + let install_meta = meta.0; + // let setts_meta = meta.1; + let build_meta = meta.2; + + // BUILD NOT EMPTY. SOURCE: -> BUILD -> INSTALL -> SETTS + // BUILD EMPTY. BIN: -> INSTALL -> SETTS + enum Strategies { + Bin, + Source, + } + + let strategy; //default + + if build_meta.is_none() { + log::info!("BUILD file is empty. Skipping build, preparing to install"); + strategy = Strategies::Bin; + } else { + strategy = Strategies::Source; + log::info!("BUILD file is not empty. Skipping install, preparing to build"); + } + + match strategy { + Strategies::Bin => { + if install_meta.install.custom_script.is_none() { + log::info!("Strategy: BIN; No custom script. Running default install hook."); + } else { + log::info!("Strategy: BIN; Running custom script."); + let script = install_meta.install.custom_script.as_ref().unwrap(); + + // Validate script before execution + Self::validate_custom_script(script)?; + + if !script.starts_with("./") { + let output = std::process::Command::new(script).output().map_err(|e| { + std::io::Error::other(format!("Failed to execute custom script: {}", e)) + })?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(std::io::Error::other(format!( + "Custom script failed:\n{}", + stderr + ))); + } + } else { + let output = std::process::Command::new("/bin/sh") + .arg("-c") + .arg(script) + .output() + .map_err(|e| { + std::io::Error::other(format!( + "Failed to execute custom script: {}", + e + )) + })?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(std::io::Error::other(format!( + "Custom script failed:\n{}", + stderr + ))); + } + } + } + } + Strategies::Source => { + log::info!("Strategy: SOURCE; Running default build hook."); + if let Err(e) = self.execute_build(&build_meta.unwrap()) { + return Err(std::io::Error::other(format!("Build failed: {}", e))); + } + } + } + + Ok(true) + } +} diff --git a/src/pkgtoolkit/index.rs b/src/pkgtoolkit/index.rs new file mode 100644 index 0000000..37e02e4 --- /dev/null +++ b/src/pkgtoolkit/index.rs @@ -0,0 +1,135 @@ +use std::{ + fs::{self, File, create_dir_all}, + io, + path::Path, +}; + +use flate2::read::GzDecoder; +use tar::Archive; +use toml; + +use super::types::{Index, Install, Package}; + +#[allow(dead_code)] +pub trait IndexOperations { + fn gen_index(repo_path: &str) -> Result<bool, std::io::Error>; +} + +impl IndexOperations for Package { + /// Generates an INDEX.toml file for a given repository directory. + /// + /// This function scans the `repo_path` directory for `.mesk` files. + /// For each `.mesk` file found, it extracts the `INSTALL` metadata to get package details. + /// It then collects all package information and serializes it into an `INDEX.toml` file + /// located in the root of the `repo_path` directory. + /// If `license` or `descr` fields are missing in the `INSTALL` file, they are set to empty strings (""). + /// + /// # Arguments + /// * `repo_path` - A string slice representing the path to the repository directory. + /// + /// # Errors + /// + /// Returns an error if: + /// - The `repo_path` directory cannot be read. + /// - An `.mesk` file cannot be opened or read. + /// - The `INSTALL` file cannot be extracted from an archive. + /// - The `INSTALL` file content cannot be parsed as TOML. + /// - The final `INDEX.toml` file cannot be written to disk. + fn gen_index(repo_path: &str) -> Result<bool, std::io::Error> { + let repo_dir = Path::new(repo_path); + if !repo_dir.is_dir() { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("Repository directory does not exist: {}", repo_path), + )); + } + + let mut all_packages = Vec::new(); + + for entry_res in fs::read_dir(repo_path)? { + let entry = entry_res?; + let path = entry.path(); + + if path.extension().and_then(|s| s.to_str()) == Some("mesk") { + log::info!("Processing archive for index: {}", path.display()); + + let temp_extract_dir = std::env::temp_dir().join(format!( + "mesk_index_temp_{}", + path.file_stem().unwrap_or_default().to_string_lossy() + )); + create_dir_all(&temp_extract_dir)?; + + let file = File::open(&path)?; + let gz = GzDecoder::new(file); + let mut archive = Archive::new(gz); + + for tar_entry_res in archive.entries()? { + let mut tar_entry = tar_entry_res?; + let entry_path = tar_entry.path()?; + if entry_path.file_name().and_then(|n| n.to_str()) == Some("INSTALL") { + if let Some(parent_name) = entry_path + .parent() + .and_then(|p| p.file_name()) + .and_then(|n| n.to_str()) + { + let install_extract_path = + temp_extract_dir.join(parent_name).join("INSTALL"); + if let Some(parent_dir) = install_extract_path.parent() { + create_dir_all(parent_dir)?; + } + tar_entry.unpack(&install_extract_path)?; + log::debug!( + "Extracted INSTALL from {} to {}", + path.display(), + install_extract_path.display() + ); + + let install_content = fs::read_to_string(&install_extract_path)?; + let install_data: Install = toml::from_str(&install_content) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + let pkg_for_index = Package { + name: install_data.package.name, + version: install_data.package.version, + arch: install_data.package.arch, + descr: Some( + install_data.package.descr.unwrap_or_default(), + ), + license: Some( + install_data + .package + .license + .unwrap_or_default(), + ), + url: install_data.package.url, + }; + + all_packages.push(pkg_for_index); + break; + } else { + log::warn!( + "INSTALL file in archive {} has unexpected path structure, skipping.", + path.display() + ); + } + } + } + fs::remove_dir_all(&temp_extract_dir)?; + } + } + + let index = Index { + packages: all_packages, + }; + + let index_toml_content = toml::to_string_pretty(&index) + .map_err(|e| std::io::Error::other(format!("Failed to serialize INDEX.toml: {}", e)))?; + + let index_path = repo_dir.join("INDEX.toml"); + fs::write(&index_path, index_toml_content) + .map_err(|e| std::io::Error::other(format!("Failed to write INDEX.toml: {}", e)))?; + + log::info!("Successfully generated INDEX.toml at {:?}", index_path); + Ok(true) + } +} 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()) + } +} diff --git a/src/pkgtoolkit/mod.rs b/src/pkgtoolkit/mod.rs index 63c952a..2fc316a 100644 --- a/src/pkgtoolkit/mod.rs +++ b/src/pkgtoolkit/mod.rs @@ -1 +1,54 @@ -pub mod pkgtools; +// Core package toolkit modules +pub mod archive; +pub mod build; +pub mod index; +pub mod install; +pub mod types; + +// ============================================================================ +// Public API - Core Types and Data Structures +// ============================================================================ + +// Package metadata and core types +#[allow(unused_imports)] +pub use types::{ + Archs, // Supported architectures + Package, // Package information + PackageManifest, // Installation manifest for tracking +}; + +// Installation and configuration types +#[allow(unused_imports)] +pub use types::{ + Install, // Complete installation specification + InstallMeta, // Installation metadata + Setts, // Package settings and environment +}; + +// Build system types +#[allow(unused_imports)] +pub use types::{ + Build, // Build configuration + BuildSystems, // Supported build systems + Index, // Package index +}; + +// ============================================================================ +// Public API - Package Operations Traits +// ============================================================================ + +// Archive operations for package extraction and validation +#[allow(unused_imports)] +pub use archive::ArchiveOperations; + +// Build operations for compiling packages +#[allow(unused_imports)] +pub use build::BuildOperations; + +// Install operations for package installation and manifest generation +#[allow(unused_imports)] +pub use install::InstallOperations; + +// Index operations for package repository management +#[allow(unused_imports)] +pub use index::IndexOperations; diff --git a/src/pkgtoolkit/pkgtools.rs b/src/pkgtoolkit/pkgtools.rs deleted file mode 100644 index 2a8a18e..0000000 --- a/src/pkgtoolkit/pkgtools.rs +++ /dev/null @@ -1,615 +0,0 @@ -use crate::cfg::config::Config; -use std::{ - fs::{self, File, Permissions, create_dir_all, set_permissions}, - io, - os::unix::fs::PermissionsExt, - path::{Path, StripPrefixError}, - process::Command, - str, -}; -// use emissary_core::i2np::tunnel::build; -use flate2::read::GzDecoder; -use serde::{Deserialize, Serialize}; -use tar::Archive; -use toml; - -#[derive(Serialize, Debug, Deserialize, Clone)] -pub enum Archs { - X86_64, - Aarch64, - X86, - ArmV7, - ArmV8, -} - -#[derive(Serialize, Debug, Deserialize, Clone)] -pub struct Package { - pub name: String, - pub version: String, - pub arch: Archs, - pub descr: Option<String>, - pub license: Option<String>, - pub url: String, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct InstallMeta { - pub path: String, - pub user: String, - pub group: String, - pub mode: String, - // Cancels the previous fields and installs them using the shell script - pub custom_script: Option<String>, - // pub files: Option<Vec<String>>, -} - -#[allow(dead_code)] -#[derive(Deserialize, Debug, Clone)] -struct Install { - package: Package, - install: InstallMeta, - #[serde(default)] - files: Vec<String>, -} - -#[allow(dead_code)] -#[derive(Deserialize, Debug)] -struct Setts { - // Export environment variables if this needed - env: Option<String>, - // Test the package after installation - test: Option<String>, -} - -#[derive(Deserialize, Serialize, Debug)] -pub enum BuildSystems { - Make, - CMake, - Meson, - Cargo, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct PackageManifest { - pub name: String, - pub version: String, - pub all_files: Vec<String>, -} - -#[allow(dead_code)] -#[derive(Deserialize)] -struct Build { - build_system: BuildSystems, - env: Option<String>, - script: Option<String>, -} - -impl Archs { - pub fn as_str(&self) -> &'static str { - match self { - Archs::X86_64 => "x86_64", - Archs::Aarch64 => "aarch64", - Archs::X86 => "x86", - Archs::ArmV7 => "armv7", - Archs::ArmV8 => "armv8", - } - } -} - -#[allow(dead_code)] -impl Package { - /// Execute the build script for the package. - /// - /// This function takes the `Build` meta information as an argument and - /// executes the build script accordingly. It also handles the different - /// build systems supported (Make, CMake, Meson, Cargo). - /// - /// # Errors - /// - /// Returns an error if the build command fails. - fn execute_build(&self, build_meta: &Build) -> Result<(), std::io::Error> { - let config = Config::parse().map_err(|e| std::io::Error::other(e.to_string()))?; - let build_dir = - Path::new(&config.paths.cache_dir).join(format!("{}-{}", self.name, self.version)); - - let mut cmd = match build_meta.build_system { - BuildSystems::Make => { - let mut c = Command::new("make"); - c.current_dir(&build_dir); - if build_meta.script.is_some() { - c.arg("-f").arg(build_meta.script.as_ref().unwrap()); - } - c.arg("all"); - c - } - BuildSystems::CMake => { - let build_dir_build = build_dir.join("build"); - create_dir_all(&build_dir_build)?; - let mut c = Command::new("cmake"); - c.arg("-S") - .arg(&build_dir) - .arg("-B") - .arg(&build_dir_build) - .current_dir(&build_dir); - c - } - BuildSystems::Meson => { - let build_dir_build = build_dir.join("build"); - create_dir_all(&build_dir_build)?; - let mut c = Command::new("meson"); - c.arg("setup").arg(&build_dir_build).current_dir(&build_dir); - c - } - BuildSystems::Cargo => { - let mut c = Command::new("cargo"); - c.arg("build").arg("--release").current_dir(&build_dir); - c - } - }; - - let output = cmd - .output() - .map_err(|e| std::io::Error::other(format!("Build command failed: {}", e)))?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(std::io::Error::other(format!("Build failed:\n{}", stderr))); - } - - Ok(()) - } - - /// 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| io::Error::new(io::ErrorKind::InvalidData, e))? - .to_string_lossy() - .to_string(); - files.push(rel_path); - } - } - Ok(files) - } - - /// Extracts a .tar.gz archive to the cache directory specified in Config. - /// - /// This function handles opening the archive file, decompressing it with GzDecoder, - /// and unpacking the contents into the configured cache directory. - /// - /// # Arguments - /// * `path_to_archive` - A string representing the path to the .tar.gz file. - /// - /// # Returns - /// * `Ok(())` if the archive is successfully unpacked. - /// * `Err(std::io::Error)` if there's an issue opening, reading, or unpacking the archive. - pub fn extract_archive(path_to_archive: &str) -> Result<(), std::io::Error> { - let config = Config::parse().map_err(|e| std::io::Error::other(e.to_string()))?; - let cache_dir = &config.paths.cache_dir; - create_dir_all(cache_dir)?; - // Очистим возможные мета-файлы предыдущих распаковок, чтобы не было утечек состояния - for meta_name in ["INSTALL", "SETTS", "BUILD"] { - let meta_path = Path::new(cache_dir).join(meta_name); - if meta_path.exists() { - fs::remove_file(&meta_path)?; - } - } - let file = File::open(path_to_archive)?; - let gz = GzDecoder::new(file); - let mut archive = Archive::new(gz); - // Unpack directly into the cache directory. Игнорируем AlreadyExists, чтобы не мешать валидации. - match archive.unpack(cache_dir) { - Ok(()) => Ok(()), - Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(()), - Err(e) => Err(e), - } - } - - /// Load meta information from the .mesk archive. - /// - /// This function parses the meta information from the .mesk archive, - /// which includes the package name, version, architecture, description, - /// installation path, user, group, mode, and custom installation script - /// and deserializing this information. Returns (Install, Option<Setts>, Option<Build>) - /// - /// The function expects the 'INSTALL', 'SETTS', and 'BUILD' files to be present - /// in the `config.paths.cache_dir`. It specifically requires the 'INSTALL' file. - /// - /// # Errors - /// - /// Returns an error if the `cache_dir` cannot be created, if the required 'INSTALL' file - /// is not found, or if the 'INSTALL' file is empty. - #[allow(clippy::type_complexity)] - fn loadmeta( - minimal_package_meta: &mut Self, - ) -> Result<(Install, Option<Setts>, Option<Build>), std::io::Error> { - // Changed return type for more flexibility - /* - Example INSTALL format: - [package] - name = "my-package" - version = "1.0.0" - arch = "X86_64" - descr = "Just example INSTALL script" - - [install] - path = "/usr/bin/my-package" - user = "root" - group = "root" - mode = "755" - - # Also [install] can be - # path = "/usr/bin/my-package" - # user = "root" - # group = "root" - # mode = "755" - # custom_script = "./install.sh" OR - # custom_script = """ - # echo "Installing my-package" - # sudo apt-get install my-package - # """ - */ - - let config = Config::parse().map_err(|e| std::io::Error::other(e.to_string()))?; // Propagate error if parsing fails - - // Ensure the cache directory exists - fs::create_dir_all(&config.paths.cache_dir)?; - - let cache_dir = &config.paths.cache_dir; - let install_path = - Path::new(cache_dir).join(format!("{}/INSTALL", minimal_package_meta.name)); - let setts_path = Path::new(cache_dir).join(format!("{}/SETTS", minimal_package_meta.name)); - let build_path = Path::new(cache_dir).join(format!("{}/BUILD", minimal_package_meta.name)); - - if !install_path.exists() { - return Err(io::Error::new( - io::ErrorKind::NotFound, - "File INSTALL not found in cache directory", - )); - } - - let install_content = fs::read_to_string(&install_path)?; - let install_meta: Install = toml::from_str(&install_content) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - - let mut setts_meta: Option<Setts> = None; - let mut build_meta: Option<Build> = None; - - if setts_path.exists() { - let setts_content = fs::read_to_string(&setts_path)?; - setts_meta = Some( - toml::from_str(&setts_content) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, - ); - } - - if build_path.exists() { - let build_content = fs::read_to_string(&build_path)?; - build_meta = Some( - toml::from_str(&build_content) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, - ); - } - - if let Some(ref _script) = install_meta.install.custom_script { - println!( - "Custom script found for package: {}", - install_meta.package.name - ); - } else { - println!( - "No custom script for package: {}", - install_meta.package.name - ); - } - - Ok((install_meta, setts_meta, build_meta)) - } - - /// Checks if the archive contains INSTALL, SETTS and BUILD files. - /// - /// Checks if INSTALL file exists and is not empty. If it does not exist or is empty, returns an error. - /// - /// Checks if SETTS and BUILD files exist and are not empty. If they do not exist or are empty, logs a warning. - /// # Errors - /// * Returns an error if INSTALL file does not exist or is empty. - /// * Returns an error if INSTALL file is empty. - /// - // TODO: Add meta-files validation here. - pub fn check(path_to_archive: String) -> Result<bool, std::io::Error> { - Self::extract_archive(&path_to_archive)?; - - let config = Config::parse().map_err(|e| std::io::Error::other(e.to_string()))?; - - let install_path = Path::new(&config.paths.cache_dir).join("INSTALL"); - let setts_path = Path::new(&config.paths.cache_dir).join("SETTS"); - let build_path = Path::new(&config.paths.cache_dir).join("BUILD"); - - if !install_path.exists() { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - "INSTALL file not found in archive", - )); - } - - let install_content = std::fs::read_to_string(&install_path)?; - if install_content.trim().is_empty() { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "INSTALL file is empty", - )); - } - - if !setts_path.exists() { - log::warn!("SETTS file not found in archive. Make sure you dont need this."); - } else { - let setts_content = std::fs::read_to_string(&setts_path)?; - if setts_content.trim().is_empty() { - log::warn!("SETTS file is empty. Make sure you dont need this."); - } - } - - if !build_path.exists() { - log::warn!("BUILD file not found in archive. Make sure you dont need this."); - } else { - let build_content = std::fs::read_to_string(&build_path)?; - if build_content.trim().is_empty() { - log::warn!("BUILD file is empty. Make sure you dont need this."); - } - } - - let content = std::fs::read_to_string(&install_path).map_err(|e| { - log::warn!("Failed to read file: {}", e); - e - })?; - let install_content: Result<Install, toml::de::Error> = toml::from_str(&content); - - log::info!("Validating arch..."); - let install_parsed = match install_content { - Ok(v) => v, - Err(e) => { - log::error!("Arch mismatch while parsing INSTALL: {}", e); - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - format!("Arch mismatch: {}", e), - )); - } - }; - if std::env::consts::ARCH != install_parsed.package.arch.as_str() { - let pkg_arch = &install_parsed.package.arch; - log::error!( - "Arch mismatch. Package arch: {:?}, Host arch: {}", - pkg_arch, - std::env::consts::ARCH - ); - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Arch mismatch", - )); - } - - Ok(true) - } - - /// Builds the package according to the BUILD file in the archive. - /// - /// There are two strategies for building 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. - pub fn build(&mut self) -> Result<bool, std::io::Error> { - let meta = Self::loadmeta(self)?; - let install_meta = meta.0; - // let setts_meta = meta.1; - let build_meta = meta.2; - - // BUILD NOT EMPTY. SOURCE: -> BUILD -> INSTALL -> SETTS - // BUILD EMPTY. BIN: -> INSTALL -> SETTS - enum Strategies { - Bin, - Source, - } - - let strategy; //default - - if build_meta.is_none() { - log::info!("BUILD file is empty. Skipping build, preparing to install"); - strategy = Strategies::Bin; - } else { - strategy = Strategies::Source; - log::info!("BUILD file is not empty. Skipping install, preparing to build"); - } - - match strategy { - Strategies::Bin => { - if install_meta.install.custom_script.is_none() { - log::info!("Strategy: BIN; No custom script. Running default install hook."); - } else { - log::info!("Strategy: BIN; Running custom script."); - let script = install_meta.install.custom_script.as_ref().unwrap(); - if !script.starts_with("./") { - let _output = std::process::Command::new(script); - } else { - let _output = std::process::Command::new(format!("/bin/sh '{}'", script)); - } - } - } - Strategies::Source => { - log::info!("Strategy: SOURCE; Running default build hook."); - let _ = self.execute_build(&build_meta.unwrap()); - } - } - - Ok(true) - } - - /// 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. - pub 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(); - let _ = self.execute_build(build_meta_ref); - - 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); - if let Some(parent) = dest.parent() { - create_dir_all(parent)?; - } - fs::copy(&src, dest)?; - // TODO: Permission - } - - // Cleanup staging - 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." - ); - 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." - ); - // Опционально: all_files = vec![]; или сканируйте систему (не рекомендуется) - } 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); - - 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(&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 {:?}", - self.name, - manifest_path - ); - Ok(true) - } - - pub fn gen_index() -> Result<bool, std::io::Error> { - todo!(); - } -} diff --git a/src/pkgtoolkit/types.rs b/src/pkgtoolkit/types.rs new file mode 100644 index 0000000..01a807a --- /dev/null +++ b/src/pkgtoolkit/types.rs @@ -0,0 +1,89 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Debug, Deserialize, Clone)] +pub enum Archs { + X86_64, + Aarch64, + X86, + ArmV7, + ArmV8, +} + +#[derive(Serialize, Debug, Deserialize, Clone)] +pub struct Package { + pub name: String, + pub version: String, + pub arch: Archs, + pub descr: Option<String>, + pub license: Option<String>, + pub url: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct InstallMeta { + pub path: String, + pub user: String, + pub group: String, + pub mode: String, + // Cancels the previous fields and installs them using the shell script + pub custom_script: Option<String>, + // pub files: Option<Vec<String>>, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug, Clone)] +pub struct Install { + pub package: Package, + pub install: InstallMeta, + #[serde(default)] + pub files: Vec<String>, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug)] +pub struct Setts { + // Export environment variables if this needed + pub env: Option<String>, + // Test the package after installation + pub test: Option<String>, +} + +#[derive(Deserialize, Serialize, Debug)] +pub enum BuildSystems { + Make, + CMake, + Meson, + Cargo, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PackageManifest { + pub name: String, + pub version: String, + pub all_files: Vec<String>, +} + +#[allow(dead_code)] +#[derive(Deserialize)] +pub struct Build { + pub build_system: BuildSystems, + pub env: Option<String>, + pub script: Option<String>, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Index { + pub packages: Vec<Package>, +} + +impl Archs { + pub fn as_str(&self) -> &'static str { + match self { + Archs::X86_64 => "x86_64", + Archs::Aarch64 => "aarch64", + Archs::X86 => "x86", + Archs::ArmV7 => "armv7", + Archs::ArmV8 => "armv8", + } + } +} |
