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