Skip to main content

sails_cli/
program_new.rs

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