Skip to main content

sails_cli/
program_new.rs

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