mod_installer/config/
args.rs

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::BoolishValueParser, builder::OsStr, Parser};
8
9use super::colors::styles;
10
11pub(crate) const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
12
13pub(crate) const LONG: &str = r"
14
15  /\/\   ___   __| | (_)_ __  ___| |_ __ _| | | ___ _ __
16 /    \ / _ \ / _` | | | '_ \/ __| __/ _` | | |/ _ \ '__|
17/ /\/\ \ (_) | (_| | | | | | \__ \ || (_| | | |  __/ |
18\/    \/\___/ \__,_| |_|_| |_|___/\__\__,_|_|_|\___|_|
19";
20
21pub(crate) const WEIDU_LOG_MODE_ERROR: &str = r"
22Please provide a valid weidu logging setting, options are:
23--weidu-log-mode log X       log output and details to X
24--weidu-log-mode autolog     log output and details to WSETUP.DEBUG
25--weidu-log-mode logapp      append to log instead of overwriting
26--weidu-log-mode log-extern  also log output from commands invoked by WeiDU
27";
28
29#[cfg(not(target_os = "windows"))]
30pub(crate) const WEIDU_FILE_NAME: &str = "weidu";
31#[cfg(target_os = "windows")]
32pub(crate) const WEIDU_FILE_NAME: &str = "weidu.exe";
33
34// https://docs.rs/clap/latest/clap/_derive/index.html#arg-attributes
35#[derive(Parser, Debug, PartialEq)]
36#[command(
37    version,
38    propagate_version = true,
39    styles = styles(),
40    about = format!("{}\n{}", LONG, std::env::var("CARGO_PKG_DESCRIPTION").unwrap_or_default())
41)]
42pub(crate) struct Args {
43    /// Install Type
44    #[command(subcommand)]
45    pub(crate) command: InstallType,
46}
47
48/// Type of Install, Normal or EET
49#[derive(Subcommand, Debug, PartialEq, Clone)]
50pub(crate) enum InstallType {
51    #[command()]
52    Normal(Normal),
53    #[command()]
54    Eet(Eet),
55}
56
57/// Normal install for (BG1EE,BG2EE,IWDEE) (STABLE)
58#[derive(Parser, Debug, PartialEq, Clone)]
59#[clap(short_flag = 'n')]
60pub(crate) struct Normal {
61    /// Path to target log
62    #[clap(env, long, short = 'f', value_parser = path_must_exist, required = true)]
63    pub(crate) log_file: PathBuf,
64
65    /// Absolute Path to game directory
66    #[clap(env, short, long, value_parser = parse_absolute_path, required = true)]
67    pub(crate) game_directory: PathBuf,
68
69    /// Instead of operating on an existing directory, create a new one with this flag as its name and then copy the original contents into it.
70    #[clap(env, long, short = 'n', required = false)]
71    pub(crate) new_game_directory: Option<PathBuf>,
72
73    /// CommonOptions
74    #[clap(flatten)]
75    pub(crate) options: Options,
76}
77
78/// EET install for (eet) (ALPHA)
79#[derive(Parser, Debug, PartialEq, Clone)]
80#[clap(short_flag = 'e')]
81pub(crate) struct Eet {
82    /// Absolute Path to bg1ee game directory
83    #[clap(env, short='1', long, value_parser = parse_absolute_path, required = true)]
84    pub(crate) bg1_game_directory: PathBuf,
85
86    /// Path to bg1ee weidu.log file
87    #[clap(env, short='y', long, value_parser = path_must_exist, required = true)]
88    pub(crate) bg1_log_file: PathBuf,
89
90    /// Absolute Path to bg2ee game directory
91    #[clap(env, short='2', long, value_parser = parse_absolute_path, required = true)]
92    pub(crate) bg2_game_directory: PathBuf,
93
94    /// Path to bg2ee weidu.log file
95    #[clap(env, short='z', long, value_parser = path_must_exist, required = true)]
96    pub(crate) bg2_log_file: PathBuf,
97
98    /// Instead of operating on an existing directory, create a new one with this flag as its name and then copy the original contents into it.
99    #[clap(
100        env,
101        short,
102        long,
103        action = clap::ArgAction::SetTrue,
104        required = false,
105    )]
106    pub(crate) generate_directories: bool,
107
108    /// If generate_directories is true. This is the path for the new pre-eet directory.
109    #[clap(env, short = 'p', long, value_parser = path_must_exist)]
110    pub(crate) new_pre_eet_dir: Option<PathBuf>,
111
112    /// If generate_directories is true. This is the path for the new eet directory.
113    #[clap(env, short = 'n', long, value_parser = path_must_exist)]
114    pub(crate) new_eet_dir: Option<PathBuf>,
115
116    /// CommonOptions
117    #[clap(flatten)]
118    pub(crate) options: Options,
119}
120
121#[derive(Parser, Debug, PartialEq, Clone)]
122pub(crate) struct Options {
123    /// Absolute Path to weidu binary
124    #[clap(
125        env,
126        short,
127        long,
128        value_parser = parse_absolute_path,
129        default_value_os_t = find_weidu_bin(),
130        default_missing_value = find_weidu_bin_on_path(),
131        required = false
132    )]
133    pub(crate) weidu_binary: PathBuf,
134
135    /// Path to mod directories
136    #[clap(
137        env,
138        short,
139        long,
140        value_parser = path_exists_full,
141        use_value_delimiter = true,
142        value_delimiter = ',',
143        default_values_os_t = current_work_dir(),
144        default_missing_value = working_dir(),
145        required = false
146    )]
147    pub(crate) mod_directories: Vec<PathBuf>,
148
149    /// Game Language
150    #[clap(short, long, default_value = "en_US")]
151    pub(crate) language: String,
152
153    /// Depth to walk folder structure
154    #[clap(env, long, short, default_value = "5")]
155    pub(crate) depth: usize,
156
157    /// Compare against installed weidu log, note this is best effort
158    #[clap(
159        env,
160        short,
161        long,
162        num_args=0..=1,
163        action = clap::ArgAction::Set,
164        default_value_t = true,
165        default_missing_value = "true",
166        value_parser = BoolishValueParser::new(),
167    )]
168    pub(crate) skip_installed: bool,
169
170    /// If a warning occurs in the weidu child process exit
171    #[clap(
172        env,
173        short,
174        long,
175        num_args=0..=1,
176        action = clap::ArgAction::Set,
177        default_value_t = true,
178        default_missing_value = "true",
179        value_parser = BoolishValueParser::new(),
180    )]
181    pub(crate) abort_on_warnings: bool,
182
183    /// Timeout time per mod in seconds, default is 1 hour
184    #[clap(env, long, short, default_value = "3600")]
185    pub(crate) timeout: usize,
186
187    /// Weidu log setting "--autolog" is default
188    #[clap(
189        env,
190        long,
191        short='u',
192        default_value = "autolog",
193        value_parser = parse_weidu_log_mode,
194        required = false
195    )]
196    pub(crate) weidu_log_mode: String,
197
198    /// Strict Version and Component/SubComponent matching
199    #[clap(
200        env,
201        short = 'x',
202        long,
203        num_args=0..=1,
204        action = clap::ArgAction::SetTrue,
205        default_value_t = false,
206        default_missing_value = "false",
207        value_parser = BoolishValueParser::new(),
208        required = false,
209    )]
210    pub(crate) strict_matching: bool,
211
212    /// When a missing log is discovered ask the user for the download uri, download the mod and install it
213    #[clap(
214        env,
215        long,
216        num_args=0..=1,
217        action = clap::ArgAction::SetFalse,
218        default_value_t = true,
219        default_missing_value = "true",
220        value_parser = BoolishValueParser::new(),
221    )]
222    pub(crate) download: bool,
223}
224
225fn parse_weidu_log_mode(arg: &str) -> Result<String, String> {
226    let mut args = arg.split(' ');
227    let mut output = vec![];
228    while let Some(arg) = args.next() {
229        match arg {
230            "log" if path_must_exist(arg).is_ok() => {
231                let path = args.next().unwrap();
232                output.push(format!("--{arg} {path}"));
233            }
234            "autolog" => output.push(format!("--{arg}")),
235            "logapp" => output.push(format!("--{arg}")),
236            "log-extern" => output.push(format!("--{arg}")),
237            _ => return Err(format!("{}, Provided {}", WEIDU_LOG_MODE_ERROR, arg)),
238        };
239    }
240    Ok(output.join(" "))
241}
242
243fn path_must_exist(arg: &str) -> Result<PathBuf, std::io::Error> {
244    let path = PathBuf::from(arg);
245    path.try_exists()?;
246    Ok(path)
247}
248
249fn path_exists_full(arg: &str) -> Result<PathBuf, std::io::Error> {
250    fs::canonicalize(path_must_exist(arg)?)
251}
252
253fn parse_absolute_path(arg: &str) -> Result<PathBuf, String> {
254    let path = path_must_exist(arg).map_err(|err| err.to_string())?;
255    fs::canonicalize(path).map_err(|err| err.to_string())
256}
257
258fn find_weidu_bin() -> PathBuf {
259    if let Some(paths) = var_os("PATH") {
260        for path in split_paths(&paths) {
261            let full_path = path.join(WEIDU_FILE_NAME);
262            if full_path.is_file() && !full_path.is_dir() {
263                return full_path;
264            }
265        }
266    }
267    PathBuf::new()
268}
269
270fn find_weidu_bin_on_path() -> OsStr {
271    OsStr::from(find_weidu_bin().into_os_string())
272}
273
274fn current_work_dir() -> Vec<PathBuf> {
275    vec![std::env::current_dir().unwrap()]
276}
277
278fn working_dir() -> OsStr {
279    OsStr::from(current_work_dir().first().unwrap().clone().into_os_string())
280}
281
282#[cfg(test)]
283mod tests {
284
285    use super::*;
286    use pretty_assertions::assert_eq;
287    use std::error::Error;
288
289    #[test]
290    fn test_parse_weidu_log_mode() -> Result<(), Box<dyn Error>> {
291        let tests = vec![
292            ("autolog", Ok("--autolog".to_string())),
293            ("log /home", Ok("--log /home".to_string())),
294            ("autolog logapp", Ok("--autolog --logapp".to_string())),
295            (
296                "autolog logapp log-extern",
297                Ok("--autolog --logapp --log-extern".to_string()),
298            ),
299            (
300                "log /home logapp log-extern",
301                Ok("--log /home --logapp --log-extern".to_string()),
302            ),
303            (
304                "fish",
305                Err(format!("{}, Provided {}", WEIDU_LOG_MODE_ERROR, "fish")),
306            ),
307            (
308                "log /home fish",
309                Err(format!("{}, Provided {}", WEIDU_LOG_MODE_ERROR, "fish")),
310            ),
311        ];
312        for (test, expected) in tests {
313            println!("{:#?}", test);
314            let result = parse_weidu_log_mode(test);
315            assert_eq!(
316                result, expected,
317                "Result {result:?} didn't match Expected {expected:?}",
318            );
319        }
320        Ok(())
321    }
322
323    #[test]
324    fn test_bool_flags() -> Result<(), Box<dyn Error>> {
325        let fake_game_dir = std::env::current_dir().unwrap().join("fixtures");
326        let fake_weidu_bin = fake_game_dir.clone().join("weidu");
327        let fake_log_file = fake_game_dir.clone().join("weidu.log");
328        let fake_mod_dirs = fake_game_dir.clone().join("mods");
329        let tests = vec![
330            ("true", true),
331            ("t", true),
332            ("yes", true),
333            ("y", true),
334            ("1", true),
335            ("false", false),
336            ("f", false),
337            ("no", false),
338            ("n", false),
339            ("0", false),
340        ];
341        for (flag_value, expected_flag_value) in tests {
342            let expected = Args {
343                command: InstallType::Normal(Normal {
344                    log_file: fake_log_file.clone(),
345                    game_directory: fake_game_dir.clone(),
346                    new_game_directory: None,
347                    options: Options {
348                        weidu_binary: fake_weidu_bin.clone(),
349                        mod_directories: vec![fake_mod_dirs.clone()],
350                        language: "en_US".to_string(),
351                        depth: 5,
352                        skip_installed: expected_flag_value,
353                        abort_on_warnings: expected_flag_value,
354                        timeout: 3600,
355                        weidu_log_mode: "--autolog".to_string(),
356                        strict_matching: false,
357                        download: true,
358                    },
359                }),
360            };
361            let test_arg_string = format!(
362                "mod_installer -n -x -a {} -s {} -w {} -m {} -f {} -g {}",
363                flag_value,
364                flag_value,
365                fake_weidu_bin.to_str().unwrap_or_default(),
366                fake_mod_dirs.to_str().unwrap_or_default(),
367                fake_log_file.to_str().unwrap_or_default(),
368                fake_game_dir.to_str().unwrap_or_default(),
369            );
370            let result = Args::parse_from(test_arg_string.split(' '));
371            assert_eq!(
372                result, expected,
373                "Result {result:?} didn't match Expected {expected:?}",
374            );
375        }
376        Ok(())
377    }
378
379    #[test]
380    fn test_eet_flags() -> Result<(), Box<dyn Error>> {
381        let fake_game_dir = std::env::current_dir().unwrap().join("fixtures");
382        let fake_weidu_bin = fake_game_dir.clone().join("weidu");
383        let fake_log_file = fake_game_dir.clone().join("weidu.log");
384        let new_dir = PathBuf::new().join("test");
385        let expected_flag_value = true;
386
387        let expected = Args {
388            command: InstallType::Eet(Eet {
389                bg1_game_directory: fake_game_dir.clone(),
390                bg1_log_file: fake_log_file.clone(),
391                bg2_game_directory: fake_game_dir.clone(),
392                bg2_log_file: fake_log_file.clone(),
393                options: Options {
394                    weidu_binary: fake_weidu_bin.clone(),
395                    mod_directories: vec![std::env::current_dir().unwrap()],
396                    language: "en_US".to_string(),
397                    depth: 5,
398                    skip_installed: expected_flag_value,
399                    abort_on_warnings: expected_flag_value,
400                    timeout: 3600,
401                    weidu_log_mode: "--autolog".to_string(),
402                    strict_matching: !expected_flag_value,
403                    download: true,
404                },
405                generate_directories: false,
406                new_pre_eet_dir: None,
407                new_eet_dir: Some("test".into()),
408            }),
409        };
410        let test_arg_string = format!(
411            "mod_installer eet -w {} -1 {} -y {} -2 {} -z {} -n {}",
412            fake_weidu_bin.to_str().unwrap_or_default(),
413            fake_game_dir.to_str().unwrap_or_default(),
414            fake_log_file.to_str().unwrap_or_default(),
415            fake_game_dir.to_str().unwrap_or_default(),
416            fake_log_file.to_str().unwrap_or_default(),
417            new_dir.to_str().unwrap_or_default(),
418        );
419        let result = Args::parse_from(test_arg_string.split(' '));
420        assert_eq!(
421            result, expected,
422            "Result {result:?} didn't match Expected {expected:?}",
423        );
424
425        Ok(())
426    }
427}