Skip to main content

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