mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Package creation functions
//!
//! Functions for creating deployment packages from collected artifacts.

use super::types::{AssetInfo, BinaryInfo, PackageConfig, PackageManifest};
use super::utils;
use anyhow::{Context, Result};
use flate2::write::GzEncoder;
use std::fs;
use std::path::{Path, PathBuf};

/// Create package archive from collected artifacts
///
/// # Arguments
///
/// * `project_root` - Root directory of the project
/// * `project_name` - Name of the project for package filename
/// * `project_version` - Version string for package filename
/// * `config` - Package configuration
/// * `manifest` - Package manifest to include
/// * `binaries` - Collected binary files
/// * `assets` - Collected asset files
pub(super) fn create_package(
    project_root: &Path,
    project_name: &str,
    project_version: &str,
    config: &PackageConfig,
    manifest: &PackageManifest,
    binaries: &[BinaryInfo],
    assets: &[AssetInfo],
) -> Result<PathBuf> {
    fs::create_dir_all(&config.output_dir)?;

    let staging_dir = config.output_dir.join("staging");
    let _ = fs::remove_dir_all(&staging_dir);
    fs::create_dir_all(&staging_dir)?;

    // Copy binaries
    let bin_dir = staging_dir.join("bin");
    fs::create_dir_all(&bin_dir)?;

    let target_dir = project_root
        .join("target")
        .join(config.target_arch.as_str())
        .join(&config.build_profile);

    for binary in binaries {
        let source_name = binary.name.trim_end_matches(".exe");
        let source_path = target_dir.join(source_name);
        let dest_path = bin_dir.join(&binary.name);
        fs::copy(&source_path, &dest_path).with_context(|| format!("Failed to copy binary: {}", binary.name))?;

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mut perms = fs::metadata(&dest_path)?.permissions();
            perms.set_mode(0o755);
            fs::set_permissions(&dest_path, perms)?;
        }
    }

    // Copy configs
    let config_dir = staging_dir.join("config");
    fs::create_dir_all(&config_dir)?;

    for config_root in [project_root.join("config"), project_root.join("configs")] {
        if config_root.exists() {
            utils::copy_dir_recursive(&config_root, &config_dir)?;
        }
    }

    // Copy assets
    if !assets.is_empty() {
        let asset_dir = staging_dir.join("assets");
        fs::create_dir_all(&asset_dir)?;

        for asset_root in [
            project_root.join("assets"),
            project_root.join("models"),
            project_root.join("data"),
            project_root.join("calibration"),
        ] {
            if asset_root.exists() {
                utils::copy_dir_recursive(&asset_root, &asset_dir)?;
            }
        }
    }

    // Write manifest
    let manifest_path = staging_dir.join("manifest.json");
    fs::write(&manifest_path, serde_json::to_string_pretty(&manifest)?)?;

    // Create tarball
    let package_filename = format!(
        "{}-{}-{}.tar.gz",
        project_name,
        project_version,
        config.target_arch.as_str()
    );
    let package_path = config.output_dir.join(&package_filename);

    create_tarball(&staging_dir, &package_path)?;

    // Clean staging
    let _ = fs::remove_dir_all(&staging_dir);

    Ok(package_path)
}

/// Create tar.gz archive from source directory
///
/// # Arguments
///
/// * `source_dir` - Directory to archive
/// * `output_path` - Output path for the tar.gz file
pub(super) fn create_tarball(source_dir: &Path, output_path: &Path) -> Result<()> {
    let tar_gz = fs::File::create(output_path)?;
    let enc = GzEncoder::new(tar_gz, flate2::Compression::default());
    let mut tar = tar::Builder::new(enc);

    tar.append_dir_all(".", source_dir)?;
    tar.finish()?;

    Ok(())
}