use crate::cfg::config::Config; use std::{ fs::create_dir_all, path::{Path, PathBuf}, process::Command, }; use glob::glob; 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; fn find_makefile( &self, build_meta: &Build, search_dir: &Path, ) -> std::io::Result>; fn run_command(cmd: std::process::Command, context: &str) -> Result<(), std::io::Error>; fn run_build_system( source_dir: &Path, required_file: &str, configure_cmd: &[&str], build_cmd: &[&str], work_dir: &Path, envs: &[(String, String)], context: &str, ) -> Result<(), std::io::Error>; } #[allow(dead_code)] impl BuildOperations for Package { /// Runs a command and checks if it was successful. /// If the command fails, it returns an error with the command's /// stdout and stderr. /// /// # Arguments /// /// * `cmd`: The command to run. /// * `context`: A string to prefix the error message with if the command fails. fn run_command(mut cmd: std::process::Command, context: &str) -> Result<(), std::io::Error> { let output = cmd .output() .map_err(|e| std::io::Error::other(format!("{} failed: {}", context, 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!( "{} failed:\nStdout: {}\nStderr: {}", context, stdout, stderr ))); } Ok(()) } /// Runs a build system given the following parameters: /// /// `source_dir`: The directory containing the source code /// `required_file`: The file that must exist in the source directory /// `configure_cmd`: The command to run for configure step /// `build_cmd`: The command to run for build step /// `work_dir`: The directory where the build process will take place /// `envs`: A list of environment variables to set during the build process /// `context`: A string context to provide for error messages fn run_build_system( source_dir: &Path, required_file: &str, configure_cmd: &[&str], build_cmd: &[&str], work_dir: &Path, envs: &[(String, String)], context: &str, ) -> Result<(), std::io::Error> { let required_path = source_dir.join(required_file); if !required_path.exists() { return Err(std::io::Error::other(format!( "{} file not found: {}", context, required_path.display() ))); } if !work_dir.exists() { create_dir_all(work_dir)?; } // Configure step let mut config_cmd = std::process::Command::new(configure_cmd[0]); config_cmd.args(&configure_cmd[1..]).current_dir(source_dir); for (key, value) in envs { config_cmd.env(key, value); } log::info!("Running {} configuration: {:?}", context, config_cmd); Self::run_command(config_cmd, &format!("{} config", context))?; // Build step let mut build_cmd_inner = std::process::Command::new(build_cmd[0]); build_cmd_inner.args(&build_cmd[1..]).current_dir(work_dir); for (key, value) in envs { build_cmd_inner.env(key, value); } log::info!("Running {} build: {:?}", context, build_cmd_inner); Self::run_command(build_cmd_inner, &format!("{} build", context))?; Ok(()) } /// Finds the build system file (e.g. Makefile, meson.build, CMakeLists.txt, Cargo.toml) /// based on the build system specified in the build metadata. /// /// # Arguments /// /// * `build_meta`: The build metadata containing the build system type. /// * `search_dir`: The directory to search in for the build system file. /// /// # Returns /// /// A `Result` containing an `Option` which is `Some` if the build /// system file is found, and `None` otherwise. fn find_makefile( &self, build_meta: &Build, search_dir: &Path, ) -> std::io::Result> { let (patterns, recursive) = match build_meta.build_system { BuildSystems::Make => (vec!["Makefile", "makefile", "GNUmakefile"], false), BuildSystems::Meson => (vec!["meson.build", "meson_options.txt"], true), BuildSystems::CMake => (vec!["CMakeLists.txt"], true), BuildSystems::Cargo => (vec!["Cargo.toml"], false), }; for pattern in &patterns { let glob_pattern = if recursive { search_dir .join("**") .join(pattern) .to_string_lossy() .into_owned() } else { search_dir.join(pattern).to_string_lossy().into_owned() }; let entries = glob(&glob_pattern) .map_err(|e| std::io::Error::other(format!("Invalid glob pattern: {}", e)))?; for entry in entries { let path = entry.map_err(|e| std::io::Error::other(format!("Glob error: {}", e)))?; return Ok(Some(path)); } } Ok(None) } /// 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)); if !build_dir.exists() { return Err(std::io::Error::other(format!( "Build directory not found: {}", build_dir.display() ))); } 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())); } } } 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.starts_with('/') { 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()); let _ = inner_cmd.output(); inner_cmd } else { let mut inner_cmd = Command::new("/bin/sh"); inner_cmd.arg("-c"); inner_cmd.arg(script); let _ = inner_cmd.output(); 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 => { let found = self .find_makefile(&build_meta, &build_dir) .map_err(|e| { std::io::Error::other(format!("Failed to search for Makefile: {}", e)) })? .ok_or_else(|| std::io::Error::other("Makefile not found"))?; if !found.exists() { return Err(std::io::Error::other(format!( "Makefile not found: {}", found.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); Self::run_command(cmd, "Make build")?; } BuildSystems::CMake => { let found = self .find_makefile(&build_meta, &build_dir) .map_err(|e| { std::io::Error::other(format!("Failed to search for CMakeLists: {}", e)) })? .ok_or_else(|| std::io::Error::other("Makefile not found"))?; if !found.exists() { return Err(std::io::Error::other(format!( "Makefile not found: {}", found.display() ))); } Self::run_build_system( &build_dir, "CMakeLists.txt", &["cmake", "-S", ".", "-B", "build"], &["make", "-j", &num_cpus::get().to_string()], &build_dir.join("build"), &cmd_envs, "CMake", )?; } BuildSystems::Meson => { Self::run_build_system( &build_dir, "meson.build", &["meson", "setup", "build"], &["ninja", "-j", &num_cpus::get().to_string()], &build_dir.join("build"), &cmd_envs, "Meson", )?; } BuildSystems::Cargo => { Self::run_build_system( &build_dir, "Cargo.toml", &["cargo", "build", "--release"], &[], &build_dir, &cmd_envs, "Cargo", )?; } } 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 { 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 self.arch.as_str().to_lowercase() != install_meta.package.arch.as_str().to_lowercase() { return Err(std::io::Error::other(format!( "Package arch mismatch. Expected: {}, Actual: {}", install_meta.package.arch.as_str(), self.arch.as_str().to_lowercase() ))); } 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) } }