1use std::env::{split_paths, var_os};
2use std::fmt::Debug;
3use std::fs;
4use std::path::PathBuf;
5
6use clap::Subcommand;
7use clap::builder::ArgPredicate;
8use clap::{Parser, builder::BoolishValueParser, builder::OsStr};
9
10use crate::config::log_options::LogOptions;
11
12use super::colors::styles;
13
14pub const CARGO_PKG_NAME: &str = "mod_installer";
15
16pub const LONG: &str = r"
17
18 /\/\ ___ __| | (_)_ __ ___| |_ __ _| | | ___ _ __
19 / \ / _ \ / _` | | | '_ \/ __| __/ _` | | |/ _ \ '__|
20/ /\/\ \ (_) | (_| | | | | | \__ \ || (_| | | | __/ |
21\/ \/\___/ \__,_| |_|_| |_|___/\__\__,_|_|_|\___|_|
22";
23
24#[cfg(not(target_os = "windows"))]
25pub const WEIDU_FILE_NAME: &str = "weidu";
26#[cfg(target_os = "windows")]
27pub const WEIDU_FILE_NAME: &str = "weidu.exe";
28
29#[derive(Parser, Debug, PartialEq)]
31#[command(
32 name = CARGO_PKG_NAME,
33 version,
34 propagate_version = true,
35 styles = styles(),
36 about = format!("{}\n{}", LONG, env!("CARGO_PKG_DESCRIPTION"))
37)]
38pub struct Args {
39 #[command(subcommand)]
41 pub command: CommandType,
42}
43
44#[derive(Subcommand, Debug, PartialEq, Clone)]
46pub enum CommandType {
47 #[command()]
48 Normal(Normal),
49 #[command()]
50 Eet(Eet),
51 #[command(subcommand)]
52 Scan(Scan),
53}
54
55#[derive(Parser, Debug, PartialEq, Clone)]
57#[clap(short_flag = 'n')]
58pub struct Normal {
59 #[clap(env, long, short = 'f', value_parser = path_must_exist, required = true)]
61 pub log_file: PathBuf,
62
63 #[clap(env, short, long, value_parser = parse_absolute_path, required = true)]
65 pub game_directory: PathBuf,
66
67 #[clap(env, long, short = 'n', required = false)]
69 pub generate_directory: Option<PathBuf>,
70
71 #[clap(flatten)]
73 pub options: Options,
74}
75
76#[derive(Parser, Debug, PartialEq, Clone)]
78#[clap(short_flag = 'e')]
79pub struct Eet {
80 #[clap(env, short='1', long, value_parser = parse_absolute_path, required = true)]
82 pub bg1_game_directory: PathBuf,
83
84 #[clap(env, short='y', long, value_parser = path_must_exist, required = true)]
86 pub bg1_log_file: PathBuf,
87
88 #[clap(env, short='2', long, value_parser = parse_absolute_path, required = true)]
90 pub bg2_game_directory: PathBuf,
91
92 #[clap(env, short='z', long, value_parser = path_must_exist, required = true)]
94 pub bg2_log_file: PathBuf,
95
96 #[clap(env, short = 'p', long, value_parser = path_must_exist)]
98 pub new_pre_eet_dir: Option<PathBuf>,
99
100 #[clap(env, short = 'n', long, value_parser = path_must_exist)]
102 pub new_eet_dir: Option<PathBuf>,
103
104 #[clap(flatten)]
106 pub options: Options,
107}
108
109#[derive(Subcommand, Debug, PartialEq, Clone)]
111#[clap(short_flag = 's')]
112pub enum Scan {
113 #[command()]
114 Languages(ScanLangauges),
115 #[command()]
116 Components(ScanComponents),
117}
118
119#[derive(Parser, Debug, PartialEq, Clone)]
120#[clap(short_flag = 'l')]
121pub struct ScanLangauges {
122 #[clap(short, long, required = false, default_value = "")]
124 pub filter_by_selected_language: String,
125
126 #[clap(flatten)]
128 pub options: Options,
129}
130
131#[derive(Parser, Debug, PartialEq, Clone)]
132#[clap(short_flag = 'c')]
133pub struct ScanComponents {
134 #[clap(short, long, required = false, default_value = "")]
136 pub filter_by_selected_language: String,
137
138 #[clap(flatten)]
140 pub options: Options,
141}
142
143#[derive(Parser, Debug, PartialEq, Clone, Default)]
144pub struct Options {
145 #[clap(
147 env,
148 short,
149 long,
150 value_parser = parse_absolute_path,
151 default_value_os_t = find_weidu_bin(),
152 default_missing_value = find_weidu_bin_on_path(),
153 required = false
154 )]
155 pub weidu_binary: PathBuf,
156
157 #[clap(
159 env,
160 short,
161 long,
162 value_parser = path_exists_full,
163 use_value_delimiter = true,
164 value_delimiter = ',',
165 default_values_os_t = current_work_dir(),
166 default_missing_value = working_dir(),
167 required = false
168 )]
169 pub mod_directories: Vec<PathBuf>,
170
171 #[clap(short, long, default_value = "en_US")]
173 pub language: String,
174
175 #[clap(env, long, short, default_value = "5")]
177 pub depth: usize,
178
179 #[clap(
181 env,
182 short,
183 long,
184 num_args=0..=1,
185 action = clap::ArgAction::SetFalse,
186 default_value_t = true,
187 value_parser = BoolishValueParser::new(),
188 )]
189 pub skip_installed: bool,
190
191 #[clap(
193 env,
194 short,
195 long,
196 num_args=0..=1,
197 action = clap::ArgAction::SetTrue,
198 default_value_t = false,
199 value_parser = BoolishValueParser::new(),
200 conflicts_with = "never_abort",
201 )]
202 pub abort_on_warnings: bool,
203
204 #[clap(
206 env,
207 short = 'v',
208 long,
209 num_args=0..=1,
210 action = clap::ArgAction::SetTrue,
211 default_value_t = false,
212 value_parser = BoolishValueParser::new(),
213 conflicts_with = "abort_on_warnings",
214 )]
215 pub never_abort: bool,
216
217 #[clap(
219 env,
220 short,
221 long,
222 default_value = "3600",
223 default_value_if("batch_mode", ArgPredicate::IsPresent, "10800")
224 )]
225 pub timeout: usize,
226
227 #[clap(
229 env,
230 long,
231 short='u',
232 use_value_delimiter = true,
233 value_delimiter = ',',
234 default_value = "autolog,logapp,log-extern",
235 value_parser = LogOptions::value_parser,
236 required = false
237 )]
238 pub weidu_log_mode: Vec<LogOptions>,
239
240 #[clap(
242 env,
243 short = 'x',
244 long,
245 num_args=0..=1,
246 action = clap::ArgAction::SetTrue,
247 default_value_t = false,
248 value_parser = BoolishValueParser::new(),
249 )]
250 pub strict_matching: bool,
251
252 #[clap(
254 env,
255 long,
256 num_args=0..=1,
257 action = clap::ArgAction::SetFalse,
258 default_value_t = true,
259 value_parser = BoolishValueParser::new(),
260 )]
261 pub download: bool,
262
263 #[clap(
265 env,
266 short,
267 long,
268 num_args=0..=1,
269 action = clap::ArgAction::SetTrue,
270 default_value_t = false,
271 value_parser = BoolishValueParser::new(),
272 )]
273 pub overwrite: bool,
274
275 #[clap(
277 env,
278 short,
279 long,
280 num_args=0..=1,
281 action = clap::ArgAction::SetFalse,
282 default_value_t = true,
283 conflicts_with = "batch_mode",
284 value_parser = BoolishValueParser::new(),
285 )]
286 pub check_last_installed: bool,
287
288 #[clap(env, short = 'i', long, default_value_t = 500)]
290 pub tick: u64,
291
292 #[clap(env, short = '0', long, default_value_t = 10)]
294 pub lookback: usize,
295
296 #[clap(
298 env,
299 long,
300 num_args=0..=1,
301 action = clap::ArgAction::SetTrue,
302 default_value_t = false,
303 value_parser = BoolishValueParser::new(),
304 )]
305 pub casefold: bool,
306
307 #[clap(short = 'k', long, use_value_delimiter = true, value_delimiter = ',')]
309 pub generic_weidu_args: Vec<String>,
310
311 #[clap(flatten)]
313 pub batch: BatchOptions,
314
315 #[clap(env = "OCAMLRUNPARAM", long, default_value = "s=16M,o=500,O=1000000")]
317 pub ocamlrunparam: String,
318}
319
320#[derive(Parser, Debug, PartialEq, Clone, Default)]
321pub struct BatchOptions {
322 #[clap(
324 env,
325 long,
326 num_args=0..=1,
327 action = clap::ArgAction::SetTrue,
328 default_value_t = false,
329 conflicts_with = "check_last_installed",
330 value_parser = BoolishValueParser::new(),
331 )]
332 pub batch_mode: bool,
333
334 #[clap(env, long, default_value_t = 5, required = false)]
336 pub batch_size: usize,
337
338 #[clap(
340 env,
341 long,
342 use_value_delimiter = true,
343 value_delimiter = ',',
344 default_value = "setup-stratagems.tp2",
345 ignore_case = true,
346 required = false
347 )]
348 pub batch_skip: Vec<String>,
349}
350
351pub fn path_must_exist(arg: &str) -> Result<PathBuf, std::io::Error> {
352 let path = PathBuf::from(arg);
353 path.try_exists()?;
354 Ok(path)
355}
356
357fn path_exists_full(arg: &str) -> Result<PathBuf, std::io::Error> {
358 fs::canonicalize(path_must_exist(arg)?)
359}
360
361fn parse_absolute_path(arg: &str) -> Result<PathBuf, String> {
362 let path = path_must_exist(arg).map_err(|err| err.to_string())?;
363 fs::canonicalize(path).map_err(|err| err.to_string())
364}
365
366fn find_weidu_bin() -> PathBuf {
367 if let Some(paths) = var_os("PATH") {
368 for path in split_paths(&paths) {
369 let full_path = path.join(WEIDU_FILE_NAME);
370 if full_path.is_file() && !full_path.is_dir() {
371 return full_path;
372 }
373 }
374 }
375 PathBuf::new()
376}
377
378fn find_weidu_bin_on_path() -> OsStr {
379 OsStr::from(find_weidu_bin().into_os_string())
380}
381
382fn current_work_dir() -> Vec<PathBuf> {
383 vec![std::env::current_dir().unwrap_or_default()]
384}
385
386fn working_dir() -> OsStr {
387 OsStr::from(current_work_dir().first().unwrap().clone().into_os_string())
388}
389
390#[cfg(test)]
391mod tests {
392
393 use super::*;
394 use pretty_assertions::assert_eq;
395 use std::{error::Error, path::PathBuf};
396
397 #[test]
398 fn test_bool_flags() -> Result<(), Box<dyn Error>> {
399 let workspace_root: PathBuf = std::env::current_dir()?;
400 let fake_game_dir: PathBuf = workspace_root.join("fixtures");
401 let fake_weidu_bin = fake_game_dir.clone().join("weidu");
402 let fake_log_file = fake_game_dir.clone().join("weidu.log");
403 let fake_mod_dirs = fake_game_dir.clone().join("mods");
404 let tests = vec![
405 ("true", true),
406 ("t", true),
407 ("yes", true),
408 ("y", true),
409 ("1", true),
410 ("false", false),
411 ("f", false),
412 ("no", false),
413 ("n", false),
414 ("0", false),
415 ];
416 for (flag_value, expected_flag_value) in tests {
417 let expected = Args {
418 command: CommandType::Normal(Normal {
419 log_file: fake_log_file.clone(),
420 game_directory: fake_game_dir.clone(),
421 generate_directory: None,
422 options: Options {
423 weidu_binary: fake_weidu_bin.clone(),
424 mod_directories: vec![fake_mod_dirs.clone()],
425 language: "en_US".to_string(),
426 depth: 5,
427 skip_installed: expected_flag_value,
428 abort_on_warnings: expected_flag_value,
429 timeout: 3600,
430 weidu_log_mode: vec![
431 LogOptions::AutoLog,
432 LogOptions::LogAppend,
433 LogOptions::LogExternal,
434 ],
435 strict_matching: true,
436 download: true,
437 overwrite: false,
438 check_last_installed: true,
439 tick: 500,
440 lookback: 10,
441 casefold: false,
442 never_abort: false,
443 generic_weidu_args: vec![],
444 batch: BatchOptions {
445 batch_mode: false,
446 batch_size: 5,
447 batch_skip: vec!["setup-stratagems.tp2".into()],
448 },
449 ocamlrunparam: "s=16M,o=500,O=1000000".into(),
450 },
451 }),
452 };
453 let test_arg_string = format!(
454 "mod_installer -n -x -a {} -s {} -w {} -m {} -f {} -g {}",
455 flag_value,
456 flag_value,
457 fake_weidu_bin.to_str().unwrap_or_default(),
458 fake_mod_dirs.to_str().unwrap_or_default(),
459 fake_log_file.to_str().unwrap_or_default(),
460 fake_game_dir.to_str().unwrap_or_default(),
461 );
462 let result = Args::parse_from(test_arg_string.split(' '));
463 assert_eq!(
464 result, expected,
465 "Result {result:?} didn't match Expected {expected:?}",
466 );
467 }
468 Ok(())
469 }
470
471 #[test]
472 fn test_eet_flags() -> Result<(), Box<dyn Error>> {
473 let workspace_root: PathBuf = std::env::current_dir()?;
474 let fake_game_dir: PathBuf = workspace_root.join("fixtures");
475 let fake_weidu_bin = fake_game_dir.clone().join("weidu");
476 let fake_log_file = fake_game_dir.clone().join("weidu.log");
477 let new_dir = PathBuf::new().join("test");
478 let expected_flag_value = true;
479
480 let expected = Args {
481 command: CommandType::Eet(Eet {
482 bg1_game_directory: fake_game_dir.clone(),
483 bg1_log_file: fake_log_file.clone(),
484 bg2_game_directory: fake_game_dir.clone(),
485 bg2_log_file: fake_log_file.clone(),
486 options: Options {
487 weidu_binary: fake_weidu_bin.clone(),
488 mod_directories: vec![std::env::current_dir().unwrap()],
489 language: "en_US".to_string(),
490 depth: 5,
491 skip_installed: expected_flag_value,
492 abort_on_warnings: !expected_flag_value,
493 timeout: 3600,
494 weidu_log_mode: vec![
495 LogOptions::AutoLog,
496 LogOptions::LogAppend,
497 LogOptions::LogExternal,
498 ],
499 strict_matching: !expected_flag_value,
500 download: expected_flag_value,
501 overwrite: !expected_flag_value,
502 check_last_installed: expected_flag_value,
503 tick: 500,
504 lookback: 10,
505 casefold: false,
506 never_abort: false,
507 generic_weidu_args: vec![],
508 batch: BatchOptions {
509 batch_mode: false,
510 batch_size: 5,
511 batch_skip: vec!["setup-stratagems.tp2".into()],
512 },
513 ocamlrunparam: "s=16M,o=500,O=1000000".into(),
514 },
515 new_pre_eet_dir: None,
516 new_eet_dir: Some("test".into()),
517 }),
518 };
519 let test_arg_string = format!(
520 "mod_installer eet -w {} -1 {} -y {} -2 {} -z {} -n {}",
521 fake_weidu_bin.to_str().unwrap_or_default(),
522 fake_game_dir.to_str().unwrap_or_default(),
523 fake_log_file.to_str().unwrap_or_default(),
524 fake_game_dir.to_str().unwrap_or_default(),
525 fake_log_file.to_str().unwrap_or_default(),
526 new_dir.to_str().unwrap_or_default(),
527 );
528 let result = Args::parse_from(test_arg_string.split(' '));
529 assert_eq!(
530 result, expected,
531 "Result {result:?} didn't match Expected {expected:?}",
532 );
533
534 Ok(())
535 }
536 #[test]
537 fn test_log_flags() -> Result<(), Box<dyn Error>> {
538 let workspace_root: PathBuf = std::env::current_dir()?;
539 let fake_game_dir: PathBuf = workspace_root.join("fixtures");
540 let fake_weidu_bin = fake_game_dir.clone().join("weidu");
541 let fake_log_file = fake_game_dir.clone().join("weidu.log");
542 let fake_mod_dirs = fake_game_dir.clone().join("mods");
543 let log_file = tempfile::NamedTempFile::new()?;
544 let log_file_path = log_file.path().as_os_str().to_str().unwrap_or_default();
545 let expected = Args {
546 command: CommandType::Normal(Normal {
547 log_file: fake_log_file.clone(),
548 game_directory: fake_game_dir.clone(),
549 generate_directory: None,
550 options: Options {
551 weidu_binary: fake_weidu_bin.clone(),
552 mod_directories: vec![fake_mod_dirs.clone()],
553 language: "en_US".to_string(),
554 depth: 5,
555 skip_installed: true,
556 abort_on_warnings: false,
557 timeout: 3600,
558 weidu_log_mode: vec![
559 LogOptions::AutoLog,
560 LogOptions::LogAppend,
561 LogOptions::LogExternal,
562 #[cfg(not(target_os = "windows"))]
563 LogOptions::Log(log_file.path().canonicalize()?),
564 #[cfg(windows)]
565 LogOptions::Log(log_file.path()),
566 ],
567 strict_matching: false,
568 download: true,
569 overwrite: false,
570 check_last_installed: true,
571 tick: 500,
572 lookback: 10,
573 casefold: false,
574 never_abort: false,
575 generic_weidu_args: vec![],
576 batch: BatchOptions {
577 batch_mode: false,
578 batch_size: 5,
579 batch_skip: vec!["setup-stratagems.tp2".into()],
580 },
581 ocamlrunparam: "s=16M,o=500,O=1000000".into(),
582 },
583 }),
584 };
585 let log_arg = format!("autolog,logapp,log-extern,log {log_file_path}");
586 let test_arg_string = vec![
587 "mod_installer",
588 "-n",
589 "-w",
590 fake_weidu_bin.to_str().unwrap_or_default(),
591 "-m",
592 fake_mod_dirs.to_str().unwrap_or_default(),
593 "--log-file",
594 fake_log_file.to_str().unwrap_or_default(),
595 "--game-directory",
596 fake_game_dir.to_str().unwrap_or_default(),
597 "--weidu-log-mode",
598 &log_arg,
599 ];
600 let result = Args::parse_from(test_arg_string);
601 assert_eq!(
602 result, expected,
603 "Result {result:?} didn't match Expected {expected:?}",
604 );
605 Ok(())
606 }
607}