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