substrate_manager/ops/
substrate_add.rs

1use std::{
2    fs,
3    path::{Path, PathBuf},
4    process::Command,
5};
6
7use anyhow::Ok;
8use regex::Regex;
9
10use crate::{
11    core::manifest::Manifest,
12    util::{to_pascal_case, to_snake_case, Config, SubstrateResult},
13};
14
15pub struct AddOptions {
16    /// The name of the project's package
17    pub package_name: String,
18    /// The path of the project's package
19    pub package_path: PathBuf,
20    /// The name of the crate
21    pub crate_spec: String,
22
23    /// Feature flags to activate
24    pub features: Vec<String>,
25    /// Whether the default feature should be activated
26    pub default_features: Option<bool>,
27
28    /// Source of the crate
29    pub source: CrateSource,
30}
31
32pub enum CrateSource {
33    DefaultRegistry,
34    Git(String, String),
35    Path(String),
36    CustomRegistry(String),
37}
38
39pub fn add_pallet_std_to_manifest(
40    cwd: &Path,
41    runtime_path: &Path,
42    crate_spec: &str,
43) -> SubstrateResult<()> {
44    let feature = format!("{}/std", crate_spec);
45    let mut runtime_manifest = Manifest::new(cwd.join(runtime_path).join("Cargo.toml"));
46    let mut runtime_document = runtime_manifest.read_document()?;
47    // Add pallet/std to features vector
48    let feature_array = runtime_document["features"].as_table_mut().unwrap()["std"]
49        .as_array_mut()
50        .unwrap();
51    if !feature_array.iter().any(|f| f.as_str().unwrap() == feature) {
52        feature_array.push::<String>(feature);
53    }
54
55    runtime_manifest.write_document(runtime_document)?;
56
57    Ok(())
58}
59
60// Inspired by parity's substrate-deps: https://github.com/paritytech/substrate-deps/blob/master/src/runtime.rs#L11
61pub fn add_pallet_to_runtime(
62    cwd: &Path,
63    runtime_path: &Path,
64    crate_spec: &str,
65) -> SubstrateResult<Option<usize>> {
66    let runtime_lib_path = cwd.join(runtime_path).join("src/lib.rs");
67    let mod_name = to_snake_case(crate_spec);
68
69    let pallet_trait_existing = Regex::new(
70        format!(
71            r"(?xm)
72                ^impl\s+{}::Config\s+for\s+Runtime\s+\{{
73                    [^\}}]+
74                \}}
75        ",
76            mod_name
77        )
78        .as_ref(),
79    )?;
80
81    let construct_runtime = Regex::new(
82        r"construct_runtime!\(\s*(?P<visibility>pub\s+)?(?P<variant>enum|struct)\s+Runtime\s*(where[^\{]+)?\{(?P<pallets>[\s\S]+)\}\s*\);",
83    )?;
84
85    let mut pallet_trait_impl = format!("impl {}::Config for Runtime {{ \n", mod_name);
86    pallet_trait_impl.push_str(&format!("	/* {} Trait config goes here */ \n", mod_name));
87    pallet_trait_impl.push('}');
88
89    let pallet_config = format!(
90        r"
91        {}: {},",
92        to_pascal_case(&mod_name),
93        mod_name
94    );
95
96    let original = fs::read_to_string(&runtime_lib_path)?;
97    let mut buffer = original.clone();
98    let mut line_number: Option<usize> = None;
99    if pallet_trait_existing.is_match(&original) {
100        buffer = pallet_trait_existing
101            .replace(&original, |caps: &regex::Captures| {
102                line_number = Some(original[..caps.get(0).unwrap().start()].lines().count() + 1);
103                pallet_trait_impl.to_owned()
104            })
105            .to_string();
106    } else {
107        let mat = construct_runtime
108            .find(&original)
109            .ok_or_else(|| anyhow::anyhow!("couldn't find construct_runtime call"))?;
110        line_number = Some(original[..mat.start()].lines().count() + 1);
111        buffer.insert_str(mat.start(), format!("{}\n\n", pallet_trait_impl).as_str());
112    };
113
114    let modified = buffer.clone();
115    let caps = construct_runtime
116        .captures(&modified)
117        .ok_or_else(|| anyhow::anyhow!("couldn't find construct_runtime call"))?;
118    let pallets = caps.name("pallets").ok_or_else(|| {
119        anyhow::anyhow!("couldn't find runtime pallets config inside construct_runtime",)
120    })?;
121
122    let existing_pallets = modified.get(pallets.start()..pallets.end()).unwrap();
123    let line_number_with_mod_name = existing_pallets
124        .lines()
125        .position(|line| line.contains(&mod_name));
126
127    if let Some(line_number) = line_number_with_mod_name {
128        let line_to_replace = existing_pallets.lines().nth(line_number).unwrap();
129        let new_existing_pallets = existing_pallets.replace(line_to_replace, &pallet_config[1..]);
130
131        let new_buffer = modified.replacen(existing_pallets, &new_existing_pallets, 1);
132        fs::write(runtime_lib_path, new_buffer)?;
133    } else {
134        // Insert the pallet_config at the end of pallets
135        buffer.insert_str(pallets.end() - 2, &pallet_config);
136        fs::write(runtime_lib_path, buffer)?;
137    }
138
139    Ok(line_number)
140}
141
142// TODO:
143// - Make sure the crate is a valid pallet
144// - Try to implement pallet's `Config` trait for runtime by scraping docs to try to find the default implementation
145pub fn add_pallet(opts: &AddOptions, config: &Config) -> SubstrateResult<()> {
146    let crate_source_arg = match &opts.source {
147        CrateSource::DefaultRegistry => vec![],
148        CrateSource::Git(url, branch) => {
149            let mut args = vec!["--git", url];
150            if !branch.is_empty() {
151                args.extend(["--branch", branch]);
152            }
153            args
154        },
155        CrateSource::Path(path) => vec!["--path", path],
156        CrateSource::CustomRegistry(registry) => vec!["--registry", registry],
157    };
158
159    let status = Command::new("cargo")
160        .arg("add")
161        .arg("-p")
162        .arg(&opts.package_name)
163        .arg(&opts.crate_spec)
164        .arg("--features")
165        .arg(&opts.features.join(","))
166        .arg("--no-default-features")
167        .args(crate_source_arg)
168        .status()?;
169
170    if !status.success() {
171        return Err(anyhow::anyhow!(
172            "Failed to install pallet: `{}`",
173            opts.crate_spec,
174        ));
175    }
176
177    add_pallet_std_to_manifest(config.cwd(), &opts.package_path, &opts.crate_spec)?;
178
179    let trait_line_number =
180        add_pallet_to_runtime(config.cwd(), &opts.package_path, &opts.crate_spec)?;
181    println!(
182        "\nPallet `{}` has been successfully added to the runtime!",
183        opts.crate_spec
184    );
185    if let Some(line_number) = trait_line_number {
186        println!(
187            "Don't forget to implement the `Config` trait in `runtime/src/lib.rs`, line: {}",
188            line_number
189        );
190    }
191
192    Ok(())
193}