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