Skip to main content

voirs_cli/packaging/
mod.rs

1//! Binary packaging and distribution system for VoiRS CLI.
2//!
3//! This module provides comprehensive packaging functionality for distributing
4//! the VoiRS CLI across multiple package managers and platforms. It includes:
5//!
6//! - Binary optimization and packaging
7//! - Package manager integration (Homebrew, Chocolatey, Scoop, Debian)
8//! - Update management and distribution
9//! - Cross-platform packaging pipeline
10//!
11//! ## Features
12//!
13//! - **Binary Packaging**: Optimized binary generation with compression and validation
14//! - **Multi-Platform Support**: Packages for macOS, Windows, and Linux distributions
15//! - **Package Managers**: Native integration with popular package managers
16//! - **Update System**: Automated update checking and distribution
17//! - **Validation**: Comprehensive package validation and integrity checking
18//!
19//! ## Example
20//!
21//! ```rust,no_run
22//! use voirs_cli::packaging::{PackagingPipeline, PackagingOptions};
23//!
24//! # async fn example() -> anyhow::Result<()> {
25//! let options = PackagingOptions::default();
26//! let pipeline = PackagingPipeline::new(options);
27//! let packages = pipeline.run_full_packaging().await?;
28//! println!("Generated {} packages", packages.len());
29//! # Ok(())
30//! # }
31//! ```
32
33pub mod binary;
34pub mod managers;
35pub mod update;
36
37pub use binary::{BinaryPackager, BinaryPackagingConfig};
38pub use managers::{generate_all_packages, PackageManager, PackageManagerFactory, PackageMetadata};
39pub use update::{UpdateChannel, UpdateConfig, UpdateManager, UpdateState, VersionInfo};
40
41use crate::error::VoirsCLIError;
42use anyhow::Result;
43use std::path::PathBuf;
44use tracing::{error, info};
45
46#[derive(Debug, Clone)]
47pub struct PackagingOptions {
48    pub binary_config: BinaryPackagingConfig,
49    pub package_metadata: PackageMetadata,
50    pub output_directory: PathBuf,
51    pub managers: Vec<String>,
52    pub update_config: UpdateConfig,
53}
54
55impl Default for PackagingOptions {
56    fn default() -> Self {
57        Self {
58            binary_config: BinaryPackagingConfig::default(),
59            package_metadata: PackageMetadata::default(),
60            output_directory: PathBuf::from("packages"),
61            managers: vec![
62                "homebrew".to_string(),
63                "chocolatey".to_string(),
64                "scoop".to_string(),
65                "debian".to_string(),
66            ],
67            update_config: UpdateConfig::default(),
68        }
69    }
70}
71
72pub struct PackagingPipeline {
73    options: PackagingOptions,
74}
75
76impl PackagingPipeline {
77    pub fn new(options: PackagingOptions) -> Self {
78        Self { options }
79    }
80
81    pub async fn run_full_packaging(&self) -> Result<Vec<PathBuf>> {
82        info!("Starting full packaging pipeline");
83
84        let mut package_paths = Vec::new();
85
86        // Step 1: Build optimized binary
87        info!("Step 1: Building optimized binary");
88        let binary_packager = BinaryPackager::new(self.options.binary_config.clone());
89        let binary_path = binary_packager.package_binary()?;
90
91        // Validate the binary
92        if !binary_packager.validate_binary(&binary_path)? {
93            return Err(
94                VoirsCLIError::PackagingError("Binary validation failed".to_string()).into(),
95            );
96        }
97
98        // Get binary size for reporting
99        let binary_size = binary_packager.get_binary_size(&binary_path)?;
100        info!(
101            "Binary size: {} bytes ({:.2} MB)",
102            binary_size,
103            binary_size as f64 / 1_048_576.0
104        );
105
106        // Step 2: Update package metadata with actual binary path
107        let mut metadata = self.options.package_metadata.clone();
108        metadata.binary_path = binary_path;
109
110        // Step 3: Generate packages for all specified managers
111        info!(
112            "Step 2: Generating packages for managers: {:?}",
113            self.options.managers
114        );
115
116        for manager_name in &self.options.managers {
117            match self
118                .generate_package_for_manager(manager_name, &metadata)
119                .await
120            {
121                Ok(path) => {
122                    package_paths.push(path);
123                    info!("Successfully generated {} package", manager_name);
124                }
125                Err(e) => {
126                    error!("Failed to generate {} package: {}", manager_name, e);
127                }
128            }
129        }
130
131        info!(
132            "Packaging pipeline completed. Generated {} packages",
133            package_paths.len()
134        );
135        Ok(package_paths)
136    }
137
138    async fn generate_package_for_manager(
139        &self,
140        manager_name: &str,
141        metadata: &PackageMetadata,
142    ) -> Result<PathBuf> {
143        let manager = PackageManagerFactory::create_manager(manager_name)?;
144        let manager_output_dir = self.options.output_directory.join(manager_name);
145
146        std::fs::create_dir_all(&manager_output_dir)?;
147
148        let package_path = manager.generate_package(metadata, &manager_output_dir)?;
149
150        // Validate the generated package
151        if !manager.validate_package(&package_path)? {
152            return Err(VoirsCLIError::PackagingError(format!(
153                "Package validation failed for {}",
154                manager_name
155            ))
156            .into());
157        }
158
159        Ok(package_path)
160    }
161
162    pub fn get_package_info(&self) -> PackageInfo {
163        PackageInfo {
164            name: self.options.package_metadata.name.clone(),
165            version: self.options.package_metadata.version.clone(),
166            supported_managers: managers::PackageManagerFactory::get_supported_managers()
167                .iter()
168                .map(|&s| s.to_string())
169                .collect(),
170            binary_targets: binary::get_supported_targets()
171                .iter()
172                .map(|&s| s.to_string())
173                .collect(),
174        }
175    }
176}
177
178#[derive(Debug, Clone)]
179pub struct PackageInfo {
180    pub name: String,
181    pub version: String,
182    pub supported_managers: Vec<String>,
183    pub binary_targets: Vec<String>,
184}
185
186pub fn validate_packaging_environment() -> Result<Vec<String>> {
187    info!("Validating packaging environment");
188
189    let mut issues = Vec::new();
190
191    // Check for required tools
192    let required_tools = vec![
193        ("cargo", "Rust package manager"),
194        ("git", "Version control system"),
195    ];
196
197    for (tool, description) in required_tools {
198        if !is_tool_available(tool) {
199            issues.push(format!("Missing required tool: {} ({})", tool, description));
200        }
201    }
202
203    // Check for optional tools
204    let optional_tools = vec![
205        ("strip", "Binary stripping tool"),
206        ("upx", "Binary compression tool"),
207        ("cross", "Cross-compilation tool"),
208    ];
209
210    for (tool, description) in optional_tools {
211        if !is_tool_available(tool) {
212            info!("Optional tool not available: {} ({})", tool, description);
213        }
214    }
215
216    if issues.is_empty() {
217        info!("Packaging environment validation passed");
218    } else {
219        error!(
220            "Packaging environment validation failed with {} issues",
221            issues.len()
222        );
223    }
224
225    Ok(issues)
226}
227
228fn is_tool_available(tool: &str) -> bool {
229    std::process::Command::new(tool)
230        .arg("--version")
231        .output()
232        .map(|output| output.status.success())
233        .unwrap_or(false)
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use tempfile::TempDir;
240
241    #[test]
242    fn test_packaging_options_default() {
243        let options = PackagingOptions::default();
244        assert_eq!(options.package_metadata.name, "voirs");
245        assert!(!options.managers.is_empty());
246        assert_eq!(options.output_directory, PathBuf::from("packages"));
247    }
248
249    #[test]
250    fn test_packaging_pipeline_creation() {
251        let options = PackagingOptions::default();
252        let pipeline = PackagingPipeline::new(options);
253        assert_eq!(pipeline.options.package_metadata.name, "voirs");
254    }
255
256    #[test]
257    fn test_package_info() {
258        let options = PackagingOptions::default();
259        let pipeline = PackagingPipeline::new(options);
260        let info = pipeline.get_package_info();
261
262        assert_eq!(info.name, "voirs");
263        assert!(!info.supported_managers.is_empty());
264        assert!(!info.binary_targets.is_empty());
265    }
266
267    #[test]
268    fn test_validate_packaging_environment() {
269        let issues = validate_packaging_environment().unwrap();
270        // We can't assert specific tools are available in all test environments
271        // but we can verify the function runs without error
272        assert!(issues.is_empty() || !issues.is_empty());
273    }
274
275    #[test]
276    fn test_is_tool_available() {
277        // Test with a tool that should be available on most systems
278        let _result = is_tool_available("echo");
279        // Can't assert true/false as it depends on the system
280        // but we can verify it doesn't panic by simply calling it
281    }
282}