Files
honey-biscuit-workshop/workshop-baker/src/bake.rs
T
Catty Steve 26809df720 feat(bake): DAG scheduler, decorator execution, notify refactor, workshop-schedule crate
- Add workshop-schedule crate (petgraph-based DAG scheduling)
- Implement all bake decorators with combinator chain execution
- Refactor notify module: extract handler, rule, template, util, queue
- Add NotificationQueue with priority/drain semantics
- Enhance finalize plugin system with internal plugin registration
- Update ExecutionContext and event types
- Add per-crate AGENTS.md knowledge base files
- Remove inline AGENTS.md files (consolidated into per-crate docs)
2026-06-15 20:45:06 +08:00

229 lines
8.6 KiB
Rust

use crate::bake::builder::RenderMode;
use crate::bake::constant::DEFAULT_WORKSPACE;
use crate::bake::decorator::Decorator;
use crate::bake::error::BakeError;
use crate::bake::util::{resolve_mode, FuncMode};
use crate::cli::Cli;
use crate::prebake;
use crate::prebake::PrebakeConfig;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::time::Duration;
use workshop_baker_params::BakeSettings;
use workshop_engine::{Engine, EventSender, ExecutionContext};
mod builder;
pub mod constant;
mod decorator;
pub mod error;
mod execute;
pub mod parser;
mod schedule;
pub mod trivial;
pub mod util;
pub async fn bake(
script_path: &Path,
bake_base_path: &Path,
prebake_path: &Path,
_cli: &Cli,
ctx: &mut ExecutionContext,
event_tx: EventSender,
) -> Result<(), BakeError> {
log::debug!("Reading script: {:?}", script_path);
let script_content = std::fs::read_to_string(script_path).map_err(|e| {
BakeError::IoError { path: script_path.display().to_string(), source: e }
})?;
log::debug!("Reading prebake.yml: {:?}", prebake_path);
let prebake_content = std::fs::read_to_string(prebake_path).map_err(|e| {
BakeError::IoError { path: prebake_path.display().to_string(), source: e }
})?;
let prebake = prebake::parse(prebake_content.as_str()).map_err(|e| {
log::error!("Failed to parse prebake.yml: {}", e);
BakeError::YamlParseError(e)
})?;
let workspace = get_workspace(&prebake);
let functions = parser::parse_script(&script_content)?;
let has_pipeline = functions.pipeline_functions.iter().any(|f| {
f.decorators
.iter()
.any(|d| matches!(d, decorator::Decorator::Pipeline))
});
// TODO: if PIP(pipeline implicit parameter).trivial is set, always use RenderMode::Off
let render_mode = RenderMode::Full;
let engine = Engine::new();
// TODO: resolve from merged param layers (PIP integration)
let bake_settings = BakeSettings::default();
if let Some(env) = &prebake.envvars
&& let Some(prebake_env) = &env.bake
{
ctx.env_vars.extend(prebake_env.clone());
}
if !has_pipeline {
log::trace!("Running trivial script");
trivial::run_trivial(
&script_content,
bake_base_path,
&workspace,
&engine,
ctx,
&event_tx,
render_mode,
)
.await?;
} else {
log::trace!("Running pipeline script");
let remaining = functions.remaining_code;
let (mut dag, func_map) = schedule::build_dag(functions.pipeline_functions)?;
// Pre-scan: pipe sources + decorator conflict checks.
let mut pipe_sources: HashSet<String> = HashSet::new();
for func in func_map.values() {
decorator::check_conflicts(func)?;
for decorator in &func.decorators {
if let Decorator::Pipe(names) = decorator {
for source in names {
if *source != func.name {
pipe_sources.insert(source.clone());
}
}
}
}
}
let mut stdout_cache: HashMap<String, String> = HashMap::new();
let mut async_handles: Vec<tokio::task::JoinHandle<()>> = Vec::new();
while !dag.is_empty() {
log::debug!("[BAKE_SCHED] ready={} seq={} wild={} groups={:?}",
dag.ready_count(),
dag.ready_sequential().len(),
dag.ready_wildcards().len(),
dag.ready_groups(),
);
// Rule 1: sequential always goes first
let seq_candidates = dag.ready_sequential();
if let Some(seq_name) = seq_candidates.first() {
let seq_name = seq_name.to_string();
log::info!("[BAKE_SCHED] seq {}", seq_name);
let func = &func_map[&seq_name];
let mode = resolve_mode(func);
match mode {
FuncMode::Daemon => {
execute::execute_daemon(
&engine, func, ctx, &event_tx,
&workspace, &remaining, bake_base_path,
Duration::from_secs(bake_settings.health_period),
Duration::from_secs(bake_settings.health_timeout),
bake_settings.health_max_retries,
).await?;
}
FuncMode::Async => {
let handle = execute::execute_async(
func, ctx, &event_tx,
&workspace, &remaining, bake_base_path,
);
async_handles.push(handle);
}
FuncMode::Normal => {
let result = execute::execute_normal(
&engine, func, ctx, &event_tx,
&workspace, &remaining, bake_base_path, &stdout_cache,
).await;
match result {
Ok(stdout) => {
if pipe_sources.contains(&seq_name) {
stdout_cache.insert(seq_name.clone(), stdout);
}
}
Err(e) if dag.is_fallible(&seq_name) => {
log::warn!("'{}' failed (fallible): {}", seq_name, e);
}
Err(e) => return Err(e),
}
}
}
// Pop AFTER execution (Daemon/Async pop at start)
if !matches!(mode, FuncMode::Daemon | FuncMode::Async) {
dag.pop(&seq_name).map_err(|e| BakeError::IncompatibleDecorators {
function: seq_name.clone(),
reason: e.to_string(),
})?;
} else {
dag.pop(&seq_name).map_err(|e| BakeError::IncompatibleDecorators {
function: seq_name.clone(),
reason: e.to_string(),
})?;
}
continue;
}
// All remaining are parallel — pick a batch
if let Some(batch) = dag.pick_batch() {
log::info!("[BAKE_SCHED] par {:?}", batch);
let results = execute::fire_parallel_batch(
&batch, &engine, &func_map, ctx, &event_tx,
&workspace, &remaining, bake_base_path, &stdout_cache,
).await?;
for (name, stdout) in results {
if pipe_sources.contains(&name) {
stdout_cache.insert(name.clone(), stdout);
}
if let Err(e) = dag.pop(&name) {
log::warn!("Failed to pop '{}' from DAG: {}", name, e);
}
}
} else if dag.is_stalled() {
// Deadlock: named parallel groups exist but can't complete
let stalled = dag.stalled_groups();
let remaining = dag.remaining();
return Err(BakeError::IncompatibleDecorators {
function: remaining.join(", "),
reason: format!(
"parallel group(s) {:?} can never complete — blocked by @after chain",
stalled
),
});
} else {
// Ready set is empty — cycle
let remaining = dag.remaining();
if !remaining.is_empty() {
return Err(BakeError::CircularDependency(
remaining.into_iter().map(|s| s.to_string()).collect(),
));
}
break;
}
}
// Wait for all @async tasks to finish
for handle in async_handles {
let _ = handle.await;
}
}
Ok(())
}
fn get_workspace(prebake_config: &PrebakeConfig) -> PathBuf {
prebake_config
.bootstrap
.as_ref()
.and_then(|b| b.workspace.as_ref())
.map(|ws| {
if ws.fallback {
PathBuf::from(DEFAULT_WORKSPACE)
} else {
PathBuf::from(ws.path.clone())
}
})
.unwrap_or_else(|| PathBuf::from(DEFAULT_WORKSPACE))
}
// ── AI Coding Agent marker ──
// Code in this module PARTIALLY or FULLY utilized AI Coding Agent