26809df720
- 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)
180 lines
5.5 KiB
Rust
180 lines
5.5 KiB
Rust
use crate::bake::constant::PIPELINE_SKIP_ERRORCODE;
|
|
use crate::bake::decorator::Decorator;
|
|
use crate::bake::error::BakeError;
|
|
use crate::bake::parser::Function;
|
|
use minijinja::{Environment, context};
|
|
use std::fs::Permissions;
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use tempfile::NamedTempFile;
|
|
|
|
/// Three-level rendering control.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum RenderMode {
|
|
/// No template, hardcoded "{{ main }}". Debug escape hatch.
|
|
Off,
|
|
/// Uses bake_base.sh with essential execution skeleton only.
|
|
Minimal,
|
|
/// Uses bake_base.sh with helpers (logs, colors, etc.).
|
|
Full,
|
|
}
|
|
|
|
impl RenderMode {
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
RenderMode::Off => "off",
|
|
RenderMode::Minimal => "minimal",
|
|
RenderMode::Full => "full",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Hardcoded template for RenderMode::Off.
|
|
const TRIVIAL_TEMPLATE: &str = "{{ main }}";
|
|
|
|
/// Renders a `Function` into an executable shell script using a minijinja template.
|
|
///
|
|
/// `render_mode` controls the template source:
|
|
/// - `Off`: hardcoded `{{ main }}`, no bake_base file needed.
|
|
/// - `Minimal` / `Full`: reads bake_base.sh; the template distinguishes the two
|
|
/// via the `render_mode` context variable.
|
|
pub fn build_script(
|
|
function: &Function,
|
|
bake_base: &Path,
|
|
target_dir: &Path,
|
|
render_mode: RenderMode,
|
|
) -> Result<PathBuf, BakeError> {
|
|
let name = &function.name;
|
|
let body = &function.body;
|
|
let condition = function.find_decorator(|d| {
|
|
if let Decorator::If(content) = d {
|
|
Some(content.clone())
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
// let temp_path = temp_path.unwrap_or_else(|| PathBuf::from("/tmp"));
|
|
let temp_path = target_dir;
|
|
let filename = format!("bakefn_{}.sh", name);
|
|
let target = target_dir.join(filename);
|
|
|
|
let template_content = match render_mode {
|
|
RenderMode::Off => TRIVIAL_TEMPLATE.to_string(),
|
|
RenderMode::Minimal | RenderMode::Full => {
|
|
log::debug!("Reading bake_base: {:?}", bake_base);
|
|
std::fs::read_to_string(bake_base).map_err(|e| BakeError::IoError {
|
|
path: bake_base.display().to_string(),
|
|
source: e,
|
|
})?
|
|
}
|
|
};
|
|
|
|
let condition_code = match condition {
|
|
Some(content) => {
|
|
// with some bash workaround
|
|
format!(
|
|
"if [[ ! {} ]]; then\n exit {}\nfi\n",
|
|
content, PIPELINE_SKIP_ERRORCODE
|
|
)
|
|
}
|
|
None => String::new(),
|
|
};
|
|
|
|
// preexport/export: stubs for the future IPC-based @export.
|
|
let preexport_code = String::new();
|
|
let export_code = String::new();
|
|
|
|
let mut env = Environment::new();
|
|
env.add_template("script", &template_content)?;
|
|
let rendered = env.get_template("script")?.render(
|
|
context! {
|
|
main => body,
|
|
condition => condition_code,
|
|
preexport => preexport_code,
|
|
name => name,
|
|
export => export_code,
|
|
render_mode => render_mode.as_str(),
|
|
},
|
|
)?;
|
|
|
|
let mut temp_file =
|
|
NamedTempFile::new_in(&temp_path).map_err(|e| BakeError::ScriptGenerationFailed {
|
|
script: temp_path.display().to_string(),
|
|
action: "creating temp file".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
|
|
temp_file
|
|
.as_file_mut()
|
|
.set_permissions(Permissions::from_mode(0o700))
|
|
.map_err(|e| BakeError::ScriptGenerationFailed {
|
|
script: temp_path.display().to_string(),
|
|
action: "setting permissions".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
|
|
std::fs::write(&temp_file, rendered).map_err(|e| BakeError::ScriptGenerationFailed {
|
|
script: temp_path.display().to_string(),
|
|
action: "writing to temp file".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
temp_file
|
|
.persist(&target)
|
|
.map_err(|e| BakeError::ScriptGenerationFailed {
|
|
script: target.display().to_string(),
|
|
action: "persisting temp file".to_string(),
|
|
reason: e.error.to_string(),
|
|
})?;
|
|
Ok(target)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_build_script_full_mode() {
|
|
let temp_dir = tempfile::tempdir().unwrap();
|
|
let bake_base = temp_dir.path().join("bake_base.sh");
|
|
|
|
std::fs::write(&bake_base, "{{ main }}\n").unwrap();
|
|
|
|
let function = Function {
|
|
name: "test".to_string(),
|
|
decorators: vec![],
|
|
body: "echo 'hello world'".to_string(),
|
|
};
|
|
let result = build_script(
|
|
&function, &bake_base, temp_dir.path(), RenderMode::Full,
|
|
);
|
|
|
|
assert!(result.is_ok());
|
|
let script_path = result.unwrap();
|
|
let content = std::fs::read_to_string(&script_path).unwrap();
|
|
assert!(content.contains("echo 'hello world'"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_script_off_mode() {
|
|
let temp_dir = tempfile::tempdir().unwrap();
|
|
let function = Function {
|
|
name: "test".to_string(),
|
|
decorators: vec![],
|
|
body: "echo 'direct output'".to_string(),
|
|
};
|
|
|
|
let result = build_script(
|
|
&function,
|
|
Path::new("/nonexistent"),
|
|
temp_dir.path(),
|
|
RenderMode::Off,
|
|
);
|
|
|
|
assert!(result.is_ok());
|
|
let script_path = result.unwrap();
|
|
let content = std::fs::read_to_string(&script_path).unwrap();
|
|
assert_eq!(content.trim(), "echo 'direct output'");
|
|
}
|
|
}
|