Skip to main content

bake/
context.rs

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