use super::{cache::cache_key, error::*, loader::load_make, model::*, state::*};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Instant;
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
pub struct Context {
pub root: PathBuf,
pub make: Arc<Make>,
pub state_path: PathBuf,
pub env: Vec<(String, String)>,
hook_guard: Arc<Mutex<HashSet<String>>>,
call_chain: Arc<Mutex<Vec<String>>>,
}
impl Context {
pub fn new(
root: PathBuf, make: Arc<Make>, state_path: PathBuf, env: Vec<(String, String)>,
) -> Self {
Self {
root,
make,
state_path,
env,
hook_guard: Arc::new(Mutex::new(HashSet::new())),
call_chain: Arc::new(Mutex::new(Vec::new())),
}
}
fn enter_phase(&self, phase: &str) -> Result<()> {
let mut guard = self
.hook_guard
.lock()
.map_err(|_| LifecycleError::MutexPoisoned {
phase: phase.to_string(),
})?;
let mut chain = self
.call_chain
.lock()
.map_err(|_| LifecycleError::MutexPoisoned {
phase: phase.to_string(),
})?;
if guard.contains(phase) {
let cycle_start = chain.iter().position(|p| p == phase).unwrap_or(0);
let mut cycle = chain[cycle_start..].to_vec();
cycle.push(phase.to_string());
return Err(LifecycleError::hook_recursion_with_chain(
phase.to_string(),
cycle,
));
}
guard.insert(phase.to_string());
chain.push(phase.to_string());
Ok(())
}
fn exit_phase(&self, phase: &str) {
match self.hook_guard.lock() {
Ok(mut guard) => {
guard.remove(phase);
}
Err(e) => {
tracing::error!(
phase = %phase,
error = %e,
"CRITICAL: Hook guard mutex poisoned - system may be in inconsistent state"
);
}
}
match self.call_chain.lock() {
Ok(mut chain) => {
if chain.last().map(|s| s.as_str()) == Some(phase) {
chain.pop();
}
}
Err(e) => {
tracing::error!(
phase = %phase,
error = %e,
"CRITICAL: Call chain mutex poisoned - system may be in inconsistent state"
);
}
}
}
}
#[tracing::instrument(name = "ggen.lifecycle.phase", skip(ctx), fields(phase = phase_name, duration_ms, status))]
pub fn run_phase(ctx: &Context, phase_name: &str) -> Result<()> {
tracing::info!(phase = phase_name, "lifecycle phase starting");
ctx.enter_phase(phase_name)?;
let result = run_phase_internal(ctx, phase_name);
ctx.exit_phase(phase_name);
match &result {
Ok(()) => {
tracing::Span::current().record("status", "success");
tracing::info!(phase = phase_name, "lifecycle phase completed");
}
Err(e) => {
tracing::Span::current().record("status", "error");
tracing::error!(phase = phase_name, error = %e, "lifecycle phase failed");
}
}
result
}
fn run_phase_internal(ctx: &Context, phase_name: &str) -> Result<()> {
let phase = ctx
.make
.lifecycle
.get(phase_name)
.ok_or_else(|| LifecycleError::phase_not_found(phase_name))?;
run_before_hooks(ctx, phase_name)?;
crate::alert_info!(&format!("Running phase: {}", phase_name));
let cmds = phase.commands();
if cmds.is_empty() {
tracing::warn!(phase = %phase_name, "Phase has no commands");
return Ok(());
}
let key = cache_key(phase_name, &cmds, &ctx.env, &[]);
let started = current_time_ms()?;
let timer = Instant::now();
tracing::info!(phase = %phase_name, "Starting phase execution");
for cmd in &cmds {
tracing::debug!(phase = %phase_name, command = %cmd, "Executing command");
execute_command(cmd, &ctx.root, &ctx.env)?;
}
let duration = timer.elapsed().as_millis();
tracing::Span::current().record("duration_ms", duration);
tracing::info!(
phase = %phase_name,
duration_ms = duration,
"Phase completed successfully"
);
let mut state = load_state(&ctx.state_path)?;
state.record_run(phase_name.to_string(), started, duration, true);
state.add_cache_key(phase_name.to_string(), key);
use super::state_validation::ValidatedLifecycleState;
let validated_state = ValidatedLifecycleState::new(state.clone())
.map_err(|e| LifecycleError::Other(format!("State validation failed: {}", e)))?;
save_state(&ctx.state_path, validated_state.state())?;
run_after_hooks(ctx, phase_name)?;
Ok(())
}
#[tracing::instrument(name = "ggen.lifecycle.pipeline", skip(ctx), fields(phases = ?phases, phase_count = phases.len()))]
pub fn run_pipeline(ctx: &Context, phases: &[String]) -> Result<()> {
tracing::info!(phases = ?phases, "starting lifecycle pipeline");
if let Some(workspaces) = &ctx.make.workspace {
let parallel = phases
.first()
.and_then(|p| ctx.make.lifecycle.get(p))
.and_then(|ph| ph.parallel)
.unwrap_or(false);
if parallel {
use rayon::prelude::*;
use rayon::ThreadPoolBuilder;
let max_threads = 8.min(num_cpus::get());
let pool = ThreadPoolBuilder::new()
.num_threads(max_threads)
.build()
.map_err(|e| {
LifecycleError::Other(format!("Failed to create thread pool: {}", e))
})?;
let results: Vec<Result<()>> = pool.install(|| {
workspaces
.par_iter()
.map(|(ws_name, workspace)| {
tracing::info!(workspace = %ws_name, "Processing workspace");
let ws_ctx = create_workspace_context(
&ctx.root, ws_name, workspace, &ctx.make, &ctx.env,
)?;
for phase in phases {
run_phase(&ws_ctx, phase)?;
}
Ok(())
})
.collect()
});
for result in results {
result?;
}
} else {
for (ws_name, workspace) in workspaces {
tracing::info!(workspace = %ws_name, "Processing workspace");
let ws_ctx =
create_workspace_context(&ctx.root, ws_name, workspace, &ctx.make, &ctx.env)?;
for phase in phases {
run_phase(&ws_ctx, phase)?;
}
}
}
} else {
for phase in phases {
run_phase(ctx, phase)?;
}
}
Ok(())
}
fn create_workspace_context(
root: &Path, ws_name: &str, workspace: &super::model::Workspace, root_make: &Arc<Make>,
env: &[(String, String)],
) -> Result<Context> {
let ws_path = root.join(&workspace.path);
let canonical_root = root
.canonicalize()
.map_err(|e| LifecycleError::Other(format!("Failed to canonicalize root path: {}", e)))?;
let canonical_ws = ws_path.canonicalize().map_err(|e| {
LifecycleError::Other(format!(
"Failed to canonicalize workspace '{}' path: {}",
ws_name, e
))
})?;
if !canonical_ws.starts_with(&canonical_root) {
return Err(LifecycleError::Other(format!(
"Security violation: workspace '{}' path '{}' is outside project root",
ws_name, workspace.path
)));
}
let ws_make_path = canonical_ws.join("make.toml");
let ws_make = if ws_make_path.exists() {
Arc::new(load_make(&ws_make_path)?)
} else {
Arc::clone(root_make)
};
let ws_state_path = canonical_ws.join(".ggen/state.json");
Ok(Context::new(
canonical_ws,
ws_make,
ws_state_path,
env.to_vec(),
))
}
fn run_before_hooks(ctx: &Context, phase_name: &str) -> Result<()> {
if let Some(hooks) = &ctx.make.hooks {
if let Some(before_all) = &hooks.before_all {
for hook_phase in before_all {
run_phase(ctx, hook_phase)?;
}
}
let before_hooks = match phase_name {
"init" => hooks.before_init.as_ref(),
"setup" => hooks.before_setup.as_ref(),
"build" => hooks.before_build.as_ref(),
"test" => hooks.before_test.as_ref(),
"deploy" => hooks.before_deploy.as_ref(),
_ => None,
};
let hooks_list = before_hooks.or_else(|| {
let hook_key = format!("before_{}", phase_name);
hooks.phase_hooks.get(&hook_key)
});
if let Some(hooks_list) = hooks_list {
for hook_phase in hooks_list {
run_phase(ctx, hook_phase)?;
}
}
}
Ok(())
}
fn run_after_hooks(ctx: &Context, phase_name: &str) -> Result<()> {
if let Some(hooks) = &ctx.make.hooks {
let after_hooks = match phase_name {
"init" => hooks.after_init.as_ref(),
"setup" => hooks.after_setup.as_ref(),
"build" => hooks.after_build.as_ref(),
"test" => hooks.after_test.as_ref(),
"deploy" => hooks.after_deploy.as_ref(),
_ => None,
};
let hooks_list = after_hooks.or_else(|| {
let hook_key = format!("after_{}", phase_name);
hooks.phase_hooks.get(&hook_key)
});
if let Some(hooks_list) = hooks_list {
for hook_phase in hooks_list {
run_phase(ctx, hook_phase)?;
}
}
if let Some(after_all) = &hooks.after_all {
for hook_phase in after_all {
run_phase(ctx, hook_phase)?;
}
}
}
Ok(())
}
fn execute_command(cmd: &str, cwd: &Path, env: &[(String, String)]) -> Result<()> {
use std::time::Duration;
let mut command = if cfg!(target_os = "windows") {
let mut c = Command::new("cmd");
c.arg("/C");
c
} else {
let mut c = Command::new("sh");
c.arg("-c"); c
};
command.current_dir(cwd).arg(cmd);
for (key, value) in env {
command.env(key, value);
}
let mut child = command
.stdout(std::process::Stdio::inherit()) .stderr(std::process::Stdio::inherit()) .spawn()
.map_err(|e| LifecycleError::command_spawn("unknown", cmd, e))?;
const DEFAULT_COMMAND_TIMEOUT_SECS: u64 = 300;
let timeout = Duration::from_secs(DEFAULT_COMMAND_TIMEOUT_SECS);
let start = Instant::now();
loop {
match child
.try_wait()
.map_err(|e| LifecycleError::command_spawn("unknown", cmd, e))?
{
Some(status) => {
if !status.success() {
let exit_code = status.code().unwrap_or(-1);
return Err(LifecycleError::command_failed(
"unknown",
cmd,
exit_code,
"Command output shown above".to_string(),
));
}
return Ok(());
}
None if start.elapsed() > timeout => {
let _ = child.kill(); return Err(LifecycleError::Other(format!(
"Command timeout after {}s: {}",
timeout.as_secs(),
cmd
)));
}
None => {
std::thread::sleep(Duration::from_millis(100));
}
}
}
}
fn current_time_ms() -> Result<u128> {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.map_err(|_| LifecycleError::Other("System clock error: time is before UNIX epoch".into()))
}