voirs_cli/packaging/
binary.rs1use 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 fs::create_dir_all(&self.config.output_dir)?;
49
50 let binary_path = self.build_optimized_binary()?;
52
53 let optimized_path = self.post_build_optimize(&binary_path)?;
55
56 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 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 if !binary_path.exists() {
185 return Err(
186 VoirsCLIError::PackagingError("Binary file does not exist".to_string()).into(),
187 );
188 }
189
190 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 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}