Skip to main content

sails_cli/
program_new.rs

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        // add sails-rs refs
330        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        // add sails-rs refs
373        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        // update `sails-rs` if not path ref and not offline
385        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        // add app ref
403        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        // add sails-rs refs
415        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        // add app ref
427        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        // add sails-rs refs
442        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        // add tokio
453        cargo_add(
454            manifest_path,
455            ["tokio"],
456            Development,
457            Some("rt,macros"),
458            self.offline,
459        )?;
460
461        // add app ref
462        cargo_add(
463            manifest_path,
464            [self.app_name()],
465            Development,
466            None,
467            self.offline,
468        )?;
469        // add client ref
470        cargo_add(
471            manifest_path,
472            [self.client_name()],
473            Development,
474            None,
475            self.offline,
476        )?;
477
478        // add tests
479        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()) // Don't pollute output
526        .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()) // Don't pollute output
651        .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()) // Don't pollute output
720        .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}