1#![deny(clippy::print_stdout)]
2mod dorsfile;
3mod error;
4mod take_while_ext;
5
6pub use crate::error::{DorsError, Error};
7
8use cargo_metadata::MetadataCommand;
9use colored::Colorize;
10use dorsfile::{Dorsfile, MemberModifiers, Run};
11use std::collections::{HashMap, HashSet};
12use std::path::{Path, PathBuf};
13use std::process::Command;
14use std::process::ExitStatus;
15use take_while_ext::TakeWhileLastExt;
16
17#[derive(Debug)]
18struct DorsfileGetter {
19 workspace_root: PathBuf,
20 workspace_dorsfile: Option<Dorsfile>,
21}
22impl DorsfileGetter {
23 pub fn new<P: AsRef<Path>>(workspace_root: P) -> Result<DorsfileGetter, Box<dyn Error>> {
24 let workspace_dorsfile_path = workspace_root.as_ref().join("./Dorsfile.toml");
25 Ok(DorsfileGetter {
26 workspace_root: workspace_root.as_ref().into(),
27 workspace_dorsfile: if workspace_dorsfile_path.exists() {
28 Some(Dorsfile::load(&workspace_dorsfile_path)?)
29 } else {
30 None
31 },
32 })
33 }
34
35 pub fn get<P: AsRef<Path>>(&self, crate_path: P) -> Result<Dorsfile, Box<dyn Error>> {
36 let crate_path = crate_path.as_ref();
37 if crate_path.canonicalize().unwrap() == self.workspace_root.canonicalize().unwrap() {
38 return Ok(self
39 .workspace_dorsfile
40 .as_ref()
41 .cloned()
42 .ok_or(DorsError::NoDorsfile)?);
43 }
44 let local = crate_path.join("./Dorsfile.toml");
45
46 let mut dorsfile = match (local.exists(), self.workspace_dorsfile.is_some()) {
47 (true, true) => {
48 let mut curr = Dorsfile::load(local)?;
50 let workspace_dorsfile = self.workspace_dorsfile.as_ref().unwrap();
51 let mut env = workspace_dorsfile.env.clone();
52 let mut task = workspace_dorsfile.task.clone();
53
54 task.values_mut().for_each(|task| {
55 task.before = None;
58 task.after = None;
59
60 if let Run::Members = task.run_from {
63 task.run_from = Run::Here;
64 }
65 });
66
67 env.extend(curr.env.drain(..));
68 task.extend(curr.task.drain());
69 curr.env = env;
70 curr.task = task;
71 curr
72 }
73 (true, false) => Dorsfile::load(local)?,
74 (false, true) => {
75 let mut curr = self.workspace_dorsfile.as_ref().cloned().unwrap();
76
77 let mut task = curr.task.clone();
78 task.values_mut().for_each(|task| {
79 task.before = None;
82 task.after = None;
83
84 if let Run::Members = task.run_from {
87 task.run_from = Run::Here;
88 }
89 });
90
91 curr.task = task;
92 curr
93 }
94 (false, false) => return Err(DorsError::NoMemberDorsfile.into()),
95 };
96
97 let builtins: HashMap<_, _> = [(
99 "CARGO_WORKSPACE_ROOT",
100 self.workspace_root.to_str().unwrap(),
101 )]
102 .iter()
103 .cloned()
104 .map(|(key, value)| (key.to_string(), value.to_string()))
105 .collect();
106 let mut env = vec![builtins];
107 env.extend(dorsfile.env.drain(..));
108 dorsfile.env = env;
109 Ok(dorsfile)
110 }
111}
112
113struct CargoWorkspaceInfo {
114 members: HashMap<String, PathBuf>,
115 root: PathBuf,
116}
117
118impl CargoWorkspaceInfo {
119 fn new(dir: &Path) -> CargoWorkspaceInfo {
120 let metadata = MetadataCommand::new().current_dir(&dir).exec().unwrap();
121 let root = metadata.workspace_root;
122 let packages: HashMap<_, _> = metadata
124 .packages
125 .iter()
126 .map(|package| (package.id.clone(), package))
127 .collect();
128 let members = metadata
129 .workspace_members
130 .into_iter()
131 .map(|member| {
132 let package = packages[&member];
133 (
134 package.name.clone(),
135 package.manifest_path.parent().unwrap().into(),
136 )
137 })
138 .collect();
139 CargoWorkspaceInfo { members, root }
140 }
141}
142
143fn run_command(
144 command: &str,
145 workdir: &Path,
146 env: &[HashMap<String, String>],
147 args: &[String],
148) -> ExitStatus {
149 use rand::distributions::Alphanumeric;
150 use rand::{thread_rng, Rng};
151 use std::iter;
152 let mut rng = thread_rng();
153 let chars: String = iter::repeat(())
154 .map(|()| rng.sample(Alphanumeric))
155 .take(10)
156 .collect();
157 let file = Path::new("./")
158 .canonicalize()
159 .unwrap()
160 .join(format!("tmp-{}.sh", chars));
161 let mut script = env
162 .iter()
163 .flatten()
164 .fold("".to_string(), |mut acc, (k, v)| {
165 acc.push_str(&format!("export {}={}\n", k, v));
166 acc
167 });
168 script.push_str(command);
169 script.push_str("\n");
170 std::fs::write(&file, &script).unwrap();
171 let exit_status = Command::new("bash")
172 .arg("-e")
173 .arg(file.to_str().unwrap())
174 .args(args)
175 .current_dir(workdir)
176 .spawn()
177 .unwrap()
178 .wait()
179 .unwrap();
180 std::fs::remove_file(file).unwrap();
181 exit_status
182}
183
184pub fn all_tasks<P: AsRef<Path>>(dir: P) -> Result<Vec<String>, Box<dyn Error>> {
185 let workspace = CargoWorkspaceInfo::new(dir.as_ref());
186 let dorsfiles = DorsfileGetter::new(&workspace.root)?;
187 Ok(dorsfiles
188 .get(dir.as_ref())?
189 .task
190 .keys()
191 .cloned()
192 .collect::<Vec<_>>())
193}
194
195fn print_task(task_name: &str, path: &Path) {
196 eprintln!(
198 " {} Running {} from `{}`",
199 "[Dors]".yellow().bold(),
200 task_name.bold(),
201 path.to_str().unwrap().bold()
202 );
203}
204
205pub fn run<P: AsRef<Path>>(task: &str, dir: P) -> Result<ExitStatus, Box<dyn Error>> {
206 run_with_args(task, dir, &[])
207}
208
209struct TaskRunner {
210 workspace: CargoWorkspaceInfo,
211 dorsfiles: DorsfileGetter,
212}
213
214pub fn run_with_args<P: AsRef<Path>>(
215 task: &str,
216 dir: P,
217 args: &[String],
218) -> Result<ExitStatus, Box<dyn Error>> {
219 let dir = dir.as_ref();
220 let workspace = CargoWorkspaceInfo::new(&dir);
221 let dorsfiles = DorsfileGetter::new(&workspace.root)?;
222 let dorsfile = dorsfiles.get(&dir)?;
223
224 TaskRunner {
225 workspace,
226 dorsfiles,
227 }
228 .run_task(
230 task,
231 &dorsfile,
232 &dir,
233 args,
234 &mut HashSet::new(),
235 &mut HashSet::new(),
236 )
237}
238
239impl TaskRunner {
240 fn run_task(
241 &self,
242 task_name: &str,
243 dorsfile: &Dorsfile,
244 dir: &Path,
245 args: &[String],
246 already_ran_befores: &mut HashSet<String>,
247 already_ran_afters: &mut HashSet<String>,
248 ) -> Result<ExitStatus, Box<dyn Error>> {
249 let task = dorsfile
250 .task
251 .get(task_name)
252 .ok_or_else(|| DorsError::NoTask(task_name.to_string()))?;
253
254 if let Some(ref befores) = task.before {
256 if let Some(befores_result) = befores
257 .iter()
258 .filter_map(|before_task_name| {
259 if !already_ran_befores.contains(before_task_name) {
260 already_ran_befores.insert(before_task_name.into());
261 Some(self.run_task(
262 before_task_name,
263 dorsfile,
264 dir,
265 &[],
266 already_ran_befores,
267 &mut HashSet::new(),
268 ))
269 } else {
270 None
271 }
272 })
273 .take_while_last(|result| result.is_ok() && result.as_ref().unwrap().success())
274 .last()
275 {
276 if befores_result.is_err() || !befores_result.as_ref().unwrap().success() {
277 return befores_result;
278 }
279 }
280 }
281
282 let result = match task.run_from {
284 Run::Here => {
285 print_task(task_name, &dir);
286 run_command(&task.command, dir, &dorsfile.env, args)
287 }
288 Run::WorkspaceRoot => {
289 let path = &self.workspace.root;
291 print_task(task_name, path);
292 run_command(&task.command, path, &dorsfile.env, args)
293 }
294 Run::Members => {
295 if dir.canonicalize().unwrap() != self.workspace.root.canonicalize().unwrap() {
296 panic!("cannot run from members from outside workspace root");
297 }
298 self.workspace
299 .members
300 .iter()
301 .filter_map(|(name, path)| {
302 let short_path = if path.is_relative() {
303 path
304 } else {
305 path.strip_prefix(&self.workspace.root).unwrap()
306 };
307 match task.member_modifiers {
308 Some(ref modifiers) => match modifiers {
309 MemberModifiers::SkipMembers(skips) => {
310 if skips.contains(name)
311 || skips.contains(&short_path.to_str().unwrap().to_string())
312 {
313 None
314 } else {
315 Some(path)
316 }
317 }
318 MemberModifiers::OnlyMembers(onlys) => {
319 if onlys.contains(name)
320 || onlys.contains(&short_path.to_str().unwrap().to_string())
321 {
322 Some(path)
323 } else {
324 None
325 }
326 }
327 },
328 None => Some(path),
329 }
330 })
331 .map(|path| {
332 let dorsfile = self.dorsfiles.get(&path)?;
333 self.run_task(
334 task_name,
335 &dorsfile,
336 &path,
337 args,
338 &mut HashSet::new(),
339 &mut HashSet::new(),
340 )
341 })
342 .take_while_last(|result| result.is_ok() && result.as_ref().unwrap().success())
343 .last()
344 .unwrap()?
345 }
346 Run::Path(ref target_path) => {
347 print_task(task_name, &target_path);
348 run_command(&task.command, &dir.join(target_path), &dorsfile.env, args)
349 }
350 };
351
352 if !result.success() {
353 return Ok(result);
354 }
355
356 if let Some(ref afters) = task.after {
358 if let Some(afters_result) = afters
359 .iter()
360 .filter_map(|after_task_name| {
361 if !already_ran_afters.contains(after_task_name) {
362 already_ran_afters.insert(after_task_name.into());
363 Some(self.run_task(
364 after_task_name,
365 dorsfile,
366 dir,
367 &[],
368 &mut HashSet::new(),
369 already_ran_afters,
370 ))
371 } else {
372 None
373 }
374 })
375 .take_while_last(|result| result.is_ok() && result.as_ref().unwrap().success())
376 .last()
377 {
378 if afters_result.is_err() || !afters_result.as_ref().unwrap().success() {
379 return afters_result;
380 }
381 }
382 }
383
384 Ok(result)
385 }
386}
387
388#[allow(clippy::print_stdout)]
389pub fn process_cmd<'a>(matches: &clap::ArgMatches<'a>) -> i32 {
390 let directory = match matches.value_of("subdirectory") {
391 Some(directory) => directory.into(),
392 None => std::env::current_dir().unwrap(),
393 };
394 if matches.is_present("list") {
395 let mut tasks = match all_tasks(directory) {
396 Ok(tasks) => tasks,
397 Err(e) => {
398 println!("{}", e);
399 return 1;
400 }
401 };
402 tasks.sort();
403 tasks.iter().for_each(|task| println!("{}", task));
404 return 0;
405 }
406
407 if matches.is_present("completions") {
408 println!(r#"complete -C "cargo dors -l" cargo dors"#);
409 println!(r#"complete -C "cargo dors -l" dors"#);
410 return 0;
411 }
412
413 if let Some(task) = matches.value_of("TASK") {
414 let args = match matches.values_of("TASK_ARGS") {
415 Some(values) => values.map(|s| s.to_string()).collect(),
416 None => vec![],
417 };
418 match run_with_args(&task, directory, &args) {
419 Ok(resp) => return resp.code().unwrap(),
420 Err(e) => {
421 println!("{}", e);
422 return 1;
423 }
424 }
425 }
426
427 let mut tasks = match all_tasks(directory) {
428 Ok(tasks) => tasks,
429 Err(e) => {
430 println!("{}", e);
431 return 1;
432 }
433 };
434 tasks.sort();
435
436 if matches.is_present("list") {
437 tasks.iter().for_each(|task| println!("{}", task));
438 return 0;
439 }
440
441 println!("{}: Please select a task to run:", "Error".red());
442 tasks.iter().for_each(|task| println!("{}", task.bold()));
443 1
444}
445
446pub fn get_about() -> &'static str {
447 "No-fuss workspace-aware task runner for rust"
448}
449
450pub fn set_app_options<'a, 'b>(app: clap::App<'a, 'b>) -> clap::App<'a, 'b> {
451 app.version(env!("CARGO_PKG_VERSION"))
452 .author("Andrew Klitzke <andrewknpe@gmail.com>")
453 .setting(clap::AppSettings::TrailingVarArg)
454 .setting(clap::AppSettings::ColoredHelp)
455 .setting(clap::AppSettings::DontCollapseArgsInUsage)
456 .about(get_about())
457 .arg(
458 clap::Arg::with_name("subdirectory")
459 .short("d")
460 .long("subdirectory")
461 .conflicts_with_all(&["completions"])
462 .display_order(0)
463 .takes_value(true)
464 .help("only run task on a specified subdirectory"),
465 )
466 .arg(
467 clap::Arg::with_name("list")
468 .short("l")
469 .long("list")
470 .conflicts_with_all(&["TASK", "TASK_ARGS", "completions"])
471 .display_order(1)
472 .help("list all the available tasks"),
473 )
474 .arg(
475 clap::Arg::with_name("completions")
476 .long("completions")
477 .help(
478 "Generate bash/zsh completions. Install once, and will automatically \
479 update when Dorsfiles are modified. Usually added to \
480 .bashrc or similar file to take effect. See `rustup completions`\
481 for a detailed explanation of how to install the output of this
482 command",
483 ),
484 )
485 .arg(clap::Arg::with_name("TASK").help("the name of the task to run"))
486 .arg(
487 clap::Arg::with_name("TASK_ARGS")
488 .help("arguments to pass to the task")
489 .requires("TASK")
490 .multiple(true),
491 )
492}