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