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 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 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 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 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 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 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 cargo_add(
449 manifest_path,
450 ["tokio"],
451 Development,
452 Some("rt,macros"),
453 self.offline,
454 )?;
455
456 cargo_add(
458 manifest_path,
459 [self.app_name()],
460 Development,
461 None,
462 self.offline,
463 )?;
464 cargo_add(
466 manifest_path,
467 [self.client_name()],
468 Development,
469 None,
470 self.offline,
471 )?;
472
473 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()) .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()) .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()) .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}