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