Skip to main content

mecha10_deploy/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        self.build_binaries(&config)?;
86
87        let binaries = self.collect_binaries(&config)?;
88        let configs = self.collect_configs()?;
89        let assets = if config.include_assets {
90            self.collect_assets()?
91        } else {
92            Vec::new()
93        };
94
95        let manifest = self.create_manifest(&config, &binaries, &configs, &assets)?;
96
97        self.validate_package(&manifest)?;
98
99        let package_path = self.create_package(&config, &manifest, &binaries, &assets)?;
100
101        Ok(package_path)
102    }
103
104    /// Build binaries for target architecture
105    fn build_binaries(&self, config: &PackageConfig) -> Result<()> {
106        let spinner = ProgressBar::new_spinner();
107        spinner.set_style(
108            ProgressStyle::default_spinner()
109                .template("   {spinner:.green} {msg}")
110                .unwrap(),
111        );
112        spinner.set_message(format!("Building for {}...", config.target_arch.as_str()));
113        spinner.enable_steady_tick(Duration::from_millis(100));
114
115        let mut cmd = Command::new("cargo");
116        cmd.arg("build").arg("--workspace").current_dir(&self.project_root);
117
118        if config.build_profile == "release" {
119            cmd.arg("--release");
120        }
121
122        cmd.arg("--target").arg(config.target_arch.as_str());
123
124        let status = cmd.status().context("Failed to run cargo build")?;
125
126        spinner.finish_and_clear();
127
128        if !status.success() {
129            return Err(anyhow::anyhow!(
130                "Build failed for target {}",
131                config.target_arch.as_str()
132            ));
133        }
134
135        Ok(())
136    }
137
138    /// Collect built binaries
139    fn collect_binaries(&self, config: &PackageConfig) -> Result<Vec<BinaryInfo>> {
140        collector::collect_binaries(&self.project_root, config)
141    }
142
143    /// Collect configuration files
144    fn collect_configs(&self) -> Result<Vec<ConfigInfo>> {
145        collector::collect_configs(&self.project_root)
146    }
147
148    /// Collect asset files
149    fn collect_assets(&self) -> Result<Vec<AssetInfo>> {
150        collector::collect_assets(&self.project_root)
151    }
152
153    /// Create package manifest
154    fn create_manifest(
155        &self,
156        config: &PackageConfig,
157        binaries: &[BinaryInfo],
158        configs: &[ConfigInfo],
159        assets: &[AssetInfo],
160    ) -> Result<PackageManifest> {
161        Ok(PackageManifest {
162            format_version: PackageManifest::FORMAT_VERSION.to_string(),
163            name: self.project_name.clone(),
164            version: self.project_version.clone(),
165            build_timestamp: Utc::now(),
166            target_arch: config.target_arch,
167            binaries: binaries.to_vec(),
168            configs: configs.to_vec(),
169            assets: assets.to_vec(),
170            dependencies: self.collect_dependencies()?,
171            metadata: config.custom_metadata.clone(),
172            build_profile: config.build_profile.clone(),
173            git_commit: self.get_git_commit(),
174            environment: config.environment.clone(),
175        })
176    }
177
178    /// Validate package contents
179    fn validate_package(&self, manifest: &PackageManifest) -> Result<()> {
180        if manifest.format_version != PackageManifest::FORMAT_VERSION {
181            return Err(anyhow::anyhow!(
182                "Invalid format version: {} (expected {})",
183                manifest.format_version,
184                PackageManifest::FORMAT_VERSION
185            ));
186        }
187
188        if manifest.binaries.is_empty() {
189            return Err(anyhow::anyhow!("Package must contain at least one binary"));
190        }
191
192        for binary in &manifest.binaries {
193            if binary.name.is_empty() {
194                return Err(anyhow::anyhow!("Binary name cannot be empty"));
195            }
196            if binary.size_bytes == 0 {
197                return Err(anyhow::anyhow!("Binary {} has zero size", binary.name));
198            }
199        }
200
201        for config in &manifest.configs {
202            if config.name.is_empty() {
203                return Err(anyhow::anyhow!("Config name cannot be empty"));
204            }
205        }
206
207        Ok(())
208    }
209
210    /// Create package archive
211    fn create_package(
212        &self,
213        config: &PackageConfig,
214        manifest: &PackageManifest,
215        binaries: &[BinaryInfo],
216        assets: &[AssetInfo],
217    ) -> Result<PathBuf> {
218        packager::create_package(
219            &self.project_root,
220            &self.project_name,
221            &self.project_version,
222            config,
223            manifest,
224            binaries,
225            assets,
226        )
227    }
228
229    /// Collect project dependencies from Cargo.lock
230    fn collect_dependencies(&self) -> Result<HashMap<String, String>> {
231        utils::collect_dependencies(&self.project_root)
232    }
233
234    /// Get git commit hash
235    fn get_git_commit(&self) -> Option<String> {
236        utils::get_git_commit(&self.project_root)
237    }
238}