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