Skip to main content

bake/
registry.rs

1use crate::{Arguments, Context, Error, Format, Result, Task, output};
2use std::collections::{BTreeMap, BTreeSet};
3use std::io::{self, Write};
4use std::path::PathBuf;
5use std::sync::Arc;
6
7/// Task registry with deterministic listing and collision errors.
8pub struct Registry {
9    pub(crate) tasks: BTreeMap<String, Task>,
10}
11
12pub(crate) struct Invocation {
13    pub(crate) name: String,
14    pub(crate) arguments: Arguments,
15}
16
17/// A task descriptor registered by `#[bake::task]` in a linked crate.
18#[doc(hidden)]
19pub struct TaskRegistration {
20    pub factory: fn() -> Task,
21    pub module_path: &'static str,
22    pub builtin: bool,
23}
24
25#[linkme::distributed_slice]
26pub static TASK_REGISTRATIONS: [TaskRegistration];
27
28fn valid_name(name: &str) -> bool {
29    !name.is_empty()
30        && name.split(':').all(|part| {
31            part.chars()
32                .next()
33                .is_some_and(|character| character.is_ascii_alphanumeric() || character == '_')
34                && part.chars().all(|character| {
35                    character.is_ascii_alphanumeric() || character == '_' || character == '-'
36                })
37        })
38}
39
40impl Registry {
41    pub fn new() -> Self {
42        let mut registry = Self {
43            tasks: BTreeMap::new(),
44        };
45        for task in output::builtins() {
46            registry.tasks.insert(task.name.clone(), task);
47        }
48        registry
49    }
50
51    /// Discover task functions registered by `#[bake::task]` in this executable
52    /// and its linked dependencies. Nested Rust modules become task namespaces.
53    ///
54    /// A dependency that contributes tasks must be referenced by the executable
55    /// (for example, `use bake_releases as _;`) so the linker includes it.
56    pub fn discover() -> Result<Self> {
57        let mut registry = Self::new();
58        for registration in TASK_REGISTRATIONS {
59            if registration.builtin {
60                continue;
61            }
62            let mut task = (registration.factory)();
63            namespace_from_module(&mut task, registration.module_path);
64            registry.register(task)?;
65        }
66        Ok(registry)
67    }
68
69    pub fn register(&mut self, task: Task) -> Result<&mut Self> {
70        validate_task(&task)?;
71        if self.tasks.contains_key(&task.name) {
72            return Err(Error::new(format!("duplicate task {:?}", task.name)));
73        }
74        self.tasks.insert(task.name.clone(), task);
75        Ok(self)
76    }
77
78    /// Replace an existing task explicitly, most often the default `output` task.
79    pub fn replace(&mut self, name: &str, task: Task) -> Result<&mut Self> {
80        validate_task(&task)?;
81        if task.name != name {
82            return Err(Error::new(format!(
83                "replacement task name {:?} does not match {name:?}",
84                task.name
85            )));
86        }
87        if !self.tasks.contains_key(name) {
88            return Err(Error::new(format!("cannot replace unknown task {name:?}")));
89        }
90        let task = if name == "output" {
91            task.handles_output()
92        } else {
93            task
94        };
95        self.tasks.insert(name.to_owned(), task);
96        Ok(self)
97    }
98
99    pub fn include(&mut self, namespace: &str, other: Registry) -> Result<&mut Self> {
100        if !namespace.is_empty() && !valid_name(namespace) {
101            return Err(Error::new("invalid task namespace"));
102        }
103        let tasks: Vec<_> = other
104            .tasks
105            .into_values()
106            .filter(|task| !task.builtin)
107            .map(|mut task| {
108                if !namespace.is_empty() {
109                    task.name = format!("{namespace}:{}", task.name);
110                }
111                task
112            })
113            .collect();
114        for task in &tasks {
115            if self.tasks.contains_key(&task.name) {
116                return Err(Error::new(format!("duplicate task {:?}", task.name)));
117            }
118        }
119        for task in tasks {
120            self.tasks.insert(task.name.clone(), task);
121        }
122        Ok(self)
123    }
124
125    pub fn tasks(&self) -> impl Iterator<Item = &Task> {
126        self.tasks.values()
127    }
128
129    /// Create an isolated execution context with this registry and a project root.
130    pub fn context(self, root: impl Into<PathBuf>) -> Context {
131        Context::new(root.into(), Arc::new(self))
132    }
133
134    pub(crate) fn plan(&self, tokens: &[String]) -> Result<Vec<Invocation>> {
135        let mut invocations = Vec::new();
136        let mut position = 0;
137        while let Some(name) = tokens.get(position) {
138            if name == "::" {
139                return Err(Error::new("expected a task name, found a chain separator"));
140            }
141            let task = self.tasks.get(name).ok_or_else(|| {
142                Error::new(format!(
143                    "unknown task {name:?}; use --list to see available tasks"
144                ))
145            })?;
146            let (arguments, consumed) =
147                Arguments::extract(&task.parameters, &tokens[position + 1..])
148                    .map_err(|error| Error::new(format!("{name}: {error}")))?;
149            invocations.push(Invocation {
150                name: name.clone(),
151                arguments,
152            });
153            position += 1 + consumed;
154            if tokens.get(position).is_some_and(|token| token == "::") {
155                position += 1;
156                if position == tokens.len() {
157                    return Err(Error::new("expected a task after ::"));
158                }
159            }
160        }
161        Ok(invocations)
162    }
163
164    pub fn help(&self, name: Option<&str>) -> Result<String> {
165        if let Some(name) = name {
166            let task = self
167                .tasks
168                .get(name)
169                .ok_or_else(|| Error::new(format!("unknown task {name:?}")))?;
170            let mut text = format!("{}\n\n{}\n\nArguments:\n", task.name, task.description);
171            for parameter in &task.parameters {
172                let kind = if parameter.positional {
173                    "positional"
174                } else {
175                    "named"
176                };
177                let display_name = if parameter.positional {
178                    parameter.name.clone()
179                } else {
180                    format!("--{} value", parameter.name.replace('_', "-"))
181                };
182                let requirement = if parameter.required {
183                    "required"
184                } else {
185                    "optional"
186                };
187                let default = parameter
188                    .default
189                    .as_ref()
190                    .map(|value| format!(", default: {value}"))
191                    .unwrap_or_default();
192                let repeated = if parameter.repeated {
193                    ", repeatable"
194                } else {
195                    ""
196                };
197                text.push_str(&format!(
198                    "  {}: {} ({kind}, {requirement}{default}{repeated})\n",
199                    display_name, parameter.type_name
200                ));
201                if !parameter.description.is_empty() {
202                    text.push_str(&format!("    {}\n", parameter.description));
203                }
204            }
205            Ok(text)
206        } else {
207            let mut text = String::from("Tasks:\n");
208            for task in self.tasks.values() {
209                text.push_str(&format!(
210                    "  {:24} {}\n",
211                    task.name,
212                    task.description.lines().next().unwrap_or_default()
213                ));
214            }
215            text.push_str("\nUse TASK --help for arguments. Chain tasks with ::.\n");
216            Ok(text)
217        }
218    }
219
220    /// Run the process command line and print its final result through `output`.
221    pub fn run(self) -> Result<()> {
222        let tokens: Vec<_> = std::env::args_os()
223            .skip(1)
224            .map(|token| {
225                token
226                    .into_string()
227                    .map_err(|_| Error::new("task arguments must be UTF-8"))
228            })
229            .collect::<Result<_>>()?;
230        let root = std::env::var_os("BAKE_PROJECT_ROOT")
231            .map(PathBuf::from)
232            .map_or_else(std::env::current_dir, Ok)?;
233        let output = self.run_arguments(root, &tokens)?;
234        io::stdout().lock().write_all(output.as_bytes())?;
235        Ok(())
236    }
237
238    /// Execute a command line without writing directly to stdout. Parsing and
239    /// type validation finish before any task starts. Tasks may write an output
240    /// file or return captured output.
241    pub fn run_arguments(self, root: impl Into<PathBuf>, tokens: &[String]) -> Result<String> {
242        let (format, tokens) = if tokens.first().is_some_and(|token| token == "--json") {
243            (Some(Format::Json), &tokens[1..])
244        } else {
245            (None, tokens)
246        };
247        if tokens.is_empty() || matches!(tokens, [flag] if flag == "--list" || flag == "--help") {
248            return self.help(None);
249        }
250        if let [name, flag] = tokens
251            && flag == "--help"
252        {
253            return self.help(Some(name));
254        }
255        let invocations = self.plan(tokens)?;
256        let final_task_handles_output = invocations
257            .last()
258            .and_then(|invocation| self.tasks.get(&invocation.name))
259            .is_some_and(Task::produces_output);
260        let mut context = self.context(root);
261        context.set_default_format(format);
262        for invocation in invocations {
263            context.invoke(invocation)?;
264        }
265        if !final_task_handles_output {
266            context.invoke(Invocation {
267                name: "output".into(),
268                arguments: Arguments::default(),
269            })?;
270        }
271        Ok(context.take_output())
272    }
273}
274
275fn namespace_from_module(task: &mut Task, module_path: &str) {
276    if task.name.contains(':') {
277        return;
278    }
279
280    let namespace = module_path
281        .split("::")
282        .skip(1)
283        .map(|component| component.replace('_', "-"))
284        .collect::<Vec<_>>()
285        .join(":");
286
287    if !namespace.is_empty() {
288        task.name = format!("{namespace}:{}", task.name);
289    }
290}
291
292impl Default for Registry {
293    fn default() -> Self {
294        Self::new()
295    }
296}
297
298fn validate_task(task: &Task) -> Result<()> {
299    if !valid_name(&task.name) {
300        return Err(Error::new(format!("invalid task name {:?}", task.name)));
301    }
302    let mut parameters = BTreeSet::new();
303    for parameter in &task.parameters {
304        if parameter.name.is_empty()
305            || !parameter
306                .name
307                .chars()
308                .all(|character| character.is_ascii_alphanumeric() || character == '_')
309        {
310            return Err(Error::new(format!(
311                "invalid parameter name {:?}",
312                parameter.name
313            )));
314        }
315        if !parameters.insert(&parameter.name) {
316            return Err(Error::new(format!(
317                "duplicate parameter {:?}",
318                parameter.name
319            )));
320        }
321    }
322    Ok(())
323}