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
7pub 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#[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 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 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 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 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 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(¶meter.name) {
316 return Err(Error::new(format!(
317 "duplicate parameter {:?}",
318 parameter.name
319 )));
320 }
321 }
322 Ok(())
323}