use crate::error::VoirsCLIError;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BinaryPackagingConfig {
pub target_triple: String,
pub output_dir: PathBuf,
pub static_linking: bool,
pub optimize_size: bool,
pub strip_debug: bool,
pub compress_binary: bool,
pub cross_compile: bool,
}
impl Default for BinaryPackagingConfig {
fn default() -> Self {
Self {
target_triple: get_default_target_triple(),
output_dir: PathBuf::from("target/release"),
static_linking: true,
optimize_size: true,
strip_debug: true,
compress_binary: false,
cross_compile: false,
}
}
}
#[derive(Debug, Clone)]
pub struct BinaryPackager {
config: BinaryPackagingConfig,
}
impl BinaryPackager {
pub fn new(config: BinaryPackagingConfig) -> Self {
Self { config }
}
pub fn package_binary(&self) -> Result<PathBuf> {
info!("Starting binary packaging process");
fs::create_dir_all(&self.config.output_dir)?;
let binary_path = self.build_optimized_binary()?;
let optimized_path = self.post_build_optimize(&binary_path)?;
let final_path = if self.config.compress_binary {
self.compress_binary(&optimized_path)?
} else {
optimized_path
};
info!("Binary packaging completed: {:?}", final_path);
Ok(final_path)
}
fn build_optimized_binary(&self) -> Result<PathBuf> {
info!(
"Building optimized binary for target: {}",
self.config.target_triple
);
let mut cmd = Command::new("cargo");
cmd.arg("build").arg("--release").arg("--bin").arg("voirs");
if self.config.cross_compile {
cmd.arg("--target").arg(&self.config.target_triple);
}
if self.config.optimize_size {
cmd.env("CARGO_PROFILE_RELEASE_OPT_LEVEL", "z");
cmd.env("CARGO_PROFILE_RELEASE_LTO", "true");
cmd.env("CARGO_PROFILE_RELEASE_CODEGEN_UNITS", "1");
cmd.env("CARGO_PROFILE_RELEASE_PANIC", "abort");
}
if self.config.static_linking {
cmd.env(
"CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS",
"-C target-feature=+crt-static",
);
}
let output = cmd.output()?;
if !output.status.success() {
return Err(VoirsCLIError::PackagingError(format!(
"Failed to build binary: {}",
String::from_utf8_lossy(&output.stderr)
))
.into());
}
let binary_name = get_binary_name(&self.config.target_triple);
let binary_path = if self.config.cross_compile {
self.config
.output_dir
.join(&self.config.target_triple)
.join(&binary_name)
} else {
self.config.output_dir.join(&binary_name)
};
debug!("Binary built at: {:?}", binary_path);
Ok(binary_path)
}
fn post_build_optimize(&self, binary_path: &PathBuf) -> Result<PathBuf> {
if self.config.strip_debug {
info!("Stripping debug symbols from binary");
self.strip_debug_symbols(binary_path)?;
}
Ok(binary_path.clone())
}
fn strip_debug_symbols(&self, binary_path: &PathBuf) -> Result<()> {
let strip_cmd = "strip";
let output = Command::new(strip_cmd).arg(binary_path).output()?;
if !output.status.success() {
warn!(
"Failed to strip debug symbols: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Ok(())
}
fn compress_binary(&self, binary_path: &PathBuf) -> Result<PathBuf> {
info!("Compressing binary using UPX");
let compressed_path = binary_path.with_extension("compressed");
let output = Command::new("upx")
.arg("--best")
.arg("--lzma")
.arg("-o")
.arg(&compressed_path)
.arg(binary_path)
.output();
match output {
Ok(output) if output.status.success() => {
info!("Binary compressed successfully");
Ok(compressed_path)
}
Ok(output) => {
warn!(
"UPX compression failed: {}",
String::from_utf8_lossy(&output.stderr)
);
Ok(binary_path.clone())
}
Err(e) => {
warn!("UPX not available: {}", e);
Ok(binary_path.clone())
}
}
}
pub fn get_binary_size(&self, binary_path: &PathBuf) -> Result<u64> {
let metadata = fs::metadata(binary_path)?;
Ok(metadata.len())
}
pub fn validate_binary(&self, binary_path: &PathBuf) -> Result<bool> {
debug!("Validating binary at {:?}", binary_path);
if !binary_path.exists() {
return Err(
VoirsCLIError::PackagingError("Binary file does not exist".to_string()).into(),
);
}
let output = Command::new(binary_path).arg("--version").output()?;
if output.status.success() {
let version_output = String::from_utf8_lossy(&output.stdout);
info!(
"Binary validation successful. Version: {}",
version_output.trim()
);
Ok(true)
} else {
Err(VoirsCLIError::PackagingError(
"Binary validation failed - unable to execute".to_string(),
)
.into())
}
}
}
fn get_default_target_triple() -> String {
std::env::var("TARGET").unwrap_or_else(|_| {
if cfg!(target_os = "windows") {
"x86_64-pc-windows-msvc".to_string()
} else if cfg!(target_os = "macos") {
"x86_64-apple-darwin".to_string()
} else {
"x86_64-unknown-linux-gnu".to_string()
}
})
}
fn get_binary_name(target_triple: &str) -> String {
if target_triple.contains("windows") {
"voirs.exe".to_string()
} else {
"voirs".to_string()
}
}
pub fn get_supported_targets() -> Vec<&'static str> {
vec![
"x86_64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
"x86_64-pc-windows-msvc",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"aarch64-unknown-linux-gnu",
"armv7-unknown-linux-gnueabihf",
]
}
pub fn setup_cross_compilation() -> Result<()> {
info!("Setting up cross-compilation environment");
let cross_check = Command::new("cross").arg("--version").output();
if cross_check.is_err() {
info!("Installing cross for cross-compilation");
let install_output = Command::new("cargo").arg("install").arg("cross").output()?;
if !install_output.status.success() {
return Err(
VoirsCLIError::PackagingError("Failed to install cross tool".to_string()).into(),
);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_binary_packaging_config_default() {
let config = BinaryPackagingConfig::default();
assert!(!config.target_triple.is_empty());
assert!(config.static_linking);
assert!(config.optimize_size);
assert!(config.strip_debug);
}
#[test]
fn test_get_binary_name() {
assert_eq!(get_binary_name("x86_64-pc-windows-msvc"), "voirs.exe");
assert_eq!(get_binary_name("x86_64-unknown-linux-gnu"), "voirs");
assert_eq!(get_binary_name("x86_64-apple-darwin"), "voirs");
}
#[test]
fn test_supported_targets() {
let targets = get_supported_targets();
assert!(targets.contains(&"x86_64-unknown-linux-gnu"));
assert!(targets.contains(&"x86_64-pc-windows-msvc"));
assert!(targets.contains(&"x86_64-apple-darwin"));
}
#[test]
fn test_binary_packager_creation() {
let config = BinaryPackagingConfig::default();
let packager = BinaryPackager::new(config.clone());
assert_eq!(packager.config.target_triple, config.target_triple);
}
#[test]
fn test_get_default_target_triple() {
let target = get_default_target_triple();
assert!(!target.is_empty());
assert!(target.contains("x86_64") || target.contains("aarch64"));
}
}