1use anyhow::Context as _;
2use core::slice;
3use std::ffi::OsStr;
4use std::fs;
5use std::fs::OpenOptions;
6use std::io::Write;
7use std::path::Path;
8use std::path::PathBuf;
9use std::process::Command;
10use strum::Display;
11use toml_edit::value;
12use toml_edit::Array;
13use toml_edit::Document;
14use toml_edit::Item;
15use toml_edit::Table;
16
17use crate::core::manifest::Manifest;
18use crate::templates::load_template_config;
19use crate::util::config::get_package_name;
20use crate::util::config::Config;
21use crate::util::restricted_names;
22use crate::util::to_snake_case;
23use crate::util::SubstrateResult;
24
25#[derive(Debug, Display)]
26pub enum Template {
27 Substrate,
29 Cumulus,
30 Frontier,
31 Canvas,
32 CargoContract,
34 Custom(String),
36}
37
38#[derive(Debug)]
39pub struct NewOptions {
40 pub template: Template,
41 pub path: PathBuf,
43 pub name: Option<String>,
44}
45
46pub fn validate_rust_installation() -> SubstrateResult<()> {
48 let info = String::from_utf8_lossy(
49 &Command::new("rustup")
50 .args(vec!["run", "nightly", "rustup", "show"])
52 .output()?
53 .stdout,
54 )
55 .to_string();
56
57 if !info.contains("wasm32-unknown-unknown") || !info.contains("nightly") {
58 println!("\nRust nightly toolchain is not installed. Installing...\n");
59 Command::new("rustup")
62 .args(vec!["default", "stable"])
63 .status()?;
64 Command::new("rustup").args(vec!["update"]).status()?;
65 Command::new("rustup")
66 .args(vec!["update", "nightly"])
67 .status()?;
68 Command::new("rustup")
69 .args(vec![
70 "target",
71 "add",
72 "wasm32-unknown-unknown",
73 "--toolchain",
74 "nightly",
75 ])
76 .status()?;
77 }
78
79 Ok(())
80}
81
82fn get_name(opts: &NewOptions) -> SubstrateResult<&str> {
83 if let Some(ref name) = opts.name {
84 return Ok(name);
85 }
86
87 let path = &opts.path;
88 let file_name = path.file_name().ok_or_else(|| {
89 anyhow::format_err!(
90 "cannot auto-detect package name from path {:?} ; use --name to override",
91 path.as_os_str()
92 )
93 })?;
94
95 file_name.to_str().ok_or_else(|| {
96 anyhow::format_err!(
97 "cannot create package with a non-unicode name: {:?}",
98 file_name
99 )
100 })
101}
102
103fn get_parent(opts: &NewOptions) -> SubstrateResult<&str> {
104 let path = &opts.path;
105 let parent = path.parent().ok_or_else(|| {
106 anyhow::format_err!(
107 "cannot auto-detect package parent directory from path {:?}",
108 path.as_os_str()
109 )
110 })?;
111
112 parent.to_str().ok_or_else(|| {
113 anyhow::format_err!(
114 "cannot create package with a non-unicode name: {:?}",
115 parent
116 )
117 })
118}
119
120fn validate_path(path: &Path) -> SubstrateResult<()> {
122 if cargo_util::paths::join_paths(slice::from_ref(&OsStr::new(path)), "").is_err() {
123 let path = path.to_string_lossy();
124 anyhow::bail!(
125 "the path `{path}` contains invalid PATH characters (usually `:`, `;`, or `\"`)\n\
126 It is recommended to use a different name to avoid problems."
127 );
128 }
129 Ok(())
130}
131
132fn validate_name(name: &str, show_name_help: bool) -> SubstrateResult<()> {
133 let name_help = if show_name_help {
136 "\nIf you need a package name to not match the directory name, consider using --name flag."
137 } else {
138 ""
139 };
140 let bin_help = String::from(name_help);
141
142 restricted_names::validate_package_name(name, "package name", &bin_help)?;
143
144 if restricted_names::is_keyword(name) {
145 anyhow::bail!(
146 "the name `{}` cannot be used as a package name, it is a Rust keyword{}",
147 name,
148 bin_help
149 );
150 }
151 if restricted_names::is_conflicting_artifact_name(name) {
152 anyhow::bail!(
153 "the name `{}` cannot be used as a package name, \
154 it conflicts with cargo's build directory names{}",
155 name,
156 name_help
157 );
158 }
159 if name == "test" {
160 anyhow::bail!(
161 "the name `test` cannot be used as a package name, \
162 it conflicts with Rust's built-in test library{}",
163 bin_help
164 );
165 }
166 if ["core", "std", "alloc", "proc_macro", "proc-macro"].contains(&name) {
167 let warning = format!(
168 "the name `{}` is part of Rust's standard library\n\
169 It is recommended to use a different name to avoid problems.{}",
170 name, bin_help
171 );
172
173 println!("{}", warning);
174 }
175 if restricted_names::is_windows_reserved(name) {
176 if cfg!(windows) {
177 anyhow::bail!(
178 "cannot use name `{}`, it is a reserved Windows filename{}",
179 name,
180 name_help
181 );
182 } else {
183 let warning = format!(
184 "the name `{}` is a reserved Windows filename\n\
185 This package will not work on Windows platforms.",
186 name
187 );
188 println!("{}", warning);
189 }
190 }
191 if restricted_names::is_non_ascii_name(name) {
192 let warning = format!(
193 "the name `{}` contains non-ASCII characters\n\
194 Non-ASCII crate names are not supported by Rust.",
195 name
196 );
197 println!("{}", warning);
198 }
199
200 Ok(())
201}
202
203fn print_start_hacking_message(cwd: &Path, path: &Path) {
204 println!("\nStart hacking by typing:\n");
205 if let Ok(relative_path) = path.strip_prefix(cwd) {
206 println!("cd {}", relative_path.display());
207 } else {
208 println!("cd {}", path.display());
209 }
210 println!("substrate-manager");
211}
212
213pub fn new_chain(opts: &NewOptions, config: &Config) -> SubstrateResult<()> {
214 let path = &opts.path;
215 if path.exists() {
216 anyhow::bail!(
217 "destination `{}` already exists\n\n\
218 Use `substrate init` to initialize the directory",
219 path.display()
220 )
221 }
222
223 validate_path(path)?;
224
225 let name = get_name(opts)?;
226 validate_name(name, opts.name.is_none())?;
227
228 validate_rust_installation()?;
229
230 println!("Creating new chain...\n");
231
232 generate_node_template(&opts.template, &path)?;
233
234 mk_chain(opts, name)?;
235
236 println!("\nCreated chain `{}`!", name);
237 print_start_hacking_message(config.cwd(), path);
238
239 Ok(())
240}
241
242pub fn new_contract(opts: &NewOptions, config: &Config) -> SubstrateResult<()> {
243 let path = &opts.path;
244 if path.exists() {
245 anyhow::bail!(
246 "destination `{}` already exists\n\n\
247 Use `substrate init` to initialize the directory",
248 path.display()
249 )
250 }
251
252 validate_path(path)?;
253
254 let name = get_name(opts)?;
255 validate_name(name, opts.name.is_none())?;
256
257 validate_rust_installation()?;
258 validate_cargo_contract_installation()?;
259
260 println!("Creating new contract...");
261 create_smart_contract(name, &opts.path)?;
262 mk_contract(opts, name)?;
263
264 print_start_hacking_message(config.cwd(), path);
265
266 Ok(())
267}
268
269fn get_git_commit_id(path: &Path) -> String {
271 let commit_id_output = Command::new("git")
272 .current_dir(path)
273 .args(["rev-parse", "HEAD"])
274 .output()
275 .expect("git rev-parse failed")
276 .stdout;
277
278 let commit_id = String::from_utf8_lossy(&commit_id_output);
279
280 let commit_id = commit_id.trim().to_string();
281 commit_id
282}
283
284fn find_cargo_tomls(path: &Path) -> Vec<PathBuf> {
286 let path = format!("{}/**/Cargo.toml", path.display());
287
288 let glob = glob::glob(&path).expect("Generates globbing pattern");
289
290 let mut result = Vec::new();
291 glob.into_iter().for_each(|file| match file {
292 Ok(file) => result.push(file),
293 Err(e) => println!("{:?}", e),
294 });
295
296 if result.is_empty() {
297 panic!("Did not find any `Cargo.toml` files.");
298 }
299
300 result
301}
302
303fn find_rust_files(path: &Path) -> Vec<PathBuf> {
305 let path = format!("{}/**/*.rs", path.display());
306
307 let glob = glob::glob(&path).expect("Generates globbing pattern");
308
309 let mut result = Vec::new();
310 glob.into_iter().for_each(|file| match file {
311 Ok(file) => result.push(file),
312 Err(e) => println!("{:?}", e),
313 });
314
315 if result.is_empty() {
316 panic!("Did not find any `.rs` files.");
317 }
318
319 result
320}
321
322fn process_and_replace_dependencies(
325 dependencies: &mut Table,
326 remote: &str,
327 commit_id: &str,
328 cargo_toml_path: &Path,
329) {
330 for (_, dep_value) in dependencies.iter_mut() {
331 if let Some(dep_table) = dep_value.as_inline_table_mut() {
332 if let Some(path_value) = dep_table.get("path").and_then(|p| p.as_str()) {
333 let full_path = cargo_toml_path.join(path_value);
334 if !full_path.exists() {
335 dep_table.remove("path");
336 dep_table.insert("git", remote.into());
337 dep_table.insert("rev", commit_id.into());
338 }
339 }
340 *dep_value = value(dep_table.clone());
341 }
342 }
343}
344
345fn replace_path_dependencies_with_git(
347 cargo_toml_path: &Path,
348 remote: &str,
349 commit_id: &str,
350 cargo_toml: &mut Document,
351) {
352 let mut cargo_toml_path = cargo_toml_path.to_path_buf();
353 cargo_toml_path.pop();
355
356 for &table in &["dependencies", "build-dependencies", "dev-dependencies"] {
358 if let Some(dependencies) = cargo_toml[table].as_table_mut() {
359 process_and_replace_dependencies(dependencies, remote, commit_id, &cargo_toml_path);
360 }
361 }
362
363 if let Some(workspace_deps) = cargo_toml
365 .get_mut("workspace")
366 .and_then(|w| w["dependencies"].as_table_mut())
367 {
368 process_and_replace_dependencies(workspace_deps, remote, commit_id, &cargo_toml_path);
369 }
370}
371
372fn update_top_level_cargo_toml(
377 cargo_toml: &mut Document,
378 workspace_members: Vec<&PathBuf>,
379 node_template_generated_folder: &Path,
380) {
381 let mut panic_unwind = Table::new();
382 panic_unwind.insert("panic", value("unwind"));
383
384 let mut profile = Table::new();
385 profile.insert("release", Item::Table(panic_unwind));
386
387 cargo_toml.insert("profile", Item::Table(profile));
388
389 let members = workspace_members
390 .iter()
391 .map(|p| {
392 p.strip_prefix(node_template_generated_folder)
393 .expect("Workspace member is a child of the node template path!")
394 .parent()
395 .expect("The given path ends with `Cargo.toml` as file name!")
398 .display()
399 .to_string()
400 })
401 .collect::<Array>();
402
403 cargo_toml
408 .as_table_mut()
409 .entry("workspace")
410 .or_insert(toml_edit::table())
411 .as_table_mut()
412 .unwrap()
413 .insert("members", value(members));
414}
415
416pub fn generate_node_template(template: &Template, path: &Path) -> SubstrateResult<()> {
417 let template_config = if let Template::Custom(template_config_path) = template {
418 load_template_config(template_config_path)?
419 } else {
420 load_template_config(&template.to_string())?
421 };
422
423 Command::new("git")
424 .args([
425 "clone",
426 "--filter=blob:none",
427 "--depth",
428 "1",
429 "--sparse",
430 "--branch",
431 &template_config.branch,
432 &template_config.remote,
433 path.as_os_str()
434 .to_str()
435 .expect("invalid characters in path"),
436 ])
437 .status()?;
438
439 let commit_id = get_git_commit_id(path);
441
442 Command::new("git")
443 .current_dir(path)
444 .args(["sparse-checkout", "add", &template_config.template_path])
445 .status()?;
446
447 fs::remove_dir_all(path.join(".git"))?;
449
450 if let Ok(entries) = fs::read_dir(path) {
451 for entry in entries {
452 if let Ok(entry) = entry {
453 let entry_path = entry.path();
454
455 if let Some(file_name) = entry_path.file_name().and_then(|n| n.to_str()) {
456 if entry_path.is_file()
457 && !file_name.contains("rustfmt.toml")
458 && !file_name.contains("Cargo")
459 {
460 fs::remove_file(entry_path)?;
461 }
462 }
463 }
464 }
465 }
466
467 let local_template_path = path.join(template_config.template_path);
468
469 for entry in fs::read_dir(&local_template_path)? {
470 let entry = entry?;
471 let entry_path = entry.path();
472 let relative_path = path.join(entry_path.file_name().unwrap());
473 let dest_path = path.join(relative_path);
474
475 fs::rename(&entry_path, &dest_path)?;
476 }
477
478 fs::remove_dir_all(local_template_path)?;
479
480 let top_level_cargo_toml_path = path.join("Cargo.toml");
481 let mut cargo_tomls = find_cargo_tomls(path);
482
483 if !cargo_tomls.contains(&top_level_cargo_toml_path) {
485 OpenOptions::new()
487 .create(true)
488 .write(true)
489 .open(&top_level_cargo_toml_path)
490 .expect("Create root level `Cargo.toml` failed.");
491
492 cargo_tomls.push(PathBuf::from(&top_level_cargo_toml_path));
494 }
495
496 cargo_tomls.iter().for_each(|t| {
497 let mut cargo_toml = Manifest::new(t.to_path_buf());
498 let mut cargo_toml_document = cargo_toml.read_document().expect("Read Cargo.toml failed.");
499 replace_path_dependencies_with_git(
501 t,
502 &template_config.remote,
503 &commit_id,
504 &mut cargo_toml_document,
505 );
506
507 if top_level_cargo_toml_path == t.to_path_buf() {
509 let workspace_members = cargo_tomls
511 .iter()
512 .filter(|p| **p != top_level_cargo_toml_path)
513 .collect();
514
515 update_top_level_cargo_toml(&mut cargo_toml_document, workspace_members, path);
516 }
517
518 cargo_toml
519 .write_document(cargo_toml_document)
520 .expect("Write Cargo.toml failed.");
521 });
522
523 Ok(())
524}
525
526pub fn validate_cargo_contract_installation() -> SubstrateResult<()> {
527 if which::which("cargo-contract").is_err() {
528 Command::new("cargo")
530 .args(["install", "--force", "--locked", "cargo-contract"])
531 .status()?;
532 }
533
534 Ok(())
535}
536
537pub fn create_smart_contract(name: &str, path: &Path) -> SubstrateResult<()> {
538 let path_binding = PathBuf::from("");
539 let parent = path.parent().unwrap_or(&path_binding);
540 fs::create_dir_all(parent)?;
542
543 let status = Command::new("cargo-contract")
544 .args(["contract", "new", name, "-t", parent.to_str().unwrap()])
545 .status()?;
546
547 let dir_from_path = path.file_name().unwrap();
549 if name != dir_from_path {
550 fs::rename(parent.join(name), path)?;
551 }
552
553 if !status.success() {
554 return Err(anyhow::anyhow!("failed to create smart contract"));
555 }
556
557 Ok(())
558}
559
560fn replace_occurrence_in_file(file_path: &Path, original: &str, new: &str) -> SubstrateResult<()> {
561 if file_path.exists() {
562 let file = fs::read_to_string(file_path)?;
563 let new_file = file.replace(original, new);
564 let mut file = OpenOptions::new()
565 .write(true)
566 .truncate(true)
567 .open(file_path)?;
568 file.write_all(new_file.as_bytes())?;
569 }
570 Ok(())
571}
572
573pub fn mk_chain(opts: &NewOptions, name: &str) -> SubstrateResult<()> {
574 let node_path = opts.path.join("node");
575 let node_manifest_path = node_path.join("Cargo.toml");
576 let runtime_path = opts.path.join("runtime");
577 let runtime_manifest_path = runtime_path.join("Cargo.toml");
578 let substrate_manifest_path = opts.path.join("Substrate.toml");
579
580 let original_runtime_package_name =
583 get_package_name(&runtime_path)?.expect("Runtime package name exists");
584 let original_runtime_package_name_snake = to_snake_case(&original_runtime_package_name);
585 let original_node_package_name =
586 get_package_name(&node_path)?.expect("Node package version exists");
587 let _original_node_package_name_snake = to_snake_case(&original_node_package_name);
588 let node_package_name = name.to_string() + "-node";
589 let _node_package_name_snake = to_snake_case(&node_package_name);
590 let runtime_package_name = name.to_string() + "-runtime";
591 let runtime_package_name_snake = to_snake_case(&runtime_package_name);
592
593 let mut node_manifest = Manifest::new(node_manifest_path);
594 let mut node_document = node_manifest.read_document()?;
595
596 let mut runtime_manifest = Manifest::new(runtime_manifest_path);
597 let mut runtime_document = runtime_manifest.read_document()?;
598
599 let mut substrate_manifest = Manifest::new(substrate_manifest_path);
600 let mut substrate_document = Document::new();
601
602 node_document["package"]["name"] = value(&node_package_name);
604 if node_document.get("bin").is_some() {
605 node_document["bin"][0]["name"] = value(&node_package_name);
606 }
607
608 let item = node_document["dependencies"]
615 .as_table_mut()
616 .unwrap()
617 .remove_entry(&original_runtime_package_name)
618 .unwrap()
619 .1;
620 node_document["dependencies"][&runtime_package_name] = item;
621
622 let mut root_manifest = Manifest::new(opts.path.join("Cargo.toml"));
624 let mut root_document = root_manifest.read_document()?;
625 if let Some(workspace_deps) = root_document["workspace"]["dependencies"].as_table_mut() {
626 if let Some(mut runtime_dep) = workspace_deps.remove_entry(&original_runtime_package_name) {
627 let dep_table = runtime_dep.1.as_inline_table_mut().unwrap();
628 dep_table.remove("git");
629 dep_table.remove("rev");
630 let path = runtime_path.strip_prefix(&opts.path)?;
631 dep_table.insert("path", path.to_str().unwrap().into());
632 root_document["workspace"]["dependencies"][&runtime_package_name] = runtime_dep.1;
633 root_manifest.write_document(root_document)?;
634 }
635 }
636
637 for feature in node_document["features"].as_table_mut().unwrap().iter_mut() {
639 for arr in feature.1.as_array_mut().unwrap().iter_mut() {
640 if arr
641 .as_str()
642 .unwrap()
643 .contains(&original_runtime_package_name)
644 {
645 *arr = arr
646 .as_str()
647 .unwrap()
648 .replace(&original_runtime_package_name, &runtime_package_name)
649 .into();
650 }
651 }
652 }
653
654 runtime_document["package"]["name"] = value(&runtime_package_name);
655
656 let node_rust_files = find_rust_files(&node_path);
657 for file in node_rust_files {
660 replace_occurrence_in_file(
661 &file,
662 &original_runtime_package_name_snake,
663 &runtime_package_name_snake,
664 )?;
665 }
666
667 substrate_document.insert("type", value("chain"));
668
669 node_manifest.write_document(node_document)?;
671 runtime_manifest.write_document(runtime_document)?;
672 substrate_manifest.write_document(substrate_document)?;
673
674 Ok(())
675}
676
677pub fn mk_contract(opts: &NewOptions, _name: &str) -> SubstrateResult<()> {
678 let substrate_manifest_path = opts.path.join("Substrate.toml");
679 let mut substrate_manifest = Manifest::new(substrate_manifest_path);
680 let mut substrate_document = Document::new();
681
682 substrate_document.insert("type", value("contract"));
683 substrate_manifest.write_document(substrate_document)?;
684
685 Ok(())
686}