Skip to main content

sails_cli/
program_new.rs

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