1use 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 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 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 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 fn linera_sdk_testing_dependencies(linera_root: &Path) -> (String, String) {
262 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 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}