use anyhow::Result;
use std::path::{Path, PathBuf};
use crate::{config, optimizer, pipeline};
#[derive(Debug)]
pub struct BuildResult {
pub cargo_changes: Vec<String>,
pub metrics: pipeline::SizeMetrics,
pub budget_check_passed: Option<bool>,
pub budget_threshold: Option<u64>,
pub dry_run: bool,
pub dry_run_files: Vec<String>,
}
type OptimizationResult = (Vec<String>, Vec<String>, Vec<(PathBuf, String)>);
pub struct BuildWorkflow {
project_root: PathBuf,
}
impl BuildWorkflow {
pub fn new(project_root: &Path) -> Self {
Self {
project_root: project_root.to_path_buf(),
}
}
pub fn execute(
&self,
dry_run: bool,
check_budget: bool,
_target_dir: Option<&str>,
) -> Result<BuildResult> {
let (cargo_changes, dry_run_files, backups) =
self.optimize_cargo_tomls_with_backup(dry_run)?;
let metrics = match self.run_build_pipeline() {
Ok(m) => m,
Err(e) => {
if !dry_run && !backups.is_empty() {
let _ = self.rollback_cargo_tomls(&backups);
}
return Err(e);
}
};
let (budget_check_passed, budget_threshold) = if check_budget {
self.check_budget(&metrics)?
} else {
(None, None)
};
Ok(BuildResult {
cargo_changes,
metrics,
budget_check_passed,
budget_threshold,
dry_run,
dry_run_files,
})
}
fn optimize_cargo_tomls_with_backup(&self, dry_run: bool) -> Result<OptimizationResult> {
let _config = config::ConfigLoader::load(&self.project_root)
.unwrap_or_else(|_| config::ConfigFile::default());
let mut changes = Vec::new();
let mut dry_run_files = Vec::new();
let mut backups = Vec::new();
let (cargo_changes, cargo_dry_run, cargo_backups) = self.optimize_cargo_files(dry_run)?;
changes.extend(cargo_changes);
dry_run_files.extend(cargo_dry_run);
backups.extend(cargo_backups);
let (build_std_changes, build_std_dry_run) = self.optimize_build_std_if_needed(dry_run)?;
changes.extend(build_std_changes);
dry_run_files.extend(build_std_dry_run);
Ok((changes, dry_run_files, backups))
}
fn optimize_cargo_files(&self, dry_run: bool) -> Result<OptimizationResult> {
let cargo_finder = optimizer::CargoFileFinder::new(&self.project_root);
let cargo_editor = optimizer::CargoTomlEditor::new();
let opt_config = optimizer::OptimizationConfig::default();
let mut changes = Vec::new();
let mut dry_run_files = Vec::new();
let mut backups = Vec::new();
let cargo_tomls = cargo_finder.find_cargo_tomls()?;
for toml_path in cargo_tomls {
let relative = toml_path
.strip_prefix(&self.project_root)
.unwrap_or(&toml_path)
.display()
.to_string();
if dry_run {
dry_run_files.push(relative);
} else {
if let Ok(original_content) = std::fs::read_to_string(&toml_path) {
backups.push((toml_path.clone(), original_content));
}
match cargo_editor.optimize_cargo_toml(&toml_path, &opt_config, None, dry_run) {
Ok(change_list) => {
changes.extend(change_list);
}
Err(_) => {
continue;
}
}
}
}
Ok((changes, dry_run_files, backups))
}
fn optimize_build_std_if_needed(&self, dry_run: bool) -> Result<(Vec<String>, Vec<String>)> {
let mut changes = Vec::new();
let mut dry_run_files = Vec::new();
if let Ok(true) = crate::toolchain::ToolchainDetector::new().is_nightly_toolchain() {
let build_std_optimizer = optimizer::BuildStdOptimizer::new(&self.project_root);
if !build_std_optimizer.is_configured().unwrap_or(false) {
if dry_run {
dry_run_files.push("build-std configuration".to_string());
} else {
let build_std_config = optimizer::BuildStdConfig::minimal();
if let Ok(build_std_changes) =
build_std_optimizer.apply_build_std(&build_std_config, dry_run)
{
changes.extend(build_std_changes);
}
}
}
}
Ok((changes, dry_run_files))
}
fn rollback_cargo_tomls(&self, backups: &[(PathBuf, String)]) -> Result<()> {
for (path, content) in backups {
std::fs::write(path, content)?;
}
Ok(())
}
fn run_build_pipeline(&self) -> Result<pipeline::SizeMetrics> {
use crate::error::WasmSlimError;
let _config = config::ConfigLoader::load(&self.project_root)
.unwrap_or_else(|_| config::ConfigFile::default());
let pipeline_config = pipeline::PipelineConfig {
opt_level: pipeline::WasmOptLevel::Oz,
run_wasm_snip: true,
..Default::default()
};
let build_pipeline = pipeline::BuildPipeline::new(&self.project_root, pipeline_config);
build_pipeline
.build()
.map_err(|e| anyhow::Error::from(WasmSlimError::from(e)))
}
fn check_budget(&self, metrics: &pipeline::SizeMetrics) -> Result<(Option<bool>, Option<u64>)> {
let config = config::ConfigLoader::load(&self.project_root)
.unwrap_or_else(|_| config::ConfigFile::default());
if let Some(budget) = &config.size_budget {
if let Some(max_size_kb) = budget.max_size_kb {
let max_size = max_size_kb * 1024;
let passed = metrics.after_bytes <= max_size;
if !passed {
anyhow::bail!(
"WASM bundle size ({} bytes) exceeds maximum ({} bytes)",
metrics.after_bytes,
max_size
);
}
return Ok((Some(passed), Some(max_size)));
}
}
Ok((None, None))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::{Path, PathBuf};
#[test]
fn test_build_workflow_new_with_different_paths() {
let paths = vec![
Path::new("/tmp"),
Path::new("/home/user/project"),
Path::new("."),
Path::new("./relative/path"),
];
for path in paths {
let workflow = BuildWorkflow::new(path);
assert_eq!(workflow.project_root, PathBuf::from(path));
}
}
#[test]
fn test_build_workflow_stores_project_root() {
let root = Path::new("/test/project");
let workflow = BuildWorkflow::new(root);
assert_eq!(workflow.project_root, root);
}
#[test]
fn test_build_result_can_be_created() {
use crate::pipeline::SizeMetrics;
let result = BuildResult {
cargo_changes: vec!["test".to_string()],
metrics: SizeMetrics {
before_bytes: 1000,
after_bytes: 800,
},
budget_check_passed: None,
budget_threshold: None,
dry_run: false,
dry_run_files: vec![],
};
assert_eq!(result.cargo_changes.len(), 1);
assert_eq!(result.metrics.before_bytes, 1000);
assert_eq!(result.metrics.after_bytes, 800);
}
#[test]
fn test_build_result_with_budget_check() {
use crate::pipeline::SizeMetrics;
let result = BuildResult {
cargo_changes: vec![],
metrics: SizeMetrics {
before_bytes: 2000,
after_bytes: 1500,
},
budget_check_passed: Some(true),
budget_threshold: Some(2000),
dry_run: false,
dry_run_files: vec![],
};
assert_eq!(result.budget_check_passed, Some(true));
assert_eq!(result.budget_threshold, Some(2000));
}
#[test]
fn test_build_result_with_failed_budget() {
use crate::pipeline::SizeMetrics;
let result = BuildResult {
cargo_changes: vec![],
metrics: SizeMetrics {
before_bytes: 1000,
after_bytes: 2500,
},
budget_check_passed: Some(false),
budget_threshold: Some(2000),
dry_run: false,
dry_run_files: vec![],
};
assert_eq!(result.budget_check_passed, Some(false));
assert!(result.metrics.after_bytes > result.budget_threshold.unwrap());
}
#[test]
fn test_build_result_dry_run_mode() {
use crate::pipeline::SizeMetrics;
let result = BuildResult {
cargo_changes: vec![],
metrics: SizeMetrics {
before_bytes: 1000,
after_bytes: 1000,
},
budget_check_passed: None,
budget_threshold: None,
dry_run: true,
dry_run_files: vec!["file1.toml".to_string(), "file2.toml".to_string()],
};
assert!(result.dry_run);
assert_eq!(result.dry_run_files.len(), 2);
}
#[test]
fn test_build_result_with_multiple_cargo_changes() {
use crate::pipeline::SizeMetrics;
let result = BuildResult {
cargo_changes: vec![
"change1".to_string(),
"change2".to_string(),
"change3".to_string(),
],
metrics: SizeMetrics {
before_bytes: 2000,
after_bytes: 1000,
},
budget_check_passed: None,
budget_threshold: None,
dry_run: false,
dry_run_files: vec![],
};
assert_eq!(result.cargo_changes.len(), 3);
assert!(result.metrics.before_bytes > result.metrics.after_bytes);
}
#[test]
fn test_optimization_result_empty_tuple() {
let result: OptimizationResult = (vec![], vec![], vec![]);
assert!(result.0.is_empty());
assert!(result.1.is_empty());
assert!(result.2.is_empty());
}
#[test]
fn test_optimization_result_with_changes() {
let result: OptimizationResult = (
vec!["change1".to_string(), "change2".to_string()],
vec![],
vec![],
);
assert_eq!(result.0.len(), 2);
assert!(result.1.is_empty());
assert!(result.2.is_empty());
}
#[test]
fn test_optimization_result_with_dry_run_files() {
let result: OptimizationResult = (
vec![],
vec!["file1.toml".to_string(), "file2.toml".to_string()],
vec![],
);
assert!(result.0.is_empty());
assert_eq!(result.1.len(), 2);
assert!(result.2.is_empty());
}
#[test]
fn test_optimization_result_with_backups() {
let result: OptimizationResult = (
vec![],
vec![],
vec![
(PathBuf::from("/path/backup1"), "content1".to_string()),
(PathBuf::from("/path/backup2"), "content2".to_string()),
],
);
assert!(result.0.is_empty());
assert!(result.1.is_empty());
assert_eq!(result.2.len(), 2);
}
#[test]
fn test_optimization_result_complete() {
let result: OptimizationResult = (
vec!["change".to_string()],
vec!["file.toml".to_string()],
vec![(PathBuf::from("/backup"), "content".to_string())],
);
assert_eq!(result.0.len(), 1);
assert_eq!(result.1.len(), 1);
assert_eq!(result.2.len(), 1);
}
#[test]
fn test_build_workflow_can_be_created() {
let workflow = BuildWorkflow::new(Path::new("/tmp"));
assert_eq!(workflow.project_root, PathBuf::from("/tmp"));
}
#[test]
fn test_rollback_cargo_tomls_restores_content() {
use std::fs;
use tempfile::NamedTempFile;
let workflow = BuildWorkflow::new(Path::new("/tmp"));
let temp_file1 = NamedTempFile::new().unwrap();
let temp_file2 = NamedTempFile::new().unwrap();
let original_content1 = "original content 1";
let original_content2 = "original content 2";
fs::write(temp_file1.path(), "modified").unwrap();
fs::write(temp_file2.path(), "modified").unwrap();
let backups = vec![
(
temp_file1.path().to_path_buf(),
original_content1.to_string(),
),
(
temp_file2.path().to_path_buf(),
original_content2.to_string(),
),
];
let result = workflow.rollback_cargo_tomls(&backups);
assert!(result.is_ok());
assert_eq!(
fs::read_to_string(temp_file1.path()).unwrap(),
original_content1
);
assert_eq!(
fs::read_to_string(temp_file2.path()).unwrap(),
original_content2
);
}
#[test]
fn test_rollback_cargo_tomls_with_empty_backups() {
let workflow = BuildWorkflow::new(Path::new("/tmp"));
let backups = [];
let result = workflow.rollback_cargo_tomls(&backups);
assert!(result.is_ok());
}
#[test]
fn test_build_result_debug_format() {
use crate::pipeline::SizeMetrics;
let result = BuildResult {
cargo_changes: vec!["test".to_string()],
metrics: SizeMetrics {
before_bytes: 1000,
after_bytes: 800,
},
budget_check_passed: None,
budget_threshold: None,
dry_run: false,
dry_run_files: vec![],
};
let debug_str = format!("{:?}", result);
assert!(debug_str.contains("BuildResult"));
}
#[test]
fn test_build_result_all_fields() {
use crate::pipeline::SizeMetrics;
let result = BuildResult {
cargo_changes: vec!["opt-level=z".to_string(), "lto=true".to_string()],
metrics: SizeMetrics {
before_bytes: 5000,
after_bytes: 3000,
},
budget_check_passed: Some(true),
budget_threshold: Some(4000),
dry_run: true,
dry_run_files: vec!["Cargo.toml".to_string()],
};
assert_eq!(result.cargo_changes.len(), 2);
assert_eq!(result.metrics.before_bytes, 5000);
assert_eq!(result.metrics.after_bytes, 3000);
assert_eq!(result.budget_check_passed, Some(true));
assert_eq!(result.budget_threshold, Some(4000));
assert!(result.dry_run);
assert_eq!(result.dry_run_files.len(), 1);
}
#[test]
fn test_optimization_result_type_alias() {
let _result: OptimizationResult = (
vec!["change".to_string()],
vec!["file.toml".to_string()],
vec![(PathBuf::from("/backup"), "content".to_string())],
);
}
}