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