#![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;
pub struct PackageService {
project_name: String,
project_version: String,
project_root: PathBuf,
}
impl PackageService {
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,
})
}
pub fn build(&self, config: PackageConfig) -> Result<PathBuf> {
self.build_binaries(&config)?;
let binaries = self.collect_binaries(&config)?;
let configs = self.collect_configs()?;
let assets = if config.include_assets {
self.collect_assets()?
} else {
Vec::new()
};
let manifest = self.create_manifest(&config, &binaries, &configs, &assets)?;
self.validate_package(&manifest)?;
let package_path = self.create_package(&config, &manifest, &binaries, &assets)?;
Ok(package_path)
}
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(())
}
fn collect_binaries(&self, config: &PackageConfig) -> Result<Vec<BinaryInfo>> {
collector::collect_binaries(&self.project_root, config)
}
fn collect_configs(&self) -> Result<Vec<ConfigInfo>> {
collector::collect_configs(&self.project_root)
}
fn collect_assets(&self) -> Result<Vec<AssetInfo>> {
collector::collect_assets(&self.project_root)
}
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(),
})
}
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(())
}
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,
)
}
fn collect_dependencies(&self) -> Result<HashMap<String, String>> {
utils::collect_dependencies(&self.project_root)
}
fn get_git_commit(&self) -> Option<String> {
utils::get_git_commit(&self.project_root)
}
}