1use anyhow::Context;
2use askama::Template;
3use cargo_metadata::DependencyKind::{Build, Development, Normal};
4use chrono::{Datelike, Utc};
5use convert_case::{Case, Casing};
6use std::{
7 env,
8 ffi::{OsStr, OsString},
9 fs::{self, File},
10 io::{self, Write},
11 path::{Path, PathBuf},
12 process::{Command, ExitStatus, Output, Stdio},
13};
14
15const SAILS_VERSION: &str = env!("CARGO_PKG_VERSION");
16const TOKIO_VERSION: &str = "1.50.0";
17const ICON_CONFIG: &str = "📋";
18const ICON_WORKSPACE: &str = "âš“";
19const ICON_APP: &str = "📦";
20const ICON_CLIENT: &str = "📡";
21const ICON_BUILD: &str = "🔨";
22const ICON_TESTS: &str = "🔬";
23const ICON_FORMAT: &str = "✨";
24const ICON_DONE: &str = "✅";
25const CRATES_IO: &str = "crates-io";
26
27trait ExitStatusExt: Sized {
28 fn exit_result(self) -> io::Result<()>;
29}
30
31impl ExitStatusExt for ExitStatus {
32 fn exit_result(self) -> io::Result<()> {
33 if self.success() {
34 Ok(())
35 } else {
36 Err(io::Error::from(io::ErrorKind::Other))
37 }
38 }
39}
40
41trait OutputExt: Sized {
42 fn exit_result(self) -> io::Result<Self>;
43}
44
45impl OutputExt for Output {
46 fn exit_result(self) -> io::Result<Self> {
47 if self.status.success() {
48 Ok(self)
49 } else {
50 Err(io::Error::from(io::ErrorKind::Other))
51 }
52 }
53}
54
55#[derive(Template)]
56#[template(path = ".github/workflows/ci.askama")]
57struct CIWorkflow {
58 git_branch_name: String,
59 client_file_name: String,
60}
61
62#[derive(Template)]
63#[template(path = "app/src/lib.askama")]
64struct AppLib {
65 service_name: String,
66 service_name_snake: String,
67 program_struct_name: String,
68 eth: bool,
69}
70
71#[derive(Template)]
72#[template(path = "client/src/lib.askama")]
73struct ClientLib {
74 client_file_name: String,
75}
76
77#[derive(Template)]
78#[template(path = "client/build.askama")]
79struct ClientBuild {
80 app_crate_name: String,
81 program_struct_name: String,
82}
83
84#[derive(Template)]
85#[template(path = "src/lib.askama")]
86struct RootLib {
87 app_crate_name: String,
88}
89
90#[derive(Template)]
91#[template(path = "tests/gtest.askama")]
92struct TestsGtest {
93 program_crate_name: String,
94 client_crate_name: String,
95 client_program_name: String,
96 service_name: String,
97 service_name_snake: String,
98 eth: bool,
99}
100
101#[derive(Template)]
102#[template(path = "build.askama")]
103struct RootBuild {
104 app_crate_name: String,
105 program_struct_name: String,
106}
107
108#[derive(Template)]
109#[template(path = "license.askama")]
110struct RootLicense {
111 copyright_year: String,
112 package_author: String,
113}
114
115#[derive(Template)]
116#[template(path = "readme.askama")]
117struct RootReadme {
118 program_crate_name: String,
119 github_username: String,
120 app_crate_name: String,
121 client_crate_name: String,
122 service_name: String,
123}
124
125#[derive(Template)]
126#[template(path = "rust-toolchain.askama")]
127struct RootRustToolchain;
128
129pub struct ProgramGenerator {
130 path: PathBuf,
131 package_name: String,
132 package_author: String,
133 github_username: String,
134 client_file_name: String,
135 sails_path: Option<PathBuf>,
136 offline: bool,
137 eth: bool,
138 service_name: String,
139 program_struct_name: String,
140}
141
142impl ProgramGenerator {
143 const DEFAULT_AUTHOR: &str = "Gear Technologies";
144 const DEFAULT_GITHUB_USERNAME: &str = "gear-tech";
145
146 const GITIGNORE_ENTRIES: &[&str] =
147 &[".binpath", ".DS_Store", ".vscode", ".idea", "/target", ""];
148
149 pub fn new(
150 path: PathBuf,
151 name: Option<String>,
152 author: Option<String>,
153 username: Option<String>,
154 sails_path: Option<PathBuf>,
155 offline: bool,
156 eth: bool,
157 ) -> Self {
158 let package_name = name.map_or_else(
159 || {
160 path.file_name()
161 .expect("Invalid Path")
162 .to_str()
163 .expect("Invalid UTF-8 Path")
164 .to_case(Case::Kebab)
165 },
166 |name| name.to_case(Case::Kebab),
167 );
168 let service_name = package_name.to_case(Case::Pascal);
169 let package_author = author.unwrap_or_else(|| Self::DEFAULT_AUTHOR.to_string());
170 let github_username = username.unwrap_or_else(|| Self::DEFAULT_GITHUB_USERNAME.to_string());
171 let client_file_name = format!("{}_client", package_name.to_case(Case::Snake));
172 Self {
173 path,
174 package_name,
175 package_author,
176 github_username,
177 client_file_name,
178 sails_path,
179 offline,
180 eth,
181 service_name,
182 program_struct_name: "Program".to_string(),
183 }
184 }
185
186 fn ci_workflow(&self, git_branch_name: &str) -> CIWorkflow {
187 CIWorkflow {
188 git_branch_name: git_branch_name.into(),
189 client_file_name: self.client_file_name.clone(),
190 }
191 }
192
193 fn app_lib(&self) -> AppLib {
194 AppLib {
195 service_name: self.service_name.clone(),
196 service_name_snake: self.service_name.to_case(Case::Snake),
197 program_struct_name: self.program_struct_name.clone(),
198 eth: self.eth,
199 }
200 }
201
202 fn client_lib(&self) -> ClientLib {
203 ClientLib {
204 client_file_name: self.client_file_name.clone(),
205 }
206 }
207
208 fn client_build(&self) -> ClientBuild {
209 ClientBuild {
210 app_crate_name: self.app_name().to_case(Case::Snake),
211 program_struct_name: self.program_struct_name.clone(),
212 }
213 }
214
215 fn root_lib(&self) -> RootLib {
216 RootLib {
217 app_crate_name: self.app_name().to_case(Case::Snake),
218 }
219 }
220
221 fn tests_gtest(&self) -> TestsGtest {
222 TestsGtest {
223 program_crate_name: self.package_name.to_case(Case::Snake),
224 client_crate_name: self.client_name().to_case(Case::Snake),
225 client_program_name: self.client_name().to_case(Case::Pascal),
226 service_name: self.service_name.clone(),
227 service_name_snake: self.service_name.to_case(Case::Snake),
228 eth: self.eth,
229 }
230 }
231
232 fn root_build(&self) -> RootBuild {
233 RootBuild {
234 app_crate_name: self.app_name().to_case(Case::Snake),
235 program_struct_name: self.program_struct_name.clone(),
236 }
237 }
238
239 fn root_license(&self) -> RootLicense {
240 RootLicense {
241 copyright_year: Utc::now().year().to_string(),
242 package_author: self.package_author.clone(),
243 }
244 }
245
246 fn root_readme(&self) -> RootReadme {
247 RootReadme {
248 program_crate_name: self.package_name.clone(),
249 github_username: self.github_username.clone(),
250 app_crate_name: self.app_name(),
251 client_crate_name: self.client_name(),
252 service_name: self.service_name.clone(),
253 }
254 }
255
256 fn root_rust_toolchain(&self) -> RootRustToolchain {
257 RootRustToolchain
258 }
259
260 fn app_path(&self) -> PathBuf {
261 self.path.join("app")
262 }
263
264 fn app_name(&self) -> String {
265 format!("{}-app", self.package_name)
266 }
267
268 fn client_path(&self) -> PathBuf {
269 self.path.join("client")
270 }
271
272 fn client_name(&self) -> String {
273 format!("{}-client", self.package_name)
274 }
275
276 fn cargo_add_sails_rs<P: AsRef<Path>>(
277 &self,
278 manifest_path: P,
279 dependency: cargo_metadata::DependencyKind,
280 features: Option<&str>,
281 ) -> anyhow::Result<()> {
282 let sails_package = ["sails-rs"];
283 cargo_add(
284 manifest_path,
285 sails_package,
286 dependency,
287 features,
288 self.offline,
289 )
290 }
291
292 fn print_config(&self) {
293 let sails_source = self
294 .sails_path
295 .as_ref()
296 .map(|path| path.display().to_string())
297 .unwrap_or_else(|| format!("crates.io:{SAILS_VERSION}"));
298 let print_field = |label: &str, value: &dyn std::fmt::Display| {
299 println!(" {label:<10} {value}");
300 };
301
302 println!("{ICON_CONFIG} Program config:");
303 print_field("path:", &self.path.display());
304 print_field("package:", &self.package_name);
305 print_field("author:", &self.package_author);
306 print_field("username:", &self.github_username);
307 print_field("sails-rs:", &sails_source);
308 print_field("offline:", &self.offline);
309 print_field("eth:", &self.eth);
310 }
311
312 pub fn generate(self) -> anyhow::Result<()> {
313 println!("⛵ Creating new Sails program...");
314 self.print_config();
315
316 println!("{ICON_WORKSPACE} [1/6] Initializing workspace...");
317 self.generate_root()?;
318 println!("{ICON_APP} [2/6] Generating app crate...");
319 self.generate_app()?;
320 println!("{ICON_CLIENT} [3/6] Generating client crate...");
321 self.generate_client()?;
322 println!("{ICON_BUILD} [4/6] Wiring root crate...");
323 self.generate_build()?;
324 println!("{ICON_TESTS} [5/6] Generating tests...");
325 self.generate_tests()?;
326 println!("{ICON_FORMAT} [6/6] Formatting workspace...");
327 self.fmt()?;
328 println!("{ICON_DONE} Done.");
329 Ok(())
330 }
331
332 fn generate_app(&self) -> anyhow::Result<()> {
333 let path = &self.app_path();
334 cargo_new(path, &self.app_name(), self.offline, false)?;
335 let manifest_path = &manifest_path(path);
336
337 self.cargo_add_sails_rs(manifest_path, Normal, self.eth.then_some("ethexe"))?;
339 self.cargo_add_sails_rs(
341 manifest_path,
342 Development,
343 Some(if self.eth { "ethexe,std" } else { "std" }),
344 )?;
345
346 let mut lib_rs = File::create(lib_rs_path(path))?;
347 self.app_lib().write_into(&mut lib_rs)?;
348
349 Ok(())
350 }
351
352 fn generate_root(&self) -> anyhow::Result<()> {
353 let path = &self.path;
354 cargo_new(path, &self.package_name, self.offline, true)?;
355
356 let git_branch_name = git_show_current_branch(path)?;
357 println!(" git branch: {git_branch_name}");
358
359 fs::create_dir_all(ci_workflow_dir_path(path))?;
360 let mut ci_workflow_yml = File::create(ci_workflow_path(path))?;
361 self.ci_workflow(&git_branch_name)
362 .write_into(&mut ci_workflow_yml)?;
363
364 let mut gitignore = File::create(gitignore_path(path))?;
365 gitignore.write_all(Self::GITIGNORE_ENTRIES.join("\n").as_ref())?;
366
367 let manifest_path = &manifest_path(path);
368 cargo_toml_create_workspace_and_fill_package(
369 manifest_path,
370 &self.package_name,
371 &self.package_author,
372 &self.github_username,
373 &self.sails_path,
374 )?;
375
376 let mut license = File::create(license_path(path))?;
377 self.root_license().write_into(&mut license)?;
378
379 let mut readme_md = File::create(readme_path(path))?;
380 self.root_readme().write_into(&mut readme_md)?;
381
382 let mut rust_toolchain_toml = File::create(rust_toolchain_path(path))?;
383 self.root_rust_toolchain()
384 .write_into(&mut rust_toolchain_toml)?;
385
386 self.cargo_add_sails_rs(manifest_path, Normal, self.eth.then_some("ethexe"))?;
388
389 if self.sails_path.is_none() && !self.offline {
391 cargo_info("sails-idl-embed")?;
393 cargo_info("sails-idl-gen")?;
394 cargo_info("sails-client-gen-v2")?;
395 cargo_info("sails-idl-parser-v2")?;
396 }
397
398 self.cargo_add_sails_rs(
399 manifest_path,
400 Build,
401 Some(if self.eth { "ethexe,build" } else { "build" }),
402 )?;
403
404 Ok(())
405 }
406
407 fn generate_build(&self) -> anyhow::Result<()> {
408 let path = &self.path;
409 let manifest_path = &manifest_path(path);
410
411 let mut lib_rs = File::create(lib_rs_path(path))?;
412 self.root_lib().write_into(&mut lib_rs)?;
413
414 let mut build_rs = File::create(build_rs_path(path))?;
415 self.root_build().write_into(&mut build_rs)?;
416
417 cargo_add(manifest_path, [self.app_name()], Normal, None, self.offline)?;
419 cargo_add(manifest_path, [self.app_name()], Build, None, self.offline)?;
420
421 Ok(())
422 }
423
424 fn generate_client(&self) -> anyhow::Result<()> {
425 let path = &self.client_path();
426 cargo_new(path, &self.client_name(), self.offline, false)?;
427
428 let manifest_path = &manifest_path(path);
429 self.cargo_add_sails_rs(manifest_path, Normal, self.eth.then_some("ethexe"))?;
431 self.cargo_add_sails_rs(
432 manifest_path,
433 Build,
434 Some(if self.eth { "ethexe,build" } else { "build" }),
435 )?;
436
437 cargo_add(manifest_path, [self.app_name()], Build, None, self.offline)?;
439
440 let mut build_rs = File::create(build_rs_path(path))?;
441 self.client_build().write_into(&mut build_rs)?;
442
443 let mut lib_rs = File::create(lib_rs_path(path))?;
444 self.client_lib().write_into(&mut lib_rs)?;
445
446 Ok(())
447 }
448
449 fn generate_tests(&self) -> anyhow::Result<()> {
450 let path = &self.path;
451 let manifest_path = &manifest_path(path);
452 self.cargo_add_sails_rs(
454 manifest_path,
455 Development,
456 Some(if self.eth { "ethexe,gtest" } else { "gtest" }),
457 )?;
458
459 cargo_add(
461 manifest_path,
462 ["tokio"],
463 Development,
464 Some("rt,macros"),
465 self.offline,
466 )?;
467
468 cargo_add(
470 manifest_path,
471 [self.app_name()],
472 Development,
473 None,
474 self.offline,
475 )?;
476 cargo_add(
478 manifest_path,
479 [self.client_name()],
480 Development,
481 None,
482 self.offline,
483 )?;
484
485 let test_path = &tests_path(path);
487 fs::create_dir_all(test_path.parent().context("Parent should exists")?)?;
488 let mut gtest_rs = File::create(test_path)?;
489 self.tests_gtest().write_into(&mut gtest_rs)?;
490
491 Ok(())
492 }
493
494 fn fmt(&self) -> anyhow::Result<()> {
495 let manifest_path = &manifest_path(&self.path);
496 cargo_fmt(manifest_path)
497 }
498}
499
500fn git_show_current_branch<P: AsRef<Path>>(target_dir: P) -> anyhow::Result<String> {
501 let git_command = git_command();
502 let mut cmd = Command::new(git_command);
503 cmd.stdout(Stdio::piped())
504 .arg("-C")
505 .arg(target_dir.as_ref())
506 .arg("branch")
507 .arg("--show-current");
508
509 let output = cmd
510 .output()?
511 .exit_result()
512 .context("failed to get current git branch")?;
513 let git_branch_name = String::from_utf8(output.stdout)?;
514
515 Ok(git_branch_name.trim().into())
516}
517
518fn cargo_new<P: AsRef<Path>>(
519 target_dir: P,
520 name: &str,
521 offline: bool,
522 root: bool,
523) -> anyhow::Result<()> {
524 let cargo_command = cargo_command();
525 let target_dir = target_dir.as_ref();
526 let cargo_new_or_init = if target_dir.exists() { "init" } else { "new" };
527 println!(
528 " cargo {cargo_new_or_init}: {name} -> {}",
529 target_dir.display()
530 );
531 let mut cmd = Command::new(cargo_command);
532 cmd.stdout(Stdio::null()) .arg(cargo_new_or_init)
534 .arg(target_dir)
535 .arg("--name")
536 .arg(name)
537 .arg("--lib")
538 .arg("--quiet");
539
540 if offline {
541 cmd.arg("--offline");
542 }
543
544 cmd.status()
545 .context("failed to execute `cargo new` command")?
546 .exit_result()
547 .context("failed to run `cargo new` command")?;
548
549 if !root {
550 let manifest_path = target_dir.join("Cargo.toml");
551 let cargo_toml = fs::read_to_string(&manifest_path)?;
552 let mut document: toml_edit::DocumentMut = cargo_toml.parse()?;
553
554 let crate_path = name
555 .rsplit_once('-')
556 .map(|(_, crate_path)| crate_path)
557 .unwrap_or(name);
558 let description = match crate_path {
559 "app" => "Package containing business logic for the program",
560 "client" => {
561 "Package containing the client for the program allowing to interact with it"
562 }
563 _ => unreachable!(),
564 };
565
566 let package = document
567 .entry("package")
568 .or_insert_with(toml_edit::table)
569 .as_table_mut()
570 .context("package was not a table in Cargo.toml")?;
571
572 let mut entries = vec![];
573
574 for key in ["repository", "license", "keywords", "categories"] {
575 if let Some(entry) = package.remove_entry(key) {
576 entries.push(entry);
577 }
578 }
579
580 _ = package
581 .entry("description")
582 .or_insert_with(|| toml_edit::value(description));
583
584 for (key, item) in entries {
585 package.insert_formatted(&key, item);
586 }
587
588 fs::write(manifest_path, document.to_string())?;
589
590 if let Some(parent_dir) = target_dir.parent() {
591 let manifest_path = parent_dir.join("Cargo.toml");
592 let cargo_toml = fs::read_to_string(&manifest_path)?;
593 let mut document: toml_edit::DocumentMut = cargo_toml.parse()?;
594
595 let workspace = document
596 .entry("workspace")
597 .or_insert_with(toml_edit::table)
598 .as_table_mut()
599 .context("workspace was not a table in Cargo.toml")?;
600
601 let dependencies = workspace
602 .entry("dependencies")
603 .or_insert_with(toml_edit::table)
604 .as_table_mut()
605 .context("workspace.dependencies was not a table in Cargo.toml")?;
606
607 let mut dependency = toml_edit::InlineTable::new();
608 dependency.insert("version", "0.1.0".into());
609 dependency.insert("path", crate_path.into());
610
611 dependencies.insert(name, dependency.into());
612
613 fs::write(manifest_path, document.to_string())?;
614 }
615 }
616
617 Ok(())
618}
619
620fn cargo_add<P, I, S>(
621 manifest_path: P,
622 packages: I,
623 dependency: cargo_metadata::DependencyKind,
624 features: Option<&str>,
625 offline: bool,
626) -> anyhow::Result<()>
627where
628 P: AsRef<Path>,
629 I: IntoIterator<Item = S>,
630 S: AsRef<OsStr>,
631{
632 let cargo_command = cargo_command();
633 let package_args = packages
634 .into_iter()
635 .map(|package| package.as_ref().to_os_string())
636 .collect::<Vec<OsString>>();
637 let package_names = package_args
638 .iter()
639 .map(|package| package.to_string_lossy().into_owned())
640 .collect::<Vec<_>>()
641 .join(", ");
642 let dep_kind = match dependency {
643 Development => "dev-dependency",
644 Build => "build-dependency",
645 Normal => "dependency",
646 _ => "dependency",
647 };
648 let feature_suffix = features
649 .map(|features| format!(" [features: {features}]"))
650 .unwrap_or_default();
651 println!(
652 " cargo add: {package_names} -> {} ({dep_kind}){feature_suffix}",
653 manifest_path.as_ref().display()
654 );
655
656 let mut cmd = Command::new(cargo_command);
657 cmd.stdout(Stdio::null()) .arg("add")
659 .args(&package_args)
660 .arg("--manifest-path")
661 .arg(manifest_path.as_ref())
662 .arg("--quiet");
663
664 match dependency {
665 Development => {
666 cmd.arg("--dev");
667 }
668 Build => {
669 cmd.arg("--build");
670 }
671 _ => (),
672 };
673
674 if let Some(features) = features {
675 cmd.arg("--features").arg(features);
676 }
677
678 if offline {
679 cmd.arg("--offline");
680 }
681
682 cmd.status()
683 .context("failed to execute `cargo add` command")?
684 .exit_result()
685 .context("failed to run `cargo add` command")?;
686
687 Ok(())
688}
689
690#[allow(unused)]
691fn cargo_update<P: AsRef<Path>>(manifest_path: P, package: Option<&str>) -> anyhow::Result<()> {
692 let cargo_command = cargo_command();
693 if let Some(package) = package {
694 println!(
695 " cargo update: {} -> {}",
696 package,
697 manifest_path.as_ref().display()
698 );
699 } else {
700 println!(" cargo update: {}", manifest_path.as_ref().display());
701 }
702
703 let mut cmd = Command::new(cargo_command);
704 cmd.stdout(Stdio::null()).arg("update");
705
706 if let Some(package) = package {
707 cmd.arg(package);
708 }
709
710 cmd.arg("--manifest-path")
711 .arg(manifest_path.as_ref())
712 .arg("--quiet");
713
714 cmd.status()
715 .context("failed to execute `cargo update` command")?
716 .exit_result()
717 .context("failed to run `cargo update` command")?;
718
719 Ok(())
720}
721
722fn cargo_info(package: &str) -> anyhow::Result<()> {
723 let cargo_command = cargo_command();
724 let package_version = &format!("{package}@{SAILS_VERSION}");
725 println!(" cargo info: {package_version}");
726
727 let mut cmd = Command::new(cargo_command);
728
729 cmd.stdout(Stdio::null())
730 .arg("info")
731 .arg(package_version)
732 .arg("--registry")
733 .arg(CRATES_IO)
734 .arg("--quiet");
735
736 cmd.status()
737 .context("failed to execute `cargo info` command")?
738 .exit_result()
739 .context("failed to run `cargo info` command")?;
740
741 Ok(())
742}
743
744fn cargo_fmt<P: AsRef<Path>>(manifest_path: P) -> anyhow::Result<()> {
745 let cargo_command = cargo_command();
746 println!(" cargo fmt: {}", manifest_path.as_ref().display());
747
748 let mut cmd = Command::new(cargo_command);
749 cmd.stdout(Stdio::null()) .arg("fmt")
751 .arg("--manifest-path")
752 .arg(manifest_path.as_ref())
753 .arg("--all")
754 .arg("--quiet");
755
756 cmd.status()
757 .context("failed to execute `cargo fmt` command")?
758 .exit_result()
759 .context("failed to run `cargo fmt` command")
760}
761
762fn cargo_toml_create_workspace_and_fill_package<P: AsRef<Path>>(
763 manifest_path: P,
764 name: &str,
765 author: &str,
766 username: &str,
767 sails_path: &Option<PathBuf>,
768) -> anyhow::Result<()> {
769 let manifest_path = manifest_path.as_ref();
770 let cargo_toml = fs::read_to_string(manifest_path)?;
771 let mut document: toml_edit::DocumentMut = cargo_toml.parse()?;
772
773 let package = document
774 .entry("package")
775 .or_insert_with(toml_edit::table)
776 .as_table_mut()
777 .context("package was not a table in Cargo.toml")?;
778 package.remove("edition");
779 for key in [
780 "version",
781 "authors",
782 "edition",
783 "rust-version",
784 "description",
785 "repository",
786 "license",
787 "keywords",
788 "categories",
789 ] {
790 if key == "description" {
791 _ = package.entry(key).or_insert_with(|| {
792 toml_edit::value(
793 "Package allowing to build WASM binary for the program and IDL file for it",
794 )
795 });
796 } else {
797 let item = package.entry(key).or_insert_with(toml_edit::table);
798 let mut table = toml_edit::Table::new();
799 table.insert("workspace", toml_edit::value(true));
800 table.set_dotted(true);
801 *item = table.into();
802 }
803 }
804
805 for key in ["dev-dependencies", "build-dependencies"] {
806 _ = document
807 .entry(key)
808 .or_insert_with(toml_edit::table)
809 .as_table_mut()
810 .with_context(|| format!("package.{key} was not a table in Cargo.toml"))?;
811 }
812
813 let workspace = document
814 .entry("workspace")
815 .or_insert_with(toml_edit::table)
816 .as_table_mut()
817 .context("workspace was not a table in Cargo.toml")?;
818 _ = workspace
819 .entry("resolver")
820 .or_insert_with(|| toml_edit::value("3"));
821 _ = workspace
822 .entry("members")
823 .or_insert_with(|| toml_edit::value(toml_edit::Array::new()));
824
825 let workspace_package = workspace
826 .entry("package")
827 .or_insert_with(toml_edit::table)
828 .as_table_mut()
829 .context("workspace.package was not a table in Cargo.toml")?;
830 _ = workspace_package
831 .entry("version")
832 .or_insert_with(|| toml_edit::value("0.1.0"));
833 let mut authors = toml_edit::Array::new();
834 authors.push(author);
835 _ = workspace_package
836 .entry("authors")
837 .or_insert_with(|| toml_edit::value(authors));
838 for (key, value) in [
839 ("edition", "2024".into()),
840 ("rust-version", "1.91".into()),
841 (
842 "repository",
843 format!("https://github.com/{username}/{name}"),
844 ),
845 ("license", "MIT".into()),
846 ] {
847 _ = workspace_package
848 .entry(key)
849 .or_insert_with(|| toml_edit::value(value));
850 }
851 let keywords =
852 toml_edit::Array::from_iter(["gear", "sails", "smart-contracts", "wasm", "no-std"]);
853 _ = workspace_package
854 .entry("keywords")
855 .or_insert_with(|| toml_edit::value(keywords));
856 let categories =
857 toml_edit::Array::from_iter(["cryptography::cryptocurrencies", "no-std", "wasm"]);
858 _ = workspace_package
859 .entry("categories")
860 .or_insert_with(|| toml_edit::value(categories));
861
862 let dependencies = workspace
863 .entry("dependencies")
864 .or_insert_with(toml_edit::table)
865 .as_table_mut()
866 .context("workspace.dependencies was not a table in Cargo.toml")?;
867
868 if let Some(sails_path) = sails_path {
869 let mut dependency = toml_edit::InlineTable::new();
870 dependency.insert(
871 "path",
872 sails_path
873 .canonicalize()?
874 .to_str()
875 .context("failed to convert to UTF-8 string")?
876 .into(),
877 );
878 dependencies.insert("sails-rs", dependency.into());
879 } else {
880 dependencies.insert("sails-rs", SAILS_VERSION.into());
881 }
882
883 dependencies.insert("tokio", TOKIO_VERSION.into());
884
885 fs::write(manifest_path, document.to_string())?;
886
887 Ok(())
888}
889
890fn ci_workflow_dir_path<P: AsRef<Path>>(path: P) -> PathBuf {
891 path.as_ref().join(".github/workflows")
892}
893
894fn ci_workflow_path<P: AsRef<Path>>(path: P) -> PathBuf {
895 path.as_ref().join(".github/workflows/ci.yml")
896}
897
898fn gitignore_path<P: AsRef<Path>>(path: P) -> PathBuf {
899 path.as_ref().join(".gitignore")
900}
901
902fn manifest_path<P: AsRef<Path>>(path: P) -> PathBuf {
903 path.as_ref().join("Cargo.toml")
904}
905
906fn build_rs_path<P: AsRef<Path>>(path: P) -> PathBuf {
907 path.as_ref().join("build.rs")
908}
909
910fn lib_rs_path<P: AsRef<Path>>(path: P) -> PathBuf {
911 path.as_ref().join("src/lib.rs")
912}
913
914fn tests_path<P: AsRef<Path>>(path: P) -> PathBuf {
915 path.as_ref().join("tests/gtest.rs")
916}
917
918fn license_path<P: AsRef<Path>>(path: P) -> PathBuf {
919 path.as_ref().join("LICENSE")
920}
921
922fn readme_path<P: AsRef<Path>>(path: P) -> PathBuf {
923 path.as_ref().join("README.md")
924}
925
926fn rust_toolchain_path<P: AsRef<Path>>(path: P) -> PathBuf {
927 path.as_ref().join("rust-toolchain.toml")
928}
929
930fn git_command() -> String {
931 env::var("GIT").unwrap_or("git".into())
932}
933
934fn cargo_command() -> String {
935 env::var("CARGO").unwrap_or("cargo".into())
936}