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