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