1use std::ffi::OsString;
2
3use std::path::{Path, PathBuf};
4
5use clap::error::ErrorKind;
6use clap::{Args, Command, CommandFactory, Error, FromArgMatches, Parser};
7
8use indicatif::ProgressStyle;
9use itertools::Itertools;
10use merge::Merge;
11
12use assemble_core::logging::LoggingArgs;
13use assemble_core::prelude::BacktraceEmit;
14use assemble_core::project::error::ProjectResult;
15use assemble_core::project::requests::TaskRequests;
16use assemble_core::project::shared::SharedProject;
17
18use crate::ProjectProperties;
19
20#[derive(Debug, Parser, Clone, Merge)]
31#[clap(name = "assemble")]
32#[clap(version, author)]
33#[clap(before_help = format!("{} v{}", clap::crate_name!(), clap::crate_version!()))]
34#[clap(after_help = "For project specific information, use the :help task.")]
35#[clap(term_width = 64)]
36pub struct FreightArgs {
37 #[clap(flatten)]
39 properties: ProjectProperties,
40 #[clap(flatten)]
42 logging: LoggingArgs,
43
44 #[clap(long, short = 'J')]
48 #[clap(help_heading = None)]
49 #[clap(value_parser = clap::value_parser!(u32).range(1..))]
50 workers: Option<u32>,
51 #[clap(long)]
53 #[clap(conflicts_with = "workers")]
54 #[clap(help_heading = None)]
55 #[merge(strategy = merge::bool::overwrite_false)]
56 no_parallel: bool,
57
58 #[clap(short = 'b', long)]
60 #[clap(help_heading = None)]
61 #[merge(strategy = merge::bool::overwrite_false)]
62 backtrace: bool,
63
64 #[clap(short = 'B', long)]
66 #[clap(help_heading = None)]
67 #[merge(strategy = merge::bool::overwrite_false)]
68 #[clap(conflicts_with = "backtrace")]
69 long_backtrace: bool,
70
71 #[clap(long)]
73 #[clap(help_heading = None)]
74 #[merge(strategy = merge::bool::overwrite_false)]
75 rerun_tasks: bool,
76
77 #[clap(flatten)]
78 bare_task_requests: TaskRequestsArgs,
79}
80
81#[derive(Debug, Clone, Args, merge::Merge)]
82struct TaskRequestsArgs {
83 #[clap(value_name = "TASK [TASK OPTIONS]...")]
85 #[clap(help_heading = "Tasks")]
87 #[merge(strategy = merge::vec::append)]
88 requests: Vec<String>,
89}
90
91impl<S: AsRef<str>> FromIterator<S> for TaskRequestsArgs {
92 fn from_iter<T: IntoIterator<Item = S>>(iter: T) -> Self {
93 Self {
94 requests: iter.into_iter().map(|s| s.as_ref().to_string()).collect(),
95 }
96 }
97}
98
99impl TaskRequestsArgs {
100 fn requests(&self) -> &Vec<String> {
101 &self.requests
102 }
103}
104
105impl FreightArgs {
106 pub fn command_line<S: AsRef<str>>(cmd: S) -> Self {
108 Self::try_command_line(cmd).expect("Couldn't parse cmd line")
109 }
110
111 pub fn try_command_line<S: AsRef<str>>(cmd: S) -> Result<Self, Error> {
113 Self::try_parse(cmd.as_ref().split_whitespace())
114 }
115
116 pub fn from_env() -> Self {
118 match Self::try_parse(std::env::args_os().skip(1)) {
119 Ok(s) => s,
120 Err(e) => {
121 e.exit();
122 }
123 }
124 }
125
126 fn try_parse<S, I: IntoIterator<Item = S>>(iter: I) -> Result<Self, clap::Error>
127 where
128 S: Into<OsString>,
129 {
130 let mut parsed_freight_args: FreightArgs = Parser::parse_from([""]);
131 let empty = OsString::from("");
132
133 let mut index = 0;
134 let mut window_size = 1;
135
136 let args: Vec<OsString> = iter.into_iter().map(|s: S| s.into()).collect();
137 let mut last_error = None;
138
139 let mut parsed_args = vec![&empty];
140
141 while index + window_size <= args.len() {
142 let mut arg_window = Vec::from_iter(&args[index..][..window_size]);
143 arg_window.insert(0, &empty);
144
145 let intermediate = <FreightArgs as Parser>::try_parse_from(&arg_window);
146
147 match intermediate {
148 Ok(arg_matches) => {
149 parsed_freight_args.merge(arg_matches);
150 parsed_args.extend(arg_window);
151
152 <FreightArgs as Parser>::try_parse_from(&parsed_args)?;
153 index += window_size;
154 window_size = 1;
155 }
156 Err(e) => {
157 last_error = if e.kind() == ErrorKind::UnknownArgument {
158 if parsed_freight_args.bare_task_requests.requests.is_empty() {
160 Some(e);
161 break;
162 } else {
163 parsed_freight_args.bare_task_requests.requests.extend(
164 arg_window
165 .drain(1..)
166 .map(|s| s.to_str().unwrap().to_string()),
167 );
168 index += 1;
169 Some(e)
170 }
171 } else if e.kind() == ErrorKind::InvalidValue {
172 window_size += 1;
173 Some(e)
174 } else {
175 return Err(e);
176 }
177 }
178 }
179 }
180
181 if index == args.len() {
182 Ok(parsed_freight_args)
183 } else if let Some(e) = last_error {
184 Err(e)
185 } else {
186 let mut command: Command = FreightArgs::command();
187 Err(
188 Error::raw(ErrorKind::UnknownArgument, "failed for unknown reason")
189 .format(&mut command),
190 )
191 }
192 }
193
194 pub fn task_requests(&self, project: &SharedProject) -> ProjectResult<TaskRequests> {
196 TaskRequests::build(project, self.bare_task_requests.requests())
197 }
198
199 pub fn task_requests_raw(&self) -> &[String] {
201 &self.bare_task_requests.requests[..]
202 }
203
204 pub fn with_tasks<'s, I: IntoIterator<Item = &'s str>>(&self, iter: I) -> FreightArgs {
206 let mut clone = self.clone();
207 clone.bare_task_requests = iter.into_iter().map(|s| s.to_string()).collect();
208 clone
209 }
210
211 pub fn property(&self, key: impl AsRef<str>) -> Option<&str> {
213 self.properties.property(key)
214 }
215
216 pub fn logging(&self) -> &LoggingArgs {
218 &self.logging
219 }
220
221 pub fn workers(&self) -> usize {
223 if self.no_parallel {
224 1
225 } else {
226 self.workers
227 .map(|w| w as usize)
228 .unwrap_or_else(num_cpus::get)
229 }
230 }
231
232 pub fn backtrace(&self) -> BacktraceEmit {
234 match (self.backtrace, self.long_backtrace) {
235 (true, false) => BacktraceEmit::Short,
236 (_, true) => BacktraceEmit::Long,
237 _ => BacktraceEmit::None,
238 }
239 }
240
241 pub fn rerun_tasks(&self) -> bool {
243 self.rerun_tasks
244 }
245 pub fn properties(&self) -> &ProjectProperties {
246 &self.properties
247 }
248}
249
250pub fn main_progress_bar_style(failing: bool) -> ProgressStyle {
251 let template = if failing {
252 "{msg:>12.cyan.bold} [{bar:25.red.bright} {percent:>3}% ({pos}/{len})] elapsed: {elapsed}"
253 } else {
254 "{msg:>12.cyan.bold} [{bar:25.green.bright} {percent:>3}% ({pos}/{len})] elapsed: {elapsed}"
255 };
256 ProgressStyle::with_template(template)
257 .unwrap()
258 .progress_chars("=> ")
259}
260
261#[cfg(test)]
262mod test {
263 use assemble_core::logging::ConsoleMode;
264 use clap::{Command, CommandFactory};
265 use log::LevelFilter;
266
267 use crate::cli::FreightArgs;
268
269 #[test]
270 fn can_render_help() {
271 let mut freight_command: Command = FreightArgs::command();
272 let str = freight_command.render_help();
273 println!("{}", str);
274 }
275
276 #[test]
277 fn no_parallel() {
278 let args: FreightArgs = FreightArgs::command_line("--no-parallel");
279 println!("{:#?}", args);
280 assert!(args.no_parallel);
281 assert_eq!(args.workers(), 1);
282 }
283
284 #[test]
285 fn arbitrary_workers() {
286 let args: FreightArgs = FreightArgs::command_line("--workers 13");
287 println!("{:#?}", args);
288 assert_eq!(args.workers(), 13);
289 }
290
291 #[test]
292 fn default_workers_is_num_cpus() {
293 let args: FreightArgs = FreightArgs::command_line("");
294 assert_eq!(args.workers(), num_cpus::get());
295 }
296
297 #[test]
298 fn zero_workers_illegal() {
299 assert!(
300 FreightArgs::try_command_line("-J 0").is_err(),
301 "0 workers is illegal, but error wasn't properly detected"
302 );
303 }
304
305 #[test]
306 fn workers_and_no_parallel_conflicts() {
307 assert!(FreightArgs::try_command_line("-J 2 --no-parallel").is_err());
308 }
309
310 #[test]
311 fn can_set_project_properties() {
312 let args = FreightArgs::command_line("-P hello=world -P key1 -P key2");
313 assert_eq!(args.property("hello"), Some("world"));
314 assert_eq!(args.property("key1"), Some(""));
315 assert_eq!(args.property("key2"), Some(""));
316 }
317
318 #[test]
319 fn arbitrary_task_positions() {
320 let args = FreightArgs::try_command_line(":tasks --all --debug --workers 6 help");
321 if args.is_err() {
322 eprintln!("{}", args.unwrap_err());
323 panic!("Couldn't parse");
324 }
325 let args = args.unwrap();
326 println!("args: {:#?}", args);
327 assert_eq!(
328 args.logging.log_level_filter(),
329 LevelFilter::Debug,
330 "debug log level not set"
331 );
332 assert_eq!(args.workers(), 6, "should set 6 workers");
333 assert_eq!(args.bare_task_requests.requests().len(), 3);
334 assert_eq!(
335 &args.bare_task_requests.requests()[..2],
336 &[":tasks", "--all"],
337 "first task request is tasks --all"
338 );
339 assert_eq!(
340 args.bare_task_requests.requests()[2],
341 "help",
342 "second task request is help"
343 );
344 }
345
346 #[test]
347 fn tasks_last() {
348 let args = FreightArgs::command_line("--debug --workers 6 -- :tasks --all help");
349 println!("args: {:#?}", args);
350 assert_eq!(
351 args.logging.log_level_filter(),
352 LevelFilter::Debug,
353 "debug log level not set"
354 );
355 assert_eq!(args.workers(), 6, "should set 6 workers");
356 assert_eq!(args.bare_task_requests.requests().len(), 3);
357 assert_eq!(
358 &args.bare_task_requests.requests()[..2],
359 &[":tasks", "--all"],
360 "first task request is tasks --all"
361 );
362 assert_eq!(
363 args.bare_task_requests.requests()[2],
364 "help",
365 "second task request is help"
366 );
367 }
368
369 #[test]
370 fn disallow_bare_unexpected_option() {
371 assert!(FreightArgs::try_command_line("--all").is_err());
372 }
373
374 #[test]
375 fn allow_default_tasks() {
376 let args = FreightArgs::try_command_line("--trace --workers 6 --console plain").unwrap();
377 assert_eq!(args.logging().log_level_filter(), LevelFilter::Trace);
378 assert_eq!(args.workers(), 6);
379 assert_eq!(args.logging().console, ConsoleMode::Plain);
380 }
381
382 #[test]
383 fn disallow_multiple_logging() {
384 assert!(FreightArgs::try_command_line("--trace --debug").is_err());
385 }
386}