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