summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib.rs4
-rw-r--r--src/main.rs20
-rw-r--r--src/net/http_package.rs2
-rw-r--r--src/net/i2p_package.rs3
-rw-r--r--src/pkgtoolkit/archive.rs377
-rw-r--r--src/pkgtoolkit/build.rs349
-rw-r--r--src/pkgtoolkit/index.rs135
-rw-r--r--src/pkgtoolkit/install.rs343
-rw-r--r--src/pkgtoolkit/mod.rs55
-rw-r--r--src/pkgtoolkit/pkgtools.rs615
-rw-r--r--src/pkgtoolkit/types.rs89
11 files changed, 1369 insertions, 623 deletions
diff --git a/src/lib.rs b/src/lib.rs
index b0717c0..beaf75b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -7,6 +7,4 @@ pub mod net {
pub mod i2p_package;
}
-pub mod pkgtoolkit {
- pub mod pkgtools;
-}
+pub mod pkgtoolkit;
diff --git a/src/main.rs b/src/main.rs
index b9328a0..b3367f3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,8 +4,9 @@ mod pkgtoolkit;
use crate::cfg::config::Config;
use crate::net::{http_package::HTTPPackage, i2p_package::I2PPackage};
-#[allow(unused_imports)]
-use crate::pkgtoolkit::pkgtools::Package;
+
+use crate::pkgtoolkit::Package;
+use crate::pkgtoolkit::index::IndexOperations;
use clap::{Args, Parser, Subcommand};
use std::fs::File;
@@ -49,6 +50,8 @@ enum Commands {
},
#[command(about = "Maintaners, links, developers and more info")]
Credits,
+ #[command(about = "Generate index for repository path")]
+ GenIndex { path: String },
}
#[derive(Args, Clone)]
@@ -207,6 +210,19 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!(" ");
println!("The Anthrill project repos: https://codeberg.org/NamelessTeam ");
}
+ Commands::GenIndex { path } => {
+ println!("Generating index for {}", path);
+ match Package::gen_index(path) {
+ Ok(_) => {
+ println!("Index generated successfully.");
+ }
+ Err(e) => {
+ log::error!("Failed to generate index: {}", e);
+ return Err(Box::new(e));
+ }
+ }
+ return Ok(());
+ }
}
Ok(())
diff --git a/src/net/http_package.rs b/src/net/http_package.rs
index 8fb2e19..d31cc90 100644
--- a/src/net/http_package.rs
+++ b/src/net/http_package.rs
@@ -1,7 +1,7 @@
// TODO: Add signatures checking and fix mixed sync/async use.
use crate::cfg::config::Config;
-use crate::pkgtoolkit::pkgtools::Package;
+use crate::pkgtoolkit::Package;
use flate2::read::GzDecoder;
use futures_util::stream::TryStreamExt;
use indicatif::{ProgressBar, ProgressStyle};
diff --git a/src/net/i2p_package.rs b/src/net/i2p_package.rs
index f46ddc7..a9d39d2 100644
--- a/src/net/i2p_package.rs
+++ b/src/net/i2p_package.rs
@@ -1,5 +1,6 @@
use crate::cfg::config::Config;
-use crate::pkgtoolkit::pkgtools::Package;
+use crate::pkgtoolkit::Package;
+use crate::pkgtoolkit::archive::ArchiveOperations;
use serde::Deserialize;
use tokio;
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",
+ }
+ }
+}