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 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 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(¤t_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 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}