mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Package service for creating deployment packages
//!
//! This service provides operations for building deployment packages containing
//! binaries, configurations, and assets for distribution to target systems.

#![allow(dead_code)]

mod collector;
mod packager;
mod types;
mod utils;

pub use types::{AssetInfo, BinaryInfo, ConfigInfo, PackageConfig, PackageManifest};

use anyhow::{Context, Result};
use chrono::Utc;
use indicatif::{ProgressBar, ProgressStyle};
use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;

/// Package service for creating deployment packages
///
/// # Examples
///
/// ```rust,ignore
/// use mecha10_cli::services::{PackageService, PackageConfig, TargetArch};
/// use std::path::Path;
///
/// # fn example() -> anyhow::Result<()> {
/// let service = PackageService::new(
///     "my-robot".to_string(),
///     "1.0.0".to_string(),
///     Path::new("/path/to/project")
/// )?;
///
/// // Build package with default config
/// let package_path = service.build(PackageConfig::default())?;
/// println!("Package created: {}", package_path.display());
///
/// // Build for specific target
/// let mut config = PackageConfig::default();
/// config.target_arch = TargetArch::Aarch64UnknownLinuxGnu;
/// let arm_package = service.build(config)?;
/// # Ok(())
/// # }
/// ```
pub struct PackageService {
    project_name: String,
    project_version: String,
    project_root: PathBuf,
}

impl PackageService {
    /// Create a new package service
    ///
    /// # Arguments
    ///
    /// * `project_name` - Name of the project
    /// * `project_version` - Version string
    /// * `project_root` - Root directory of the project
    pub fn new(project_name: String, project_version: String, project_root: impl Into<PathBuf>) -> Result<Self> {
        let project_root = project_root.into();
        if !project_root.exists() {
            return Err(anyhow::anyhow!(
                "Project root does not exist: {}",
                project_root.display()
            ));
        }

        Ok(Self {
            project_name,
            project_version,
            project_root,
        })
    }

    /// Build a deployment package
    ///
    /// # Arguments
    ///
    /// * `config` - Package configuration
    pub fn build(&self, config: PackageConfig) -> Result<PathBuf> {
        // Build binaries
        self.build_binaries(&config)?;

        // Collect package contents
        let binaries = self.collect_binaries(&config)?;
        let configs = self.collect_configs()?;
        let assets = if config.include_assets {
            self.collect_assets()?
        } else {
            Vec::new()
        };

        // Create manifest
        let manifest = self.create_manifest(&config, &binaries, &configs, &assets)?;

        // Validate package
        self.validate_package(&manifest)?;

        // Create package archive
        let package_path = self.create_package(&config, &manifest, &binaries, &assets)?;

        Ok(package_path)
    }

    /// Build binaries for target architecture
    fn build_binaries(&self, config: &PackageConfig) -> Result<()> {
        let spinner = ProgressBar::new_spinner();
        spinner.set_style(
            ProgressStyle::default_spinner()
                .template("   {spinner:.green} {msg}")
                .unwrap(),
        );
        spinner.set_message(format!("Building for {}...", config.target_arch.as_str()));
        spinner.enable_steady_tick(Duration::from_millis(100));

        let mut cmd = Command::new("cargo");
        cmd.arg("build").arg("--workspace").current_dir(&self.project_root);

        if config.build_profile == "release" {
            cmd.arg("--release");
        }

        cmd.arg("--target").arg(config.target_arch.as_str());

        let status = cmd.status().context("Failed to run cargo build")?;

        spinner.finish_and_clear();

        if !status.success() {
            return Err(anyhow::anyhow!(
                "Build failed for target {}",
                config.target_arch.as_str()
            ));
        }

        Ok(())
    }

    /// Collect built binaries
    fn collect_binaries(&self, config: &PackageConfig) -> Result<Vec<BinaryInfo>> {
        collector::collect_binaries(&self.project_root, config)
    }

    /// Collect configuration files
    fn collect_configs(&self) -> Result<Vec<ConfigInfo>> {
        collector::collect_configs(&self.project_root)
    }

    /// Collect asset files
    fn collect_assets(&self) -> Result<Vec<AssetInfo>> {
        collector::collect_assets(&self.project_root)
    }

    /// Create package manifest
    fn create_manifest(
        &self,
        config: &PackageConfig,
        binaries: &[BinaryInfo],
        configs: &[ConfigInfo],
        assets: &[AssetInfo],
    ) -> Result<PackageManifest> {
        Ok(PackageManifest {
            format_version: PackageManifest::FORMAT_VERSION.to_string(),
            name: self.project_name.clone(),
            version: self.project_version.clone(),
            build_timestamp: Utc::now(),
            target_arch: config.target_arch,
            binaries: binaries.to_vec(),
            configs: configs.to_vec(),
            assets: assets.to_vec(),
            dependencies: self.collect_dependencies()?,
            metadata: config.custom_metadata.clone(),
            build_profile: config.build_profile.clone(),
            git_commit: self.get_git_commit(),
            environment: config.environment.clone(),
        })
    }

    /// Validate package contents
    fn validate_package(&self, manifest: &PackageManifest) -> Result<()> {
        if manifest.format_version != PackageManifest::FORMAT_VERSION {
            return Err(anyhow::anyhow!(
                "Invalid format version: {} (expected {})",
                manifest.format_version,
                PackageManifest::FORMAT_VERSION
            ));
        }

        if manifest.binaries.is_empty() {
            return Err(anyhow::anyhow!("Package must contain at least one binary"));
        }

        for binary in &manifest.binaries {
            if binary.name.is_empty() {
                return Err(anyhow::anyhow!("Binary name cannot be empty"));
            }
            if binary.size_bytes == 0 {
                return Err(anyhow::anyhow!("Binary {} has zero size", binary.name));
            }
        }

        for config in &manifest.configs {
            if config.name.is_empty() {
                return Err(anyhow::anyhow!("Config name cannot be empty"));
            }
        }

        Ok(())
    }

    /// Create package archive
    fn create_package(
        &self,
        config: &PackageConfig,
        manifest: &PackageManifest,
        binaries: &[BinaryInfo],
        assets: &[AssetInfo],
    ) -> Result<PathBuf> {
        packager::create_package(
            &self.project_root,
            &self.project_name,
            &self.project_version,
            config,
            manifest,
            binaries,
            assets,
        )
    }

    /// Collect project dependencies from Cargo.lock
    fn collect_dependencies(&self) -> Result<HashMap<String, String>> {
        utils::collect_dependencies(&self.project_root)
    }

    /// Get git commit hash
    fn get_git_commit(&self) -> Option<String> {
        utils::get_git_commit(&self.project_root)
    }
}