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, pub license: Option, 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, // pub files: Option>, } #[allow(dead_code)] #[derive(Deserialize, Debug, Clone)] struct Install { package: Package, install: InstallMeta, #[serde(default)] files: Vec, } #[allow(dead_code)] #[derive(Deserialize, Debug)] struct Setts { // Export environment variables if this needed env: Option, // Test the package after installation test: Option, } #[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, } #[allow(dead_code)] #[derive(Deserialize)] struct Build { build_system: BuildSystems, env: Option, script: Option, } 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, 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, Option) /// /// 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, Option), 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 = None; let mut build_meta: Option = 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 { 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 = 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 { 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 { 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(); 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 { todo!(); } }