use crate::cli::args::InstallerGraphFormat;
use crate::models::{Error, Result};
use std::path::Path;
pub(crate) fn installer_test_command(
path: &Path,
matrix: Option<&str>,
coverage: bool,
) -> Result<()> {
use crate::installer::{self, ContainerRuntime, ContainerTestMatrix, MatrixConfig};
let result = installer::validate_installer(path)?;
if let Some(platforms) = matrix {
let runtime = ContainerRuntime::detect();
let runtime_name = runtime.map_or("none", |r| r.command());
let config = if platforms.is_empty() || platforms == "default" {
MatrixConfig::default_platforms()
} else if platforms == "extended" {
MatrixConfig::extended_platforms()
} else {
MatrixConfig::from_platform_string(platforms)
};
let mut matrix_runner = ContainerTestMatrix::new(path, config);
if runtime.is_none() {
println!("\u{26a0} Warning: No container runtime detected (docker/podman)");
println!(" Running in simulation mode\n");
}
let summary = matrix_runner.simulate();
println!("{}", matrix_runner.format_results());
println!("{}", summary.format());
println!(" Steps per platform: {}", result.steps);
println!(" Artifacts: {}", result.artifacts);
println!(" Runtime: {}", runtime_name);
if coverage {
println!(" Coverage: enabled");
}
println!();
if summary.all_passed() {
println!("\u{2713} All {} platform(s) passed", summary.total);
} else {
println!(
"\u{2717} {} of {} platform(s) failed",
summary.failed, summary.total
);
return Err(Error::Validation(format!(
"{} platform(s) failed testing",
summary.failed
)));
}
} else {
println!("Installer test summary:");
println!(" Steps: {}", result.steps);
println!(" Artifacts: {}", result.artifacts);
if coverage {
println!(" Coverage: enabled");
}
println!("\u{2713} Installer specification validated");
}
Ok(())
}
pub(crate) fn installer_lock_command(path: &Path, update: bool, verify: bool) -> Result<()> {
use crate::installer::{self, Lockfile};
let result = installer::validate_installer(path)?;
let lockfile_path = path.join("installer.lock");
println!("Managing lockfile for installer at {}", path.display());
println!();
if verify {
return installer_lock_verify(path, &lockfile_path, &result);
}
if update && !lockfile_path.exists() {
println!(" No existing lockfile found, generating new one...");
} else if update {
let existing = Lockfile::load(&lockfile_path)?;
println!("Updating lockfile...");
println!(
" Existing lockfile has {} artifacts",
existing.artifacts.len()
);
}
installer_lock_generate(path, &lockfile_path, &result)
}
fn installer_lock_verify(
path: &Path,
lockfile_path: &Path,
result: &crate::installer::ValidationResult,
) -> Result<()> {
use crate::installer::{HermeticContext, Lockfile, LOCKFILE_VERSION};
if !lockfile_path.exists() {
if result.artifacts == 0 {
println!("\u{2713} No lockfile needed (no external artifacts)");
} else {
return Err(Error::Validation(format!(
"Lockfile required but not found. Run 'bashrs installer lock {}' first.",
path.display()
)));
}
return Ok(());
}
let lockfile = Lockfile::load(lockfile_path)?;
lockfile.verify()?;
println!("Lockfile verification:");
println!(" Version: {}", LOCKFILE_VERSION);
println!(" Generator: {}", lockfile.generator);
println!(" Content hash: {}", lockfile.content_hash);
println!(" Artifacts: {}", lockfile.artifacts.len());
println!();
if lockfile.artifacts.len() != result.artifacts {
println!("\u{26a0} Lockfile may be outdated:");
println!(
" Lockfile has {} artifacts, spec has {}",
lockfile.artifacts.len(),
result.artifacts
);
println!(
" Run 'bashrs installer lock {} --update' to refresh",
path.display()
);
} else {
println!("\u{2713} Lockfile is valid and up-to-date");
}
let context = HermeticContext::from_lockfile(lockfile)?;
println!(" SOURCE_DATE_EPOCH: {}", context.source_date_epoch());
Ok(())
}
fn installer_lock_generate(
path: &Path,
lockfile_path: &Path,
result: &crate::installer::ValidationResult,
) -> Result<()> {
use crate::installer::{LockedArtifact, Lockfile, LockfileEnvironment, LOCKFILE_VERSION};
let epoch = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let mut lockfile = Lockfile::new();
lockfile.environment = LockfileEnvironment::deterministic(epoch);
if result.artifacts == 0 {
println!("\u{2713} No external artifacts to lock");
println!(" Hermetic mode will use empty lockfile");
lockfile.finalize();
lockfile.save(lockfile_path)?;
println!(" Created: {}", lockfile_path.display());
println!(
" SOURCE_DATE_EPOCH: {}",
lockfile.environment.source_date_epoch
);
return Ok(());
}
println!("Generating lockfile for {} artifacts...", result.artifacts);
println!();
for i in 0..result.artifacts {
let artifact = LockedArtifact::new(
&format!("artifact-{}", i + 1),
"1.0.0",
"https://example.com/artifact.tar.gz",
"sha256:placeholder",
0,
);
lockfile.add_artifact(artifact);
}
lockfile.finalize();
lockfile.save(lockfile_path)?;
println!("\u{2713} Generated lockfile: {}", lockfile_path.display());
println!(" Version: {}", LOCKFILE_VERSION);
println!(" Content hash: {}", lockfile.content_hash);
println!(" Artifacts locked: {}", lockfile.artifacts.len());
println!(
" SOURCE_DATE_EPOCH: {}",
lockfile.environment.source_date_epoch
);
println!();
println!("Note: Run with real artifact URLs to generate proper hashes.");
println!(
" Use 'bashrs installer run {} --hermetic' to execute.",
path.display()
);
Ok(())
}
pub(crate) fn installer_graph_command(path: &Path, format: InstallerGraphFormat) -> Result<()> {
use crate::installer::{format_execution_plan, InstallerGraph, InstallerSpec};
let installer_toml = if path.is_dir() {
path.join("installer.toml")
} else {
path.to_path_buf()
};
let content = std::fs::read_to_string(&installer_toml).map_err(|e| {
Error::Io(std::io::Error::new(
e.kind(),
format!("Failed to read installer.toml: {}", e),
))
})?;
let spec = InstallerSpec::parse(&content)?;
let graph = InstallerGraph::from_spec(&spec)?;
match format {
InstallerGraphFormat::Mermaid => {
println!("{}", graph.to_mermaid());
}
InstallerGraphFormat::Dot => {
println!("{}", graph.to_dot());
}
InstallerGraphFormat::Json => {
let plan = graph.create_plan();
let json_output = serde_json::json!({
"nodes": graph.nodes().iter().map(|n| {
serde_json::json!({
"id": n.id,
"name": n.name,
"estimated_duration_secs": n.estimated_duration_secs,
"capabilities": n.capabilities,
"exclusive_resource": n.exclusive_resource,
})
}).collect::<Vec<_>>(),
"execution_plan": {
"waves": plan.waves.iter().map(|w| {
serde_json::json!({
"wave_number": w.wave_number,
"step_ids": w.step_ids,
"is_sequential": w.is_sequential,
"sequential_reason": w.sequential_reason,
"estimated_duration_secs": w.estimated_duration_secs,
})
}).collect::<Vec<_>>(),
"total_duration_parallel_secs": plan.total_duration_parallel_secs,
"total_duration_sequential_secs": plan.total_duration_sequential_secs,
"speedup_percent": plan.speedup_percent,
}
});
println!(
"{}",
serde_json::to_string_pretty(&json_output).unwrap_or_default()
);
}
}
if !matches!(format, InstallerGraphFormat::Json) {
println!();
let plan = graph.create_plan();
println!("{}", format_execution_plan(&plan, 4));
}
Ok(())
}
pub(crate) fn installer_resume_command(path: &Path, from: Option<&str>) -> Result<()> {
use crate::installer::{self, CheckpointStore};
let validation = installer::validate_installer(path)?;
let checkpoint_dir = path.join(".checkpoint");
if !checkpoint_dir.exists() {
return Err(Error::Validation(format!(
"No checkpoint found at {} - run 'bashrs installer run {}' first",
checkpoint_dir.display(),
path.display()
)));
}
let store = CheckpointStore::load(&checkpoint_dir)?;
println!("Resume installer: {}", path.display());
println!();
if let Some(run_id) = store.current_run_id() {
println!("Checkpoint found: {}", run_id);
println!(
" Hermetic mode: {}",
if store.is_hermetic() { "yes" } else { "no" }
);
let steps = store.steps();
let completed = steps
.iter()
.filter(|s| s.status == installer::StepStatus::Completed)
.count();
let failed = steps
.iter()
.filter(|s| s.status == installer::StepStatus::Failed)
.count();
let pending = steps
.iter()
.filter(|s| s.status == installer::StepStatus::Pending)
.count();
println!(
" Steps: {} total, {} completed, {} failed, {} pending",
steps.len(),
completed,
failed,
pending
);
if let Some(last) = store.last_successful_step() {
println!(" Last successful: {}", last.step_id);
}
let resume_from = match from {
Some(step_id) => {
if store.get_step(step_id).is_none() {
return Err(Error::Validation(format!(
"Step '{}' not found in checkpoint",
step_id
)));
}
step_id.to_string()
}
None => store
.last_successful_step()
.map(|s| s.step_id.clone())
.ok_or_else(|| {
Error::Validation("No successful steps to resume from".to_string())
})?,
};
println!();
println!("Would resume from step: {}", resume_from);
println!();
println!("Note: Full execution not yet implemented.");
println!(" Steps in spec: {}", validation.steps);
println!(
" Run with --dry-run to validate: bashrs installer run {} --dry-run",
path.display()
);
} else {
return Err(Error::Validation(
"Checkpoint exists but has no active run".to_string(),
));
}
Ok(())
}