ftw/
ftw_command.rs

1use crate::ftw_build_type::FtwBuildType;
2use crate::ftw_configuration::FtwConfiguration;
3use crate::ftw_error::FtwError;
4use crate::ftw_machine_type::FtwMachineType;
5use crate::ftw_node_type::FtwNodeType;
6use crate::ftw_success::FtwSuccess;
7use crate::ftw_tag::FtwTag;
8use crate::ftw_target::FtwTarget;
9use crate::ftw_template::FtwTemplate;
10use crate::traits::{Compiler, Processor, Runner, ToCliArg, ToGitTag, ToGitUrl};
11use crate::type_alias::{ClassName, FtwResult, ProjectName};
12use crate::util;
13
14use cargo_generate::{generate, GenerateArgs, TemplatePath, Vcs};
15use command_macros::cmd;
16use fs_extra::remove_items;
17use kstring::KStringBase;
18use liquid::{object, Object, ParserBuilder};
19use liquid_core::model::{ScalarCow, Value};
20use regex::Regex;
21use std::fs::{create_dir_all, read_dir, write, File, OpenOptions};
22use std::io::prelude::*;
23use std::path::Path;
24use std::{thread, time};
25use strum::IntoEnumIterator;
26use voca_rs::Voca;
27use walkdir::WalkDir;
28
29#[derive(Debug, Eq, PartialEq)]
30pub enum FtwCommand {
31    New {
32        project_name: ProjectName,
33        template: FtwTemplate,
34        tag: FtwTag,
35    },
36    Class {
37        class_name: ClassName,
38        node_type: FtwNodeType,
39    },
40    Singleton {
41        class_name: ClassName,
42    },
43    Run {
44        machine_type: FtwMachineType,
45    },
46    Build {
47        targets: Vec<FtwTarget>,
48        build_type: FtwBuildType,
49    },
50    Export {
51        targets: Vec<FtwTarget>,
52        build_type: FtwBuildType,
53    },
54    Clean,
55}
56
57#[rustfmt::skip::macros(cmd, format)]
58impl FtwCommand {
59    fn generate_project(
60        project_name: &str,
61        template: &FtwTemplate,
62        tag: &FtwTag,
63    ) -> Result<(), FtwError> {
64        let git_url = &template.to_git_url();
65        let git_tag = &tag.to_git_tag();
66        let template_path = TemplatePath {
67            git: Some(git_url.to_string()),
68            branch: None,
69            favorite: None,
70            subfolder: None,
71            path: None,
72            auto_path: None,
73            test: false,
74            tag: Some(git_tag.to_string()),
75        };
76        let generate_args = GenerateArgs {
77            template_path,
78            force: false,
79            name: Some(project_name.to_string()),
80            verbose: false,
81            config: None,
82            list_favorites: false,
83            silent: true,
84            template_values_file: None,
85            vcs: Some(Vcs::Git),
86            bin: false,
87            lib: true,
88            ssh_identity: None,
89            define: vec![],
90            init: false,
91            destination: None,
92            force_git_init: false,
93            allow_commands: false,
94            overwrite: false,
95            other_args: None,
96        };
97        generate(generate_args)?;
98        Ok(())
99    }
100
101    fn append_to_gitignore(project_name: &str) -> Result<(), FtwError> {
102        let gitignore_path: String = format!("{project_name}/.gitignore");
103        let mut gitignore_file = OpenOptions::new().append(true).open(gitignore_path)?;
104        let things_to_be_ignored = [".ftw", "bin/*", "godot/export_presets.cfg", "lib/*", ".tag"];
105        for thing in things_to_be_ignored {
106            writeln!(gitignore_file, "{thing}")?;
107        }
108        Ok(())
109    }
110
111    fn delete_items(project_name: &str) -> Result<(), FtwError> {
112        let files_to_be_removed: Vec<String> = [".travis.yml", "LICENSE", "sh"]
113            .into_iter()
114            .map(|file| format!("{project_name}/{file}"))
115            .collect();
116        Ok(remove_items(&files_to_be_removed)?)
117    }
118
119    fn create_file(
120        template_contents: &str,
121        target_file_path: &str,
122        template_globals: &Object,
123    ) -> Result<(), FtwError> {
124        let builder = ParserBuilder::with_stdlib().build()?;
125        let template = builder.parse(template_contents)?;
126        let output = template.render(template_globals)?;
127        write(target_file_path, output.as_bytes())?;
128        println!("{target_file_path} has been created...");
129        Ok(())
130    }
131
132    fn is_valid_project() -> Result<bool, FtwError> {
133        let project_files = [
134            "Cargo.toml",
135            "Makefile",
136            "godot/default_env.tres",
137            "godot/export_presets.cfg",
138            "godot/native/game.gdnlib",
139            "godot/project.godot",
140            "rust/src/lib.rs",
141            "rust/Cargo.toml",
142        ];
143        let targets: Vec<String> = FtwTarget::iter()
144            .flat_map(|target| {
145                let target_cli_arg = target.to_cli_arg();
146                let gitkeep = format!("{target_cli_arg}/.gitkeep");
147                let bin_gitkeep = format!("bin/{gitkeep}");
148                let lib_gitkeep = format!("lib/{gitkeep}");
149                [bin_gitkeep, lib_gitkeep]
150            })
151            .collect();
152        let is_valid_project = project_files.iter().all(|project_file| {
153            // TODO: Remove the check for the Makefile in the future
154            if project_file == &"Makefile" {
155                Path::new(project_file).exists() || Path::new("Makefile.toml").exists()
156            } else {
157                Path::new(project_file).exists()
158            }
159        });
160        let is_valid_targets = targets.iter().all(|target| Path::new(&target).exists());
161        if is_valid_project && is_valid_targets {
162            println!("Project is valid...");
163            Ok(true)
164        } else {
165            Err(FtwError::InvalidProject)
166        }
167    }
168
169    /// # Errors
170    ///
171    /// Will return `Err` if the regular expression is invalid
172    pub fn is_derving_native_class(contents: &str) -> Result<bool, FtwError> {
173        let reg_ex = Regex::new(r"#\[derive\([a-zA-Z, ]*NativeClass[a-zA-Z, ]*\)\]+")?;
174        Ok(reg_ex.find(contents).is_some())
175    }
176
177    fn get_classes_from_directory(directory: &str) -> Result<String, FtwError> {
178        let mut classes: Vec<String> = Vec::new();
179        for entry in WalkDir::new(directory) {
180            let entry = entry?;
181            let entry_path = entry.path();
182            let path = Path::new(&entry_path);
183            let is_file = path.is_file();
184            if is_file {
185                let mut file_contents = String::new();
186                let mut file = File::open(entry.path())?;
187                file.read_to_string(&mut file_contents)?;
188                let is_native_class = FtwCommand::is_derving_native_class(&file_contents)?;
189                if is_native_class {
190                    let class_name = path.file_stem().ok_or(FtwError::PathError)?;
191                    let class_name = class_name.to_str().ok_or(FtwError::StringConversionError)?;
192                    let class_name = class_name.replace(".rs", "")._pascal_case();
193                    let module_name = class_name._snake_case();
194                    let path_display = path.display();
195                    let path_display = format!("{path_display}");
196                    let replaced_path_display = path_display.as_str().replace('\\', "/");
197                    let module_name_vec: Vec<&str> = replaced_path_display.split('/').collect();
198                    let (_, module_path) = module_name_vec.split_at(2);
199                    let mut full_module_name_vec = module_path.to_vec();
200                    full_module_name_vec.pop();
201                    full_module_name_vec.push(&module_name);
202                    full_module_name_vec.push(&class_name);
203                    classes.push(full_module_name_vec.join("::"));
204                }
205            }
206        }
207        Ok(classes.join("|"))
208    }
209
210    fn get_tmpl_globals(class_name: &str, node_type: FtwNodeType) -> Object {
211        object!({ "class_name": class_name, "node_type": node_type.to_string() })
212    }
213
214    fn create_lib_rs_file(class_name: &str, node_type: FtwNodeType) -> Result<(), FtwError> {
215        let mut tmpl_globals = FtwCommand::get_tmpl_globals(class_name, node_type);
216        let modules = FtwCommand::get_modules_from_directory("rust/src")?;
217        let k = KStringBase::from_ref("modules");
218        let v = Value::Scalar(ScalarCow::from(modules));
219        tmpl_globals.insert(k, v);
220        let classes = FtwCommand::get_classes_from_directory("rust/src")?;
221        let k = KStringBase::from_ref("classes");
222        let v = Value::Scalar(ScalarCow::from(classes));
223        tmpl_globals.insert(k, v);
224        let template = &String::from_utf8_lossy(include_bytes!("templates/lib_tmpl.rs"));
225        FtwCommand::create_file(template, "rust/src/lib.rs", &tmpl_globals)
226    }
227
228    fn create_directory(base_path: &str, directories: &[String]) -> Result<String, FtwError> {
229        let dir_path = directories.join("/");
230        let full_path = format!("{base_path}/{dir_path}");
231        create_dir_all(&full_path)?;
232        Ok(full_path)
233    }
234
235    fn get_modules_from_directory(directory: &str) -> Result<String, FtwError> {
236        let files_and_folders = read_dir(directory)?;
237        let mut modules: Vec<String> = Vec::new();
238        for entry in files_and_folders {
239            let entry = entry?;
240            let entry_path = entry.path();
241            let path = Path::new(&entry_path);
242            let is_file = path.is_file();
243            if is_file {
244                let mut file_contents = String::new();
245                let mut file = File::open(&entry.path())?;
246                file.read_to_string(&mut file_contents)?;
247                let is_native_class = FtwCommand::is_derving_native_class(&file_contents)?;
248                if is_native_class {
249                    let module_path = path
250                        .file_stem()
251                        .ok_or(FtwError::PathError)?
252                        .to_os_string()
253                        .to_str()
254                        .ok_or(FtwError::StringConversionError)?
255                        .to_string();
256                    modules.push(module_path);
257                }
258            }
259            let is_dir = path.is_dir();
260            let entry_path_display = entry_path.as_path().display();
261            let mod_rs_file = format!("{entry_path_display}/mod.rs");
262            let mod_rs_file_path = Path::new(&mod_rs_file);
263            let is_contains_mod_rs = mod_rs_file_path.exists();
264            if is_dir && is_contains_mod_rs {
265                let module_path = path
266                    .file_name()
267                    .ok_or(FtwError::PathError)?
268                    .to_str()
269                    .ok_or(FtwError::StringConversionError)?
270                    .to_string();
271                modules.push(module_path);
272            }
273        }
274        Ok(modules.join("|"))
275    }
276
277    fn create_mod_rs_file(base_src_path: &str, directories: &[String]) -> Result<(), FtwError> {
278        if directories.is_empty() {
279            Ok(())
280        } else {
281            let dir = directories.join("/");
282            let current_path = format!("{base_src_path}/{dir}");
283            let mod_rs_file = format!("{current_path}/mod.rs");
284            let modules = FtwCommand::get_modules_from_directory(&current_path)?;
285            let tmpl_globals = object!({ "modules": modules });
286            let template = &String::from_utf8_lossy(include_bytes!("templates/mod_tmpl.rs"));
287            FtwCommand::create_file(template, &mod_rs_file, &tmpl_globals)?;
288            match directories.split_last() {
289                Some((_, init)) => FtwCommand::create_mod_rs_file(base_src_path, init),
290                _ => unreachable!(),
291            }
292        }
293    }
294
295    fn create_class_rs_file(
296        class_name: &str,
297        directories: &[String],
298        node_type: FtwNodeType,
299    ) -> Result<(), FtwError> {
300        let base_src_path = "rust/src";
301        let src_dir_path = FtwCommand::create_directory(base_src_path, directories)?;
302        let class_name_snake_case = class_name._snake_case();
303        let class_rs_file = format!("{src_dir_path}/{class_name_snake_case}.rs");
304        if !Path::new(&class_rs_file).exists() {
305            let tmpl_globals = FtwCommand::get_tmpl_globals(class_name, node_type);
306            let template = &String::from_utf8_lossy(include_bytes!("templates/class_tmpl.rs"));
307            FtwCommand::create_file(template, &class_rs_file, &tmpl_globals)?;
308        }
309        FtwCommand::create_mod_rs_file(base_src_path, directories)?;
310        Ok(())
311    }
312
313    fn create_gdns_file(
314        class_name: &str,
315        directories: &[String],
316        node_type: FtwNodeType,
317    ) -> Result<(), FtwError> {
318        let gdns_dir_path = FtwCommand::create_directory("godot/native", directories)?;
319        let class_name_pascal_case = class_name._pascal_case();
320        let gdns_file = format!("{gdns_dir_path}/{class_name_pascal_case}.gdns");
321        if !Path::new(&gdns_file).exists() {
322            let tmpl_globals = FtwCommand::get_tmpl_globals(class_name, node_type);
323            let template = &String::from_utf8_lossy(include_bytes!("templates/gdns_tmpl.gdns"));
324            FtwCommand::create_file(template, &gdns_file, &tmpl_globals)?;
325        }
326        Ok(())
327    }
328
329    fn create_tscn_file(
330        class_name: &str,
331        directories: &[String],
332        node_type: FtwNodeType,
333    ) -> Result<(), FtwError> {
334        let tscn_dir_path = FtwCommand::create_directory("godot/scenes", directories)?;
335        let class_name_pascal_case = class_name._pascal_case();
336        let tscn_file = format!("{tscn_dir_path}/{class_name_pascal_case}.tscn");
337        if !Path::new(&tscn_file).exists() {
338            let mut tmpl_globals = FtwCommand::get_tmpl_globals(class_name, node_type);
339            let k = KStringBase::from_ref("dir_path");
340            let v = Value::Scalar(ScalarCow::from(if directories.is_empty() {
341                String::new()
342            } else {
343                let mut dir = directories.join("/");
344                dir.push('/');
345                dir
346            }));
347            tmpl_globals.insert(k, v);
348            let template = &String::from_utf8_lossy(include_bytes!("templates/tscn_tmpl.tscn"));
349            FtwCommand::create_file(template, &tscn_file, &tmpl_globals)?;
350        }
351        Ok(())
352    }
353
354    fn clean() -> Result<(), FtwError> {
355        let compiler =
356            FtwConfiguration::new().get_compiler(FtwTarget::default(), FtwBuildType::default());
357        compiler.clean()
358    }
359
360    fn build_lib(target: FtwTarget, build_type: FtwBuildType) -> Result<(), FtwError> {
361        let compiler = FtwConfiguration::new().get_compiler(target, build_type);
362        compiler.build()
363    }
364
365    fn export_game(target: FtwTarget, build_type: FtwBuildType) -> Result<(), FtwError> {
366        let compiler = FtwConfiguration::new().get_compiler(target, build_type);
367        compiler.export()
368    }
369
370    fn run_with_godot(machine_type: &FtwMachineType) -> Result<(), FtwError> {
371        let godot_executable = util::get_godot_exe_for_running(machine_type);
372        cmd!((godot_executable) ("--path") ("godot/") if (machine_type.is_desktop()) { (machine_type.to_cli_arg()) }).run()
373    }
374}
375
376#[rustfmt::skip]
377impl Processor for FtwCommand {
378    fn process(&self) -> FtwResult {
379        match self {
380            FtwCommand::New { project_name, template, tag } => {
381                FtwCommand::generate_project(project_name, template, tag)?;
382                FtwCommand::append_to_gitignore(project_name)?;
383                FtwCommand::delete_items(project_name)?;
384                let project_name = project_name.to_string();
385                Ok(FtwSuccess::New { project_name, template, tag })
386            }
387            FtwCommand::Class { class_name, node_type } => {
388                FtwCommand::is_valid_project()?;
389                let (class_name, directories) = util::get_class_name_and_directories(class_name);
390                FtwCommand::create_class_rs_file(&class_name, &directories, *node_type)?;
391                FtwCommand::create_gdns_file(&class_name, &directories, *node_type)?;
392                FtwCommand::create_tscn_file(&class_name, &directories, *node_type)?;
393                FtwCommand::create_lib_rs_file(&class_name, *node_type)?;
394                Ok(FtwSuccess::Class { class_name, node_type })
395            }
396            FtwCommand::Singleton { class_name } => {
397                FtwCommand::is_valid_project()?;
398                let node_type = FtwNodeType::default();
399                let (class_name, directories) = util::get_class_name_and_directories(class_name);
400                FtwCommand::create_class_rs_file(&class_name, &directories, node_type)?;
401                FtwCommand::create_gdns_file(&class_name, &directories, node_type)?;
402                FtwCommand::create_lib_rs_file(&class_name, node_type)?;
403                println!("Open Project -> Project Settings -> Autoload and then add the newly created *.gdns file as an autoload");
404                // TODO: parse and modify project.godot file to include the newly created *.gdns file as an autoload
405                Ok(FtwSuccess::Singleton { class_name })
406            }
407            FtwCommand::Run { machine_type } => {
408                FtwCommand::is_valid_project()?;
409                let build_type = FtwBuildType::default();
410                let current_platform = util::get_current_platform();
411                let target: FtwTarget = current_platform.parse().unwrap_or_default();
412                if machine_type.is_server() {
413                    target.is_linux_server()?;
414                }
415                FtwCommand::build_lib(target, build_type)?;
416                FtwCommand::run_with_godot(machine_type)?;
417                Ok(FtwSuccess::Run { machine_type })
418            }
419            FtwCommand::Build { targets, build_type } => {
420                FtwCommand::is_valid_project()?;
421                for target in targets {
422                    FtwCommand::build_lib(*target, *build_type)?;
423                    thread::sleep(time::Duration::from_millis(100));
424                }
425                Ok(FtwSuccess::Build { targets, build_type })
426            }
427            FtwCommand::Export { targets, build_type } => {
428                FtwCommand::is_valid_project()?;
429                for target in targets {
430                    FtwCommand::build_lib(*target, *build_type)?;
431                    FtwCommand::export_game(*target, *build_type)?;
432                }
433                Ok(FtwSuccess::Export { targets, build_type })
434            }
435            FtwCommand::Clean => {
436                FtwCommand::clean()?;
437                Ok(FtwSuccess::Clean)
438            }
439        }
440    }
441}
442
443#[cfg(test)]
444mod ftw_command_tests {
445    use super::*;
446    use crate::{
447        test_util::Project,
448        traits::{ToAppExt, ToLibExt, ToLibPrefix},
449    };
450    use assert_cmd::prelude::*;
451    use std::env;
452    use std::process::Command;
453
454    #[test]
455    fn test_is_valid_project_no_cargo_toml() {
456        let project = Project::default();
457        let cmd = FtwCommand::New {
458            project_name: project.get_name(),
459            template: FtwTemplate::default(),
460            tag: FtwTag::default(),
461        };
462        let _ = cmd.process();
463        let _ = env::set_current_dir(Path::new(&project.get_name()));
464        let _ = remove_items(&["Cargo.toml"]);
465        let res = FtwCommand::is_valid_project();
466        match res {
467            Err(FtwError::InvalidProject) => assert!(true),
468            _ => unreachable!(),
469        }
470        let _ = env::set_current_dir(Path::new("../"));
471        drop(project)
472    }
473
474    #[test]
475    fn test_process_ftw_command_new() {
476        let project = Project::new();
477        let cmd = FtwCommand::New {
478            project_name: project.get_name(),
479            template: FtwTemplate::default(),
480            tag: FtwTag::default(),
481        };
482        let _ = cmd.process();
483        assert!(project.exists(".gitignore"));
484        assert!(project.exists("Cargo.toml"));
485        assert!(project.exists("Makefile"));
486        assert!(project.exists("Makefile.toml"));
487        assert!(project.exists("godot/default_env.tres"));
488        assert!(project.exists("godot/export_presets.cfg"));
489        assert!(project.exists("godot/native/game.gdnlib"));
490        assert!(project.exists("godot/project.godot"));
491        assert!(project.exists("rust/Cargo.toml"));
492        assert!(project.exists("rust/src/lib.rs"));
493        assert!(!project.exists("LICENSE"));
494        assert!(!project.exists(".travis.yml"));
495        assert!(!project.exists("sh"));
496        assert!(project.read(".gitignore").contains(".ftw"));
497        assert!(project.read(".gitignore").contains("bin/*"));
498        assert!(project.read(".gitignore").contains("export_presets.cfg"));
499        assert!(project.read(".gitignore").contains("lib/*"));
500        assert!(project.read(".gitignore").contains(".tag"));
501        assert!(project.read(".tag").contains("v1.5.0"));
502        assert!(project
503            .read("rust/Cargo.toml")
504            .contains(&project.get_name()));
505    }
506
507    #[test]
508    fn test_process_ftw_command_new_with_latest_tag() {
509        let project = Project::new();
510        let cmd = FtwCommand::New {
511            project_name: project.get_name(),
512            template: FtwTemplate::default(),
513            tag: FtwTag::Latest,
514        };
515        let _ = cmd.process();
516        assert!(project.exists(".gitignore"));
517        assert!(project.exists("Cargo.toml"));
518        assert!(project.exists("Makefile"));
519        assert!(project.exists("Makefile.toml"));
520        assert!(project.exists("godot/default_env.tres"));
521        assert!(project.exists("godot/export_presets.cfg"));
522        assert!(project.exists("godot/native/game.gdnlib"));
523        assert!(project.exists("godot/project.godot"));
524        assert!(project.exists("rust/Cargo.toml"));
525        assert!(project.exists("rust/src/lib.rs"));
526        assert!(!project.exists("LICENSE"));
527        assert!(!project.exists(".travis.yml"));
528        assert!(!project.exists("sh"));
529        assert!(project.read(".gitignore").contains(".ftw"));
530        assert!(project.read(".gitignore").contains("bin/*"));
531        assert!(project.read(".gitignore").contains("export_presets.cfg"));
532        assert!(project.read(".gitignore").contains("lib/*"));
533        assert!(project.read(".gitignore").contains(".tag"));
534        assert!(project.read(".tag").contains("v1.5.0"));
535        assert!(project
536            .read("rust/Cargo.toml")
537            .contains(&project.get_name()));
538    }
539
540    #[test]
541    fn test_process_ftw_command_new_with_v150_tag() {
542        let project = Project::new();
543        let cmd = FtwCommand::New {
544            project_name: project.get_name(),
545            template: FtwTemplate::default(),
546            tag: FtwTag::Tagged {
547                git_tag: String::from("v1.5.0"),
548            },
549        };
550        let _ = cmd.process();
551        assert!(project.exists(".gitignore"));
552        assert!(project.exists("Cargo.toml"));
553        assert!(project.exists("Makefile"));
554        assert!(project.exists("Makefile.toml"));
555        assert!(project.exists("godot/default_env.tres"));
556        assert!(project.exists("godot/export_presets.cfg"));
557        assert!(project.exists("godot/native/game.gdnlib"));
558        assert!(project.exists("godot/project.godot"));
559        assert!(project.exists("rust/Cargo.toml"));
560        assert!(project.exists("rust/src/lib.rs"));
561        assert!(!project.exists("LICENSE"));
562        assert!(!project.exists(".travis.yml"));
563        assert!(!project.exists("sh"));
564        assert!(project.read(".gitignore").contains(".ftw"));
565        assert!(project.read(".gitignore").contains("bin/*"));
566        assert!(project.read(".gitignore").contains("export_presets.cfg"));
567        assert!(project.read(".gitignore").contains("lib/*"));
568        assert!(project.read(".gitignore").contains(".tag"));
569        assert!(project.read(".tag").contains("v1.5.0"));
570        assert!(project
571            .read("rust/Cargo.toml")
572            .contains(&project.get_name()));
573    }
574
575    #[test]
576    fn test_process_ftw_command_new_with_v140_tag() {
577        let project = Project::new();
578        let cmd = FtwCommand::New {
579            project_name: project.get_name(),
580            template: FtwTemplate::default(),
581            tag: FtwTag::Tagged {
582                git_tag: String::from("v1.4.0"),
583            },
584        };
585        let _ = cmd.process();
586        assert!(project.exists(".gitignore"));
587        assert!(project.exists("Cargo.toml"));
588        assert!(project.exists("Makefile"));
589        assert!(project.exists("Makefile.toml"));
590        assert!(project.exists("godot/default_env.tres"));
591        assert!(project.exists("godot/export_presets.cfg"));
592        assert!(project.exists("godot/native/game.gdnlib"));
593        assert!(project.exists("godot/project.godot"));
594        assert!(project.exists("rust/Cargo.toml"));
595        assert!(project.exists("rust/src/lib.rs"));
596        assert!(!project.exists("LICENSE"));
597        assert!(!project.exists(".travis.yml"));
598        assert!(!project.exists("sh"));
599        assert!(project.read(".gitignore").contains(".ftw"));
600        assert!(project.read(".gitignore").contains("bin/*"));
601        assert!(project.read(".gitignore").contains("export_presets.cfg"));
602        assert!(project.read(".gitignore").contains("lib/*"));
603        assert!(project.read(".gitignore").contains(".tag"));
604        assert!(project.read(".tag").contains("v1.4.0"));
605        assert!(project
606            .read("rust/Cargo.toml")
607            .contains(&project.get_name()));
608    }
609
610    #[test]
611    fn test_process_ftw_command_new_with_v130_tag() {
612        let project = Project::new();
613        let cmd = FtwCommand::New {
614            project_name: project.get_name(),
615            template: FtwTemplate::default(),
616            tag: FtwTag::Tagged {
617                git_tag: String::from("v1.3.0"),
618            },
619        };
620        let _ = cmd.process();
621        assert!(project.exists(".gitignore"));
622        assert!(project.exists("Cargo.toml"));
623        assert!(project.exists("Makefile"));
624        assert!(project.exists("Makefile.toml"));
625        assert!(project.exists("godot/default_env.tres"));
626        assert!(project.exists("godot/export_presets.cfg"));
627        assert!(project.exists("godot/native/game.gdnlib"));
628        assert!(project.exists("godot/project.godot"));
629        assert!(project.exists("rust/Cargo.toml"));
630        assert!(project.exists("rust/src/lib.rs"));
631        assert!(!project.exists("LICENSE"));
632        assert!(!project.exists(".travis.yml"));
633        assert!(!project.exists("sh"));
634        assert!(project.read(".gitignore").contains(".ftw"));
635        assert!(project.read(".gitignore").contains("bin/*"));
636        assert!(project.read(".gitignore").contains("export_presets.cfg"));
637        assert!(project.read(".gitignore").contains("lib/*"));
638        assert!(project.read(".gitignore").contains(".tag"));
639        assert!(project.read(".tag").contains("v1.3.0"));
640        assert!(project
641            .read("rust/Cargo.toml")
642            .contains(&project.get_name()));
643    }
644
645    #[test]
646    fn test_process_ftw_command_new_with_v120_tag() {
647        let project = Project::new();
648        let cmd = FtwCommand::New {
649            project_name: project.get_name(),
650            template: FtwTemplate::default(),
651            tag: FtwTag::Tagged {
652                git_tag: String::from("v1.2.0"),
653            },
654        };
655        let _ = cmd.process();
656        assert!(project.exists(".gitignore"));
657        assert!(project.exists("Cargo.toml"));
658        assert!(project.exists("Makefile"));
659        assert!(project.exists("Makefile.toml"));
660        assert!(project.exists("godot/default_env.tres"));
661        assert!(project.exists("godot/export_presets.cfg"));
662        assert!(project.exists("godot/native/game.gdnlib"));
663        assert!(project.exists("godot/project.godot"));
664        assert!(project.exists("rust/Cargo.toml"));
665        assert!(project.exists("rust/src/lib.rs"));
666        assert!(!project.exists("LICENSE"));
667        assert!(!project.exists(".travis.yml"));
668        assert!(!project.exists("sh"));
669        assert!(!project.exists(".tag"));
670        assert!(project.read(".gitignore").contains(".ftw"));
671        assert!(project.read(".gitignore").contains("bin/*"));
672        assert!(project.read(".gitignore").contains("export_presets.cfg"));
673        assert!(project.read(".gitignore").contains("lib/*"));
674        assert!(project.read(".gitignore").contains(".tag"));
675        assert!(project
676            .read("rust/Cargo.toml")
677            .contains(&project.get_name()));
678    }
679
680    #[test]
681    fn test_process_ftw_command_class() {
682        let project = Project::new();
683        let cmd = FtwCommand::New {
684            project_name: project.get_name(),
685            template: FtwTemplate::default(),
686            tag: FtwTag::default(),
687        };
688        let _ = cmd.process();
689        let _ = env::set_current_dir(Path::new(&project.get_name()));
690        let cmd = FtwCommand::Class {
691            class_name: "MyPlayer".to_string(),
692            node_type: FtwNodeType::Area2D,
693        };
694        let _ = cmd.process();
695        let _ = env::set_current_dir(Path::new("../"));
696        assert!(project.exists("rust/src/my_player.rs"));
697        assert!(project.exists("godot/native/MyPlayer.gdns"));
698        assert!(project.exists("godot/scenes/MyPlayer.tscn"));
699        assert!(project.exists("rust/src/lib.rs"));
700        assert!(project
701            .read("rust/src/my_player.rs")
702            .contains("pub struct MyPlayer"));
703        assert!(project
704            .read("rust/src/my_player.rs")
705            .contains("#[inherit(Area2D)]"));
706        assert!(project
707            .read("godot/native/MyPlayer.gdns")
708            .contains("resource_name = \"MyPlayer\""));
709        assert!(project
710            .read("godot/native/MyPlayer.gdns")
711            .contains("class_name = \"MyPlayer\""));
712        assert!(project
713            .read("godot/scenes/MyPlayer.tscn")
714            .contains("[ext_resource path=\"res://native/MyPlayer.gdns\" type=\"Script\" id=1]"));
715        assert!(project
716            .read("godot/scenes/MyPlayer.tscn")
717            .contains("[node name=\"MyPlayer\" type=\"Area2D\"]"));
718        assert!(project.read("rust/src/lib.rs").contains("mod my_player;"));
719        assert!(project
720            .read("rust/src/lib.rs")
721            .contains("handle.add_class::<my_player::MyPlayer>();"));
722    }
723
724    #[test]
725    fn test_process_ftw_command_tool_class() {
726        let project = Project::new();
727        let cmd = FtwCommand::New {
728            project_name: project.get_name(),
729            template: FtwTemplate::default(),
730            tag: FtwTag::default(),
731        };
732        let _ = cmd.process();
733        let _ = env::set_current_dir(Path::new(&project.get_name()));
734        let cmd = FtwCommand::Class {
735            class_name: "MyButtonTool".to_string(),
736            node_type: FtwNodeType::Button,
737        };
738        let _ = cmd.process();
739        let _ = env::set_current_dir(Path::new("../"));
740        assert!(project.exists("rust/src/my_button_tool.rs"));
741        assert!(project.exists("godot/native/MyButtonTool.gdns"));
742        assert!(project.exists("godot/scenes/MyButtonTool.tscn"));
743        assert!(project.exists("rust/src/lib.rs"));
744        assert!(project
745            .read("rust/src/my_button_tool.rs")
746            .contains("pub struct MyButtonTool"));
747        assert!(project
748            .read("rust/src/my_button_tool.rs")
749            .contains("#[inherit(Button)]"));
750        assert!(project
751            .read("godot/native/MyButtonTool.gdns")
752            .contains("resource_name = \"MyButtonTool\""));
753        assert!(project
754            .read("godot/native/MyButtonTool.gdns")
755            .contains("class_name = \"MyButtonTool\""));
756        assert!(project.read("godot/scenes/MyButtonTool.tscn").contains(
757            "[ext_resource path=\"res://native/MyButtonTool.gdns\" type=\"Script\" id=1]"
758        ));
759        assert!(project
760            .read("godot/scenes/MyButtonTool.tscn")
761            .contains("[node name=\"MyButtonTool\" type=\"Button\"]"));
762        assert!(project
763            .read("rust/src/lib.rs")
764            .contains("mod my_button_tool;"));
765        assert!(project
766            .read("rust/src/lib.rs")
767            .contains("handle.add_tool_class::<my_button_tool::MyButtonTool>();"));
768    }
769
770    #[test]
771    fn test_process_ftw_command_class_with_subs() {
772        let project = Project::new();
773        let cmd = FtwCommand::New {
774            project_name: project.get_name(),
775            template: FtwTemplate::default(),
776            tag: FtwTag::default(),
777        };
778        let _ = cmd.process();
779        let _ = env::set_current_dir(Path::new(&project.get_name()));
780        let cmd = FtwCommand::Class {
781            class_name: "foo/bar/baz/MyPlayer".to_string(),
782            node_type: FtwNodeType::Area2D,
783        };
784        let _ = cmd.process();
785        let _ = env::set_current_dir(Path::new("../"));
786        assert!(project.exists("rust/src/foo/bar/baz/my_player.rs"));
787        assert!(project.exists("rust/src/foo/bar/baz/mod.rs"));
788        assert!(project.exists("rust/src/foo/bar/mod.rs"));
789        assert!(project.exists("rust/src/foo/mod.rs"));
790        assert!(project.exists("godot/native/foo/bar/baz/MyPlayer.gdns"));
791        assert!(project.exists("godot/scenes/foo/bar/baz/MyPlayer.tscn"));
792        assert!(project.exists("rust/src/lib.rs"));
793        assert!(project
794            .read("rust/src/foo/bar/baz/my_player.rs")
795            .contains("pub struct MyPlayer"));
796        assert!(project
797            .read("rust/src/foo/bar/baz/my_player.rs")
798            .contains("#[inherit(Area2D)]"));
799        assert!(project
800            .read("godot/native/foo/bar/baz/MyPlayer.gdns")
801            .contains("resource_name = \"MyPlayer\""));
802        assert!(project
803            .read("godot/native/foo/bar/baz/MyPlayer.gdns")
804            .contains("class_name = \"MyPlayer\""));
805        assert!(project
806            .read("godot/scenes/foo/bar/baz/MyPlayer.tscn")
807            .contains(
808            "[ext_resource path=\"res://native/foo/bar/baz/MyPlayer.gdns\" type=\"Script\" id=1]"
809        ));
810        assert!(project
811            .read("godot/scenes/foo/bar/baz/MyPlayer.tscn")
812            .contains("[node name=\"MyPlayer\" type=\"Area2D\"]"));
813        assert!(project.read("rust/src/lib.rs").contains("mod foo;"));
814        assert!(project
815            .read("rust/src/lib.rs")
816            .contains("handle.add_class::<foo::bar::baz::my_player::MyPlayer>();"));
817        assert!(project
818            .read("rust/src/foo/bar/baz/mod.rs")
819            .contains("pub mod my_player;"));
820        assert!(project
821            .read("rust/src/foo/bar/mod.rs")
822            .contains("pub mod baz;"));
823        assert!(project.read("rust/src/foo/mod.rs").contains("pub mod bar;"));
824    }
825
826    #[test]
827    fn test_process_ftw_command_singleton() {
828        let project = Project::new();
829        let cmd = FtwCommand::New {
830            project_name: project.get_name(),
831            template: FtwTemplate::default(),
832            tag: FtwTag::default(),
833        };
834        let _ = cmd.process();
835        let _ = env::set_current_dir(Path::new(&project.get_name()));
836        let cmd = FtwCommand::Singleton {
837            class_name: "MyPlayer".to_string(),
838        };
839        let _ = cmd.process();
840        let _ = env::set_current_dir(Path::new("../"));
841        assert!(project.exists("rust/src/my_player.rs"));
842        assert!(project.exists("godot/native/MyPlayer.gdns"));
843        assert!(project.exists("rust/src/lib.rs"));
844        assert!(project
845            .read("rust/src/my_player.rs")
846            .contains("pub struct MyPlayer"));
847        assert!(project
848            .read("rust/src/my_player.rs")
849            .contains("#[inherit(Node)]"));
850        assert!(project
851            .read("godot/native/MyPlayer.gdns")
852            .contains("resource_name = \"MyPlayer\""));
853        assert!(project
854            .read("godot/native/MyPlayer.gdns")
855            .contains("class_name = \"MyPlayer\""));
856        assert!(project.read("rust/src/lib.rs").contains("mod my_player;"));
857        assert!(project
858            .read("rust/src/lib.rs")
859            .contains("handle.add_class::<my_player::MyPlayer>();"));
860    }
861
862    #[test]
863    fn test_process_ftw_command_build() {
864        let project = Project::new();
865        let cmd = FtwCommand::New {
866            project_name: project.get_name(),
867            template: FtwTemplate::default(),
868            tag: FtwTag::default(),
869        };
870        let _ = cmd.process();
871        let _ = env::set_current_dir(Path::new(&project.get_name()));
872        let target = util::get_current_platform().parse().unwrap();
873        let targets = vec![target];
874        let cmd = FtwCommand::Build {
875            targets,
876            build_type: FtwBuildType::Debug,
877        };
878        let _ = cmd.process();
879        let _ = env::set_current_dir(Path::new("../"));
880        assert!(project
881            .read("rust/Cargo.toml")
882            .contains(&project.get_name()));
883        let target_cli_arg = target.to_cli_arg();
884        let target_lib_prefix = target.to_lib_prefix();
885        let project_name = project.get_name();
886        let target_lib_ext = target.to_lib_ext();
887        assert!(project.exists(&format!(
888            "lib/{target_cli_arg}/{target_lib_prefix}{project_name}.{target_lib_ext}"
889        )));
890    }
891
892    #[test]
893    fn test_process_ftw_command_cross_build_multi_target() {
894        let project = Project::new();
895        let cmd = FtwCommand::New {
896            project_name: project.get_name(),
897            template: FtwTemplate::default(),
898            tag: FtwTag::default(),
899        };
900        let _ = cmd.process();
901        let contents = r#"[ftw]
902enable-cross-compilation=true
903"#;
904        let _ = project.create(".ftw", contents);
905        assert!(project
906            .read(".ftw")
907            .contains("enable-cross-compilation=true"));
908        let _ = env::set_current_dir(Path::new(&project.get_name()));
909        let targets = vec![
910            FtwTarget::AndroidLinuxX86_64,
911            FtwTarget::MacOsX86_64,
912            FtwTarget::LinuxX86_64,
913            FtwTarget::WindowsX86_64Gnu,
914            FtwTarget::MacOsAarch64,
915            FtwTarget::IosAarch64,
916        ];
917        let cmd = FtwCommand::Build {
918            targets: targets.clone(),
919            build_type: FtwBuildType::Debug,
920        };
921        let _ = cmd.process();
922        let cmd = FtwCommand::Clean;
923        let _ = cmd.process();
924        let _ = env::set_current_dir(Path::new("../"));
925        assert!(project
926            .read("rust/Cargo.toml")
927            .contains(&project.get_name()));
928        for target in targets {
929            let target_cli_arg = target.to_cli_arg();
930            let target_lib_prefix = target.to_lib_prefix();
931            let project_name = project.get_name();
932            let target_lib_ext = target.to_lib_ext();
933            assert!(project.exists(&format!(
934                "lib/{target_cli_arg}/{target_lib_prefix}{project_name}.{target_lib_ext}"
935            )));
936        }
937    }
938
939    #[test]
940    fn test_process_ftw_command_build_2x() {
941        let project = Project::new();
942        let cmd = FtwCommand::New {
943            project_name: project.get_name(),
944            template: FtwTemplate::default(),
945            tag: FtwTag::default(),
946        };
947        let _ = cmd.process();
948        let _ = env::set_current_dir(Path::new(&project.get_name()));
949        let target = util::get_current_platform().parse().unwrap();
950        let targets = vec![target];
951        let cmd = FtwCommand::Build {
952            targets,
953            build_type: FtwBuildType::Debug,
954        };
955        let _ = cmd.process();
956        let _ = cmd.process();
957        let _ = env::set_current_dir(Path::new("../"));
958        assert!(project
959            .read("rust/Cargo.toml")
960            .contains(&project.get_name()));
961        let target_cli_arg = target.to_cli_arg();
962        let target_lib_prefix = target.to_lib_prefix();
963        let project_name = project.get_name();
964        let target_lib_ext = target.to_lib_ext();
965        assert!(project.exists(&format!(
966            "lib/{target_cli_arg}/{target_lib_prefix}{project_name}.{target_lib_ext}"
967        )));
968    }
969
970    #[test]
971    fn test_process_ftw_command_build_release() {
972        let project = Project::new();
973        let cmd = FtwCommand::New {
974            project_name: project.get_name(),
975            template: FtwTemplate::default(),
976            tag: FtwTag::default(),
977        };
978        let _ = cmd.process();
979        let _ = env::set_current_dir(Path::new(&project.get_name()));
980        let target = util::get_current_platform().parse().unwrap();
981        let targets = vec![target];
982        let cmd = FtwCommand::Build {
983            targets,
984            build_type: FtwBuildType::Release,
985        };
986        let _ = cmd.process();
987        let _ = env::set_current_dir(Path::new("../"));
988        assert!(project
989            .read("rust/Cargo.toml")
990            .contains(&project.get_name()));
991        let target_cli_arg = target.to_cli_arg();
992        let target_lib_prefix = target.to_lib_prefix();
993        let project_name = project.get_name();
994        let target_lib_ext = target.to_lib_ext();
995        assert!(project.exists(&format!(
996            "lib/{target_cli_arg}/{target_lib_prefix}{project_name}.{target_lib_ext}"
997        )));
998    }
999
1000    #[test]
1001    fn test_process_ftw_command_export() {
1002        let project = Project::new();
1003        let cmd = FtwCommand::New {
1004            project_name: project.get_name(),
1005            template: FtwTemplate::default(),
1006            tag: FtwTag::default(),
1007        };
1008        let _ = cmd.process();
1009        let _ = env::set_current_dir(Path::new(&project.get_name()));
1010        let target = util::get_current_platform().parse().unwrap();
1011        let targets = vec![target];
1012        let cmd = FtwCommand::Export {
1013            targets,
1014            build_type: FtwBuildType::Debug,
1015        };
1016        let _ = cmd.process();
1017        let _ = env::set_current_dir(Path::new("../"));
1018        assert!(project
1019            .read("rust/Cargo.toml")
1020            .contains(&project.get_name()));
1021        let target_cli_arg = target.to_cli_arg();
1022        let target_lib_prefix = target.to_lib_prefix();
1023        let project_name = project.get_name();
1024        let target_lib_ext = target.to_lib_ext();
1025        let target_app_ext = target.to_app_ext();
1026        if target.is_linux() {
1027            assert!(project.exists(&format!(
1028                "bin/{target_cli_arg}/{target_lib_prefix}{project_name}.{target_lib_ext}"
1029            )));
1030            assert!(project.exists(&format!(
1031                "bin/{target_cli_arg}/{project_name}.debug.{target_cli_arg}.pck"
1032            )));
1033        }
1034        if target.is_windows() {
1035            assert!(project.exists(&format!(
1036                "bin/{target_cli_arg}/{target_lib_prefix}{project_name}.{target_lib_ext}"
1037            )));
1038            assert!(project.exists(&format!(
1039                "bin/{target_cli_arg}/{project_name}.debug.{target_cli_arg}.pck"
1040            )));
1041        }
1042        assert!(project.exists(&format!(
1043            "bin/{target_cli_arg}/{project_name}.debug.{target_cli_arg}{target_app_ext}"
1044        )));
1045    }
1046
1047    #[test]
1048    fn test_process_ftw_command_cross_export_multi_target() {
1049        let project = Project::new();
1050        let project_name = project.get_name();
1051        let cmd = FtwCommand::New {
1052            project_name: project_name.clone(),
1053            template: FtwTemplate::default(),
1054            tag: FtwTag::default(),
1055        };
1056        let _ = cmd.process();
1057        let contents = r#"[ftw]
1058enable-cross-compilation=true
1059"#;
1060        let _ = project.create(".ftw", contents);
1061        assert!(project
1062            .read(".ftw")
1063            .contains("enable-cross-compilation=true"));
1064        let _ = env::set_current_dir(Path::new(&project_name.clone()));
1065        Command::new("cargo")
1066            .arg("make")
1067            .arg("switch-gdnlib-msvc-to-gnu-entry")
1068            .assert()
1069            .success();
1070        Command::new("cargo")
1071            .arg("make")
1072            .arg("create-debug-keystore")
1073            .assert()
1074            .success();
1075        Command::new("cargo")
1076            .arg("make")
1077            .arg("create-release-keystore")
1078            .arg(&project_name)
1079            .assert()
1080            .success();
1081        let targets = vec![
1082            FtwTarget::AndroidLinuxAarch64,
1083            FtwTarget::AndroidLinuxArmV7,
1084            FtwTarget::LinuxX86_64,
1085            FtwTarget::MacOsX86_64,
1086            FtwTarget::WindowsX86_64Gnu,
1087            FtwTarget::MacOsAarch64,
1088            FtwTarget::IosAarch64,
1089        ];
1090        let cmd = FtwCommand::Export {
1091            targets: targets.clone(),
1092            build_type: FtwBuildType::Debug,
1093        };
1094        let _ = cmd.process();
1095        let cmd = FtwCommand::Clean;
1096        let _ = cmd.process();
1097        let _ = env::set_current_dir(Path::new("../"));
1098        assert!(project.read("rust/Cargo.toml").contains(&project_name));
1099        for target in targets {
1100            let target_cli_arg = target.to_cli_arg();
1101            let target_lib_prefix = target.to_lib_prefix();
1102            let project_name = project.get_name();
1103            let target_lib_ext = target.to_lib_ext();
1104            let target_app_ext = target.to_app_ext();
1105            assert!(project.exists(&format!(
1106                "lib/{target_cli_arg}/{target_lib_prefix}{project_name}.{target_lib_ext}"
1107            )));
1108            if target.is_linux() {
1109                assert!(project.exists(&format!(
1110                    "bin/{target_cli_arg}/{target_lib_prefix}{project_name}.{target_lib_ext}"
1111                )));
1112                assert!(project.exists(&format!(
1113                    "bin/{target_cli_arg}/{project_name}.debug.{target_cli_arg}.pck"
1114                )));
1115            }
1116            if target.is_windows() {
1117                assert!(project.exists(&format!(
1118                    "bin/{target_cli_arg}/{target_lib_prefix}{project_name}.{target_lib_ext}"
1119                )));
1120                assert!(project.exists(&format!(
1121                    "bin/{target_cli_arg}/{project_name}.debug.{target_cli_arg}.pck"
1122                )));
1123            }
1124            if target.is_ios() {
1125                assert!(project.exists(&format!(
1126                    "bin/{target_cli_arg}/{project_name}.debug.{target_cli_arg}.pck"
1127                )));
1128                assert!(project.exists(&format!(
1129                    "bin/{target_cli_arg}/{project_name}.debug.{target_cli_arg}.xcframework"
1130                )));
1131                assert!(project.exists(&format!(
1132                    "bin/{target_cli_arg}/{project_name}.debug.{target_cli_arg}.xcodeproj"
1133                )));
1134                assert!(project.exists(&format!(
1135                    "bin/{target_cli_arg}/{project_name}.debug.{target_cli_arg}"
1136                )));
1137            }
1138            if target.is_android() {
1139                assert!(project.exists(&format!(
1140                    "bin/{target_cli_arg}/{project_name}.debug.{target_cli_arg}{target_app_ext}.idsig"
1141                )));
1142            }
1143            if !target.is_ios() {
1144                assert!(project.exists(&format!(
1145                    "bin/{target_cli_arg}/{project_name}.debug.{target_cli_arg}{target_app_ext}"
1146                )));
1147            }
1148        }
1149    }
1150}