Skip to main content

voirs_cli/packaging/
binary.rs

1use crate::error::VoirsCLIError;
2use anyhow::Result;
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::PathBuf;
6use std::process::Command;
7use tracing::{debug, info, warn};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct BinaryPackagingConfig {
11    pub target_triple: String,
12    pub output_dir: PathBuf,
13    pub static_linking: bool,
14    pub optimize_size: bool,
15    pub strip_debug: bool,
16    pub compress_binary: bool,
17    pub cross_compile: bool,
18}
19
20impl Default for BinaryPackagingConfig {
21    fn default() -> Self {
22        Self {
23            target_triple: get_default_target_triple(),
24            output_dir: PathBuf::from("target/release"),
25            static_linking: true,
26            optimize_size: true,
27            strip_debug: true,
28            compress_binary: false,
29            cross_compile: false,
30        }
31    }
32}
33
34#[derive(Debug, Clone)]
35pub struct BinaryPackager {
36    config: BinaryPackagingConfig,
37}
38
39impl BinaryPackager {
40    pub fn new(config: BinaryPackagingConfig) -> Self {
41        Self { config }
42    }
43
44    pub fn package_binary(&self) -> Result<PathBuf> {
45        info!("Starting binary packaging process");
46
47        // Ensure output directory exists
48        fs::create_dir_all(&self.config.output_dir)?;
49
50        // Build the binary with optimization flags
51        let binary_path = self.build_optimized_binary()?;
52
53        // Apply post-build optimizations
54        let optimized_path = self.post_build_optimize(&binary_path)?;
55
56        // Optional compression
57        let final_path = if self.config.compress_binary {
58            self.compress_binary(&optimized_path)?
59        } else {
60            optimized_path
61        };
62
63        info!("Binary packaging completed: {:?}", final_path);
64        Ok(final_path)
65    }
66
67    fn build_optimized_binary(&self) -> Result<PathBuf> {
68        info!(
69            "Building optimized binary for target: {}",
70            self.config.target_triple
71        );
72
73        let mut cmd = Command::new("cargo");
74        cmd.arg("build").arg("--release").arg("--bin").arg("voirs");
75
76        if self.config.cross_compile {
77            cmd.arg("--target").arg(&self.config.target_triple);
78        }
79
80        // Add optimization flags
81        if self.config.optimize_size {
82            cmd.env("CARGO_PROFILE_RELEASE_OPT_LEVEL", "z");
83            cmd.env("CARGO_PROFILE_RELEASE_LTO", "true");
84            cmd.env("CARGO_PROFILE_RELEASE_CODEGEN_UNITS", "1");
85            cmd.env("CARGO_PROFILE_RELEASE_PANIC", "abort");
86        }
87
88        if self.config.static_linking {
89            cmd.env(
90                "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS",
91                "-C target-feature=+crt-static",
92            );
93        }
94
95        let output = cmd.output()?;
96
97        if !output.status.success() {
98            return Err(VoirsCLIError::PackagingError(format!(
99                "Failed to build binary: {}",
100                String::from_utf8_lossy(&output.stderr)
101            ))
102            .into());
103        }
104
105        let binary_name = get_binary_name(&self.config.target_triple);
106        let binary_path = if self.config.cross_compile {
107            self.config
108                .output_dir
109                .join(&self.config.target_triple)
110                .join(&binary_name)
111        } else {
112            self.config.output_dir.join(&binary_name)
113        };
114
115        debug!("Binary built at: {:?}", binary_path);
116        Ok(binary_path)
117    }
118
119    fn post_build_optimize(&self, binary_path: &PathBuf) -> Result<PathBuf> {
120        if self.config.strip_debug {
121            info!("Stripping debug symbols from binary");
122            self.strip_debug_symbols(binary_path)?;
123        }
124
125        Ok(binary_path.clone())
126    }
127
128    fn strip_debug_symbols(&self, binary_path: &PathBuf) -> Result<()> {
129        let strip_cmd = "strip";
130
131        let output = Command::new(strip_cmd).arg(binary_path).output()?;
132
133        if !output.status.success() {
134            warn!(
135                "Failed to strip debug symbols: {}",
136                String::from_utf8_lossy(&output.stderr)
137            );
138        }
139
140        Ok(())
141    }
142
143    fn compress_binary(&self, binary_path: &PathBuf) -> Result<PathBuf> {
144        info!("Compressing binary using UPX");
145
146        let compressed_path = binary_path.with_extension("compressed");
147
148        let output = Command::new("upx")
149            .arg("--best")
150            .arg("--lzma")
151            .arg("-o")
152            .arg(&compressed_path)
153            .arg(binary_path)
154            .output();
155
156        match output {
157            Ok(output) if output.status.success() => {
158                info!("Binary compressed successfully");
159                Ok(compressed_path)
160            }
161            Ok(output) => {
162                warn!(
163                    "UPX compression failed: {}",
164                    String::from_utf8_lossy(&output.stderr)
165                );
166                Ok(binary_path.clone())
167            }
168            Err(e) => {
169                warn!("UPX not available: {}", e);
170                Ok(binary_path.clone())
171            }
172        }
173    }
174
175    pub fn get_binary_size(&self, binary_path: &PathBuf) -> Result<u64> {
176        let metadata = fs::metadata(binary_path)?;
177        Ok(metadata.len())
178    }
179
180    pub fn validate_binary(&self, binary_path: &PathBuf) -> Result<bool> {
181        debug!("Validating binary at {:?}", binary_path);
182
183        // Check if file exists and is executable
184        if !binary_path.exists() {
185            return Err(
186                VoirsCLIError::PackagingError("Binary file does not exist".to_string()).into(),
187            );
188        }
189
190        // Try to execute the binary with --version flag
191        let output = Command::new(binary_path).arg("--version").output()?;
192
193        if output.status.success() {
194            let version_output = String::from_utf8_lossy(&output.stdout);
195            info!(
196                "Binary validation successful. Version: {}",
197                version_output.trim()
198            );
199            Ok(true)
200        } else {
201            Err(VoirsCLIError::PackagingError(
202                "Binary validation failed - unable to execute".to_string(),
203            )
204            .into())
205        }
206    }
207}
208
209fn get_default_target_triple() -> String {
210    std::env::var("TARGET").unwrap_or_else(|_| {
211        if cfg!(target_os = "windows") {
212            "x86_64-pc-windows-msvc".to_string()
213        } else if cfg!(target_os = "macos") {
214            "x86_64-apple-darwin".to_string()
215        } else {
216            "x86_64-unknown-linux-gnu".to_string()
217        }
218    })
219}
220
221fn get_binary_name(target_triple: &str) -> String {
222    if target_triple.contains("windows") {
223        "voirs.exe".to_string()
224    } else {
225        "voirs".to_string()
226    }
227}
228
229pub fn get_supported_targets() -> Vec<&'static str> {
230    vec![
231        "x86_64-unknown-linux-gnu",
232        "x86_64-unknown-linux-musl",
233        "x86_64-pc-windows-msvc",
234        "x86_64-apple-darwin",
235        "aarch64-apple-darwin",
236        "aarch64-unknown-linux-gnu",
237        "armv7-unknown-linux-gnueabihf",
238    ]
239}
240
241pub fn setup_cross_compilation() -> Result<()> {
242    info!("Setting up cross-compilation environment");
243
244    // Check if cross is installed
245    let cross_check = Command::new("cross").arg("--version").output();
246
247    if cross_check.is_err() {
248        info!("Installing cross for cross-compilation");
249        let install_output = Command::new("cargo").arg("install").arg("cross").output()?;
250
251        if !install_output.status.success() {
252            return Err(
253                VoirsCLIError::PackagingError("Failed to install cross tool".to_string()).into(),
254            );
255        }
256    }
257
258    Ok(())
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use tempfile::TempDir;
265
266    #[test]
267    fn test_binary_packaging_config_default() {
268        let config = BinaryPackagingConfig::default();
269        assert!(!config.target_triple.is_empty());
270        assert!(config.static_linking);
271        assert!(config.optimize_size);
272        assert!(config.strip_debug);
273    }
274
275    #[test]
276    fn test_get_binary_name() {
277        assert_eq!(get_binary_name("x86_64-pc-windows-msvc"), "voirs.exe");
278        assert_eq!(get_binary_name("x86_64-unknown-linux-gnu"), "voirs");
279        assert_eq!(get_binary_name("x86_64-apple-darwin"), "voirs");
280    }
281
282    #[test]
283    fn test_supported_targets() {
284        let targets = get_supported_targets();
285        assert!(targets.contains(&"x86_64-unknown-linux-gnu"));
286        assert!(targets.contains(&"x86_64-pc-windows-msvc"));
287        assert!(targets.contains(&"x86_64-apple-darwin"));
288    }
289
290    #[test]
291    fn test_binary_packager_creation() {
292        let config = BinaryPackagingConfig::default();
293        let packager = BinaryPackager::new(config.clone());
294        assert_eq!(packager.config.target_triple, config.target_triple);
295    }
296
297    #[test]
298    fn test_get_default_target_triple() {
299        let target = get_default_target_triple();
300        assert!(!target.is_empty());
301        assert!(target.contains("x86_64") || target.contains("aarch64"));
302    }
303}