Skip to main content

bake/
context.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4use crate::output::Format;
5use crate::registry::Invocation;
6use crate::{Error, Registry, Result, Value};
7use std::any::{Any, TypeId};
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use std::process::Command;
11use std::sync::Arc;
12
13/// Execution state shared by a chain of tasks and their nested calls.
14///
15/// A context is synchronous and confined to its caller. No global current
16/// directory or environment changes are needed for task execution.
17pub struct Context {
18    root: PathBuf,
19    registry: Arc<Registry>,
20    previous: Value,
21    state: HashMap<TypeId, Box<dyn Any>>,
22    depth: usize,
23    terminal_output: String,
24    default_format: Option<Format>,
25}
26
27impl Context {
28    pub(crate) fn new(root: PathBuf, registry: Arc<Registry>) -> Self {
29        Self {
30            root,
31            registry,
32            previous: Value::Null,
33            state: HashMap::new(),
34            depth: 0,
35            terminal_output: String::new(),
36            default_format: None,
37        }
38    }
39    pub fn root(&self) -> &Path {
40        &self.root
41    }
42    pub fn previous(&self) -> &Value {
43        &self.previous
44    }
45    /// Add user-facing output to the command result. Task diagnostics should use stderr.
46    pub fn write_output(&mut self, output: &str) {
47        self.terminal_output.push_str(output);
48    }
49    pub(crate) fn take_output(&mut self) -> String {
50        std::mem::take(&mut self.terminal_output)
51    }
52    pub(crate) fn set_default_format(&mut self, format: Option<Format>) {
53        self.default_format = format;
54    }
55    pub(crate) fn default_format(&self) -> Option<Format> {
56        self.default_format
57    }
58    pub fn insert<State: Any>(&mut self, state: State) {
59        self.state.insert(TypeId::of::<State>(), Box::new(state));
60    }
61    pub fn get<State: Any>(&self) -> Option<&State> {
62        self.state.get(&TypeId::of::<State>())?.downcast_ref()
63    }
64    pub fn get_mut<State: Any>(&mut self) -> Option<&mut State> {
65        self.state.get_mut(&TypeId::of::<State>())?.downcast_mut()
66    }
67
68    /// Start a process in the project root. Arguments go directly to the process;
69    /// shell parsing is only used if a task explicitly invokes a shell.
70    pub fn command(&self, program: impl AsRef<std::ffi::OsStr>) -> Command {
71        let mut command = Command::new(program);
72        command.current_dir(&self.root);
73        command
74    }
75
76    /// Invoke one registered task and store its successful output as `previous`.
77    /// Explicit calls support composition and project-defined hooks.
78    pub fn call(&mut self, name: &str, arguments: &[&str]) -> Result<Value> {
79        let tokens: Vec<_> = std::iter::once(name)
80            .chain(arguments.iter().copied())
81            .map(str::to_owned)
82            .collect();
83        let mut invocations = self.registry.plan(&tokens)?;
84        if invocations.len() != 1 {
85            return Err(Error::new("Context::call accepts exactly one task"));
86        }
87        self.invoke(invocations.remove(0))
88    }
89
90    /// Invoke a registered task if it exists, returning `None` when it is absent.
91    /// Errors from a registered task are returned unchanged.
92    pub fn call_if_registered(&mut self, name: &str, arguments: &[&str]) -> Result<Option<Value>> {
93        if !self.registry.tasks.contains_key(name) {
94            return Ok(None);
95        }
96
97        self.call(name, arguments).map(Some)
98    }
99
100    pub(crate) fn invoke(&mut self, invocation: Invocation) -> Result<Value> {
101        if self.depth >= 64 {
102            return Err(Error::new(
103                "task call depth exceeded 64; check for recursive hooks",
104            ));
105        }
106        let handler = self
107            .registry
108            .tasks
109            .get(&invocation.name)
110            .ok_or_else(|| Error::new(format!("unknown task {:?}", invocation.name)))?
111            .invoke;
112        self.depth += 1;
113        let result = handler(self, &invocation.arguments);
114        self.depth -= 1;
115        let result = result.map_err(|error| Error::new(format!("{}: {error}", invocation.name)))?;
116        self.previous = result.clone();
117        Ok(result)
118    }
119}