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