linera_service/
project.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    io::Write,
6    path::{Path, PathBuf},
7    process::Command,
8};
9
10use anyhow::{ensure, Context, Result};
11use cargo_toml::Manifest;
12use convert_case::{Case, Casing};
13use current_platform::CURRENT_PLATFORM;
14use fs_err::File;
15use tracing::debug;
16
17pub struct Project {
18    root: PathBuf,
19}
20
21impl Project {
22    pub fn create_new(name: &str, linera_root: Option<&Path>) -> Result<Self> {
23        ensure!(
24            !name.contains(std::path::is_separator),
25            "Project name {name} should not contain path-separators",
26        );
27        let root = PathBuf::from(name);
28        ensure!(
29            !root.exists(),
30            "Directory {} already exists",
31            root.display(),
32        );
33        ensure!(
34            root.extension().is_none(),
35            "Project name {name} should not have a file extension",
36        );
37        debug!("Creating directory at {}", root.display());
38        fs_err::create_dir_all(&root)?;
39
40        debug!("Creating the source directory");
41        let source_directory = Self::create_source_directory(&root)?;
42
43        debug!("Creating the tests directory");
44        let test_directory = Self::create_test_directory(&root)?;
45
46        debug!("Initializing git repository");
47        Self::initialize_git_repository(&root)?;
48
49        debug!("Writing Cargo.toml");
50        Self::create_cargo_toml(&root, name, linera_root)?;
51
52        debug!("Writing rust-toolchain.toml");
53        Self::create_rust_toolchain(&root)?;
54
55        debug!("Writing state.rs");
56        Self::create_state_file(&source_directory, name)?;
57
58        debug!("Writing lib.rs");
59        Self::create_lib_file(&source_directory, name)?;
60
61        debug!("Writing contract.rs");
62        Self::create_contract_file(&source_directory, name)?;
63
64        debug!("Writing service.rs");
65        Self::create_service_file(&source_directory, name)?;
66
67        debug!("Writing single_chain.rs");
68        Self::create_test_file(&test_directory, name)?;
69
70        Ok(Self { root })
71    }
72
73    pub fn from_existing_project(root: PathBuf) -> Result<Self> {
74        ensure!(
75            root.exists(),
76            "could not find project at {}",
77            root.display()
78        );
79        Ok(Self { root })
80    }
81
82    /// Runs the unit and integration tests of an application.
83    pub async fn test(&self) -> Result<()> {
84        let tests = Command::new("cargo")
85            .arg("test")
86            .args(["--target", CURRENT_PLATFORM])
87            .current_dir(&self.root)
88            .spawn()?
89            .wait()?;
90        ensure!(tests.success(), "tests failed");
91        Ok(())
92    }
93
94    /// Finds the workspace for a given crate. If the workspace
95    /// does not exist, returns the path of the crate.
96    fn workspace_root(&self) -> Result<&Path> {
97        let mut current_path = self.root.as_path();
98        loop {
99            let toml_path = current_path.join("Cargo.toml");
100            if toml_path.exists() {
101                let toml = Manifest::from_path(toml_path)?;
102                if toml.workspace.is_some() {
103                    return Ok(current_path);
104                }
105            }
106            match current_path.parent() {
107                None => {
108                    break;
109                }
110                Some(parent) => current_path = parent,
111            }
112        }
113        Ok(self.root.as_path())
114    }
115
116    fn create_source_directory(project_root: &Path) -> Result<PathBuf> {
117        let source_directory = project_root.join("src");
118        fs_err::create_dir(&source_directory)?;
119        Ok(source_directory)
120    }
121
122    fn create_test_directory(project_root: &Path) -> Result<PathBuf> {
123        let test_directory = project_root.join("tests");
124        fs_err::create_dir(&test_directory)?;
125        Ok(test_directory)
126    }
127
128    fn initialize_git_repository(project_root: &Path) -> Result<()> {
129        let output = Command::new("git")
130            .args([
131                "init",
132                project_root
133                    .to_str()
134                    .context("project name contains non UTF-8 characters")?,
135            ])
136            .output()?;
137
138        ensure!(
139            output.status.success(),
140            "failed to initialize git repository at {}",
141            &project_root.display()
142        );
143
144        Self::write_string_to_file(&project_root.join(".gitignore"), "/target")
145    }
146
147    fn create_cargo_toml(
148        project_root: &Path,
149        project_name: &str,
150        linera_root: Option<&Path>,
151    ) -> Result<()> {
152        let toml_path = project_root.join("Cargo.toml");
153        let (linera_sdk_dep, linera_sdk_dev_dep) = Self::linera_sdk_dependencies(linera_root);
154        let binary_root_name = project_name.replace('-', "_");
155        let contract_binary_name = format!("{binary_root_name}_contract");
156        let service_binary_name = format!("{binary_root_name}_service");
157        let toml_contents = format!(
158            include_str!("../template/Cargo.toml.template"),
159            project_name = project_name,
160            contract_binary_name = contract_binary_name,
161            service_binary_name = service_binary_name,
162            linera_sdk_dep = linera_sdk_dep,
163            linera_sdk_dev_dep = linera_sdk_dev_dep,
164        );
165        Self::write_string_to_file(&toml_path, &toml_contents)
166    }
167
168    fn create_rust_toolchain(project_root: &Path) -> Result<()> {
169        Self::write_string_to_file(
170            &project_root.join("rust-toolchain.toml"),
171            include_str!("../template/rust-toolchain.toml.template"),
172        )
173    }
174
175    fn create_state_file(source_directory: &Path, project_name: &str) -> Result<()> {
176        let project_name = project_name.to_case(Case::Pascal);
177        let state_path = source_directory.join("state.rs");
178        let file_content = format!(
179            include_str!("../template/state.rs.template"),
180            project_name = project_name
181        );
182        Self::write_string_to_file(&state_path, &file_content)
183    }
184
185    fn create_lib_file(source_directory: &Path, project_name: &str) -> Result<()> {
186        let project_name = project_name.to_case(Case::Pascal);
187        let state_path = source_directory.join("lib.rs");
188        let file_content = format!(
189            include_str!("../template/lib.rs.template"),
190            project_name = project_name
191        );
192        Self::write_string_to_file(&state_path, &file_content)
193    }
194
195    fn create_contract_file(source_directory: &Path, name: &str) -> Result<()> {
196        let project_name = name.to_case(Case::Pascal);
197        let contract_path = source_directory.join("contract.rs");
198        let contract_contents = format!(
199            include_str!("../template/contract.rs.template"),
200            module_name = name.replace('-', "_"),
201            project_name = project_name
202        );
203        Self::write_string_to_file(&contract_path, &contract_contents)
204    }
205
206    fn create_service_file(source_directory: &Path, name: &str) -> Result<()> {
207        let project_name = name.to_case(Case::Pascal);
208        let service_path = source_directory.join("service.rs");
209        let service_contents = format!(
210            include_str!("../template/service.rs.template"),
211            module_name = name.replace('-', "_"),
212            project_name = project_name
213        );
214        Self::write_string_to_file(&service_path, &service_contents)
215    }
216
217    fn create_test_file(test_directory: &Path, name: &str) -> Result<()> {
218        let project_name = name.to_case(Case::Pascal);
219        let test_path = test_directory.join("single_chain.rs");
220        let test_contents = format!(
221            include_str!("../template/tests/single_chain.rs.template"),
222            project_name = name.replace('-', "_"),
223            project_abi = project_name,
224        );
225        Self::write_string_to_file(&test_path, &test_contents)
226    }
227
228    fn write_string_to_file(path: &Path, content: &str) -> Result<()> {
229        let mut file = File::create(path)?;
230        file.write_all(content.as_bytes())?;
231        Ok(())
232    }
233
234    /// Resolves [`linera_sdk`] and [`linera_views`] dependencies.
235    fn linera_sdk_dependencies(linera_root: Option<&Path>) -> (String, String) {
236        match linera_root {
237            Some(path) => Self::linera_sdk_testing_dependencies(path),
238            None => Self::linera_sdk_production_dependencies(),
239        }
240    }
241
242    /// Resolves [`linera_sdk`] and [`linera_views`] dependencies in testing mode.
243    fn linera_sdk_testing_dependencies(linera_root: &Path) -> (String, String) {
244        // We're putting the Cargo.toml file one level above the current directory.
245        let linera_root = PathBuf::from("..").join(linera_root);
246        let linera_sdk_path = linera_root.join("linera-sdk");
247        let linera_sdk_dep = format!(
248            "linera-sdk = {{ path = \"{}\" }}",
249            linera_sdk_path.display()
250        );
251        let linera_sdk_dev_dep = format!(
252            "linera-sdk = {{ path = \"{}\", features = [\"test\", \"wasmer\"] }}",
253            linera_sdk_path.display()
254        );
255        (linera_sdk_dep, linera_sdk_dev_dep)
256    }
257
258    /// Adds [`linera_sdk`] dependencies in production mode.
259    fn linera_sdk_production_dependencies() -> (String, String) {
260        let version = env!("CARGO_PKG_VERSION");
261        let linera_sdk_dep = format!("linera-sdk = \"{}\"", version);
262        let linera_sdk_dev_dep = format!(
263            "linera-sdk = {{ version = \"{}\", features = [\"test\", \"wasmer\"] }}",
264            version
265        );
266        (linera_sdk_dep, linera_sdk_dev_dep)
267    }
268
269    pub fn build(&self, name: Option<String>) -> Result<(PathBuf, PathBuf), anyhow::Error> {
270        let name = match name {
271            Some(name) => name,
272            None => self.project_package_name()?.replace('-', "_"),
273        };
274        let contract_name = format!("{}_contract", name);
275        let service_name = format!("{}_service", name);
276        let cargo_build = Command::new("cargo")
277            .arg("build")
278            .arg("--release")
279            .args(["--target", "wasm32-unknown-unknown"])
280            .current_dir(&self.root)
281            .spawn()?
282            .wait()?;
283        ensure!(cargo_build.success(), "build failed");
284        let build_path = self
285            .workspace_root()?
286            .join("target/wasm32-unknown-unknown/release");
287        Ok((
288            build_path.join(contract_name).with_extension("wasm"),
289            build_path.join(service_name).with_extension("wasm"),
290        ))
291    }
292
293    fn project_package_name(&self) -> Result<String> {
294        let manifest = Manifest::from_path(self.cargo_toml_path())?;
295        let name = manifest
296            .package
297            .context("Cargo.toml is missing `[package]`")?
298            .name;
299        Ok(name)
300    }
301
302    fn cargo_toml_path(&self) -> PathBuf {
303        self.root.join("Cargo.toml")
304    }
305}