mecha10_cli/services/package/
mod.rs

1//! Package service for creating deployment packages
2//!
3//! This service provides operations for building deployment packages containing
4//! binaries, configurations, and assets for distribution to target systems.
5
6#![allow(dead_code)]
7
8mod collector;
9mod packager;
10mod types;
11mod utils;
12
13pub use types::{AssetInfo, BinaryInfo, ConfigInfo, PackageConfig, PackageManifest};
14
15use anyhow::{Context, Result};
16use chrono::Utc;
17use indicatif::{ProgressBar, ProgressStyle};
18use std::collections::HashMap;
19use std::path::PathBuf;
20use std::process::Command;
21use std::time::Duration;
22
23/// Package service for creating deployment packages
24///
25/// # Examples
26///
27/// ```rust,ignore
28/// use mecha10_cli::services::{PackageService, PackageConfig, TargetArch};
29/// use std::path::Path;
30///
31/// # fn example() -> anyhow::Result<()> {
32/// let service = PackageService::new(
33///     "my-robot".to_string(),
34///     "1.0.0".to_string(),
35///     Path::new("/path/to/project")
36/// )?;
37///
38/// // Build package with default config
39/// let package_path = service.build(PackageConfig::default())?;
40/// println!("Package created: {}", package_path.display());
41///
42/// // Build for specific target
43/// let mut config = PackageConfig::default();
44/// config.target_arch = TargetArch::Aarch64UnknownLinuxGnu;
45/// let arm_package = service.build(config)?;
46/// # Ok(())
47/// # }
48/// ```
49pub struct PackageService {
50    project_name: String,
51    project_version: String,
52    project_root: PathBuf,
53}
54
55impl PackageService {
56    /// Create a new package service
57    ///
58    /// # Arguments
59    ///
60    /// * `project_name` - Name of the project
61    /// * `project_version` - Version string
62    /// * `project_root` - Root directory of the project
63    pub fn new(project_name: String, project_version: String, project_root: impl Into<PathBuf>) -> Result<Self> {
64        let project_root = project_root.into();
65        if !project_root.exists() {
66            return Err(anyhow::anyhow!(
67                "Project root does not exist: {}",
68                project_root.display()
69            ));
70        }
71
72        Ok(Self {
73            project_name,
74            project_version,
75            project_root,
76        })
77    }
78
79    /// Build a deployment package
80    ///
81    /// # Arguments
82    ///
83    /// * `config` - Package configuration
84    pub fn build(&self, config: PackageConfig) -> Result<PathBuf> {
85        // Build binaries
86        self.build_binaries(&config)?;
87
88        // Collect package contents
89        let binaries = self.collect_binaries(&config)?;
90        let configs = self.collect_configs()?;
91        let assets = if config.include_assets {
92            self.collect_assets()?
93        } else {
94            Vec::new()
95        };
96
97        // Create manifest
98        let manifest = self.create_manifest(&config, &binaries, &configs, &assets)?;
99
100        // Validate package
101        self.validate_package(&manifest)?;
102
103        // Create package archive
104        let package_path = self.create_package(&config, &manifest, &binaries, &assets)?;
105
106        Ok(package_path)
107    }
108
109    /// Build binaries for target architecture
110    fn build_binaries(&self, config: &PackageConfig) -> Result<()> {
111        let spinner = ProgressBar::new_spinner();
112        spinner.set_style(
113            ProgressStyle::default_spinner()
114                .template("   {spinner:.green} {msg}")
115                .unwrap(),
116        );
117        spinner.set_message(format!("Building for {}...", config.target_arch.as_str()));
118        spinner.enable_steady_tick(Duration::from_millis(100));
119
120        let mut cmd = Command::new("cargo");
121        cmd.arg("build").arg("--workspace").current_dir(&self.project_root);
122
123        if config.build_profile == "release" {
124            cmd.arg("--release");
125        }
126
127        cmd.arg("--target").arg(config.target_arch.as_str());
128
129        let status = cmd.status().context("Failed to run cargo build")?;
130
131        spinner.finish_and_clear();
132
133        if !status.success() {
134            return Err(anyhow::anyhow!(
135                "Build failed for target {}",
136                config.target_arch.as_str()
137            ));
138        }
139
140        Ok(())
141    }
142
143    /// Collect built binaries
144    fn collect_binaries(&self, config: &PackageConfig) -> Result<Vec<BinaryInfo>> {
145        collector::collect_binaries(&self.project_root, config)
146    }
147
148    /// Collect configuration files
149    fn collect_configs(&self) -> Result<Vec<ConfigInfo>> {
150        collector::collect_configs(&self.project_root)
151    }
152
153    /// Collect asset files
154    fn collect_assets(&self) -> Result<Vec<AssetInfo>> {
155        collector::collect_assets(&self.project_root)
156    }
157
158    /// Create package manifest
159    fn create_manifest(
160        &self,
161        config: &PackageConfig,
162        binaries: &[BinaryInfo],
163        configs: &[ConfigInfo],
164        assets: &[AssetInfo],
165    ) -> Result<PackageManifest> {
166        Ok(PackageManifest {
167            format_version: PackageManifest::FORMAT_VERSION.to_string(),
168            name: self.project_name.clone(),
169            version: self.project_version.clone(),
170            build_timestamp: Utc::now(),
171            target_arch: config.target_arch,
172            binaries: binaries.to_vec(),
173            configs: configs.to_vec(),
174            assets: assets.to_vec(),
175            dependencies: self.collect_dependencies()?,
176            metadata: config.custom_metadata.clone(),
177            build_profile: config.build_profile.clone(),
178            git_commit: self.get_git_commit(),
179            environment: config.environment.clone(),
180        })
181    }
182
183    /// Validate package contents
184    fn validate_package(&self, manifest: &PackageManifest) -> Result<()> {
185        if manifest.format_version != PackageManifest::FORMAT_VERSION {
186            return Err(anyhow::anyhow!(
187                "Invalid format version: {} (expected {})",
188                manifest.format_version,
189                PackageManifest::FORMAT_VERSION
190            ));
191        }
192
193        if manifest.binaries.is_empty() {
194            return Err(anyhow::anyhow!("Package must contain at least one binary"));
195        }
196
197        for binary in &manifest.binaries {
198            if binary.name.is_empty() {
199                return Err(anyhow::anyhow!("Binary name cannot be empty"));
200            }
201            if binary.size_bytes == 0 {
202                return Err(anyhow::anyhow!("Binary {} has zero size", binary.name));
203            }
204        }
205
206        for config in &manifest.configs {
207            if config.name.is_empty() {
208                return Err(anyhow::anyhow!("Config name cannot be empty"));
209            }
210        }
211
212        Ok(())
213    }
214
215    /// Create package archive
216    fn create_package(
217        &self,
218        config: &PackageConfig,
219        manifest: &PackageManifest,
220        binaries: &[BinaryInfo],
221        assets: &[AssetInfo],
222    ) -> Result<PathBuf> {
223        packager::create_package(
224            &self.project_root,
225            &self.project_name,
226            &self.project_version,
227            config,
228            manifest,
229            binaries,
230            assets,
231        )
232    }
233
234    /// Collect project dependencies from Cargo.lock
235    fn collect_dependencies(&self) -> Result<HashMap<String, String>> {
236        utils::collect_dependencies(&self.project_root)
237    }
238
239    /// Get git commit hash
240    fn get_git_commit(&self) -> Option<String> {
241        utils::get_git_commit(&self.project_root)
242    }
243}