multiversx_sc_meta_lib/contract/sc_config/
wasm_build.rs

1use crate::tools::{build_target, wasm_opt};
2use core::panic;
3use std::{
4    collections::HashMap,
5    ffi::OsStr,
6    fs,
7    path::{Path, PathBuf},
8    process::Command,
9};
10
11use super::execute_command::{execute_spawn_command, ExecuteCommandError};
12use super::ContractVariant;
13use crate::{
14    abi_json::ContractAbiJson,
15    cli::BuildArgs,
16    ei::EIVersion,
17    ei_check_json::EiCheckJson,
18    mxsc_file_json::{save_mxsc_file_json, MxscFileJson},
19    print_util::*,
20    report_info_json::ReportInfoJson,
21    tools::{self, WasmInfo, WasmReport},
22};
23
24impl ContractVariant {
25    pub fn build_contract(
26        &self,
27        build_args: &BuildArgs,
28        output_path: &Path,
29    ) -> Result<(), ExecuteCommandError> {
30        let mut build_command = self.compose_build_command(build_args);
31
32        print_build_command(self.wasm_output_name(build_args), &build_command);
33
34        let output_build_command = execute_spawn_command(&mut build_command, "cargo");
35
36        match output_build_command {
37            Ok(_) => {}
38            Err(ExecuteCommandError::JobFailed(_)) => {
39                if !self.is_target_installed() {
40                    self.install_wasm_target();
41
42                    execute_spawn_command(&mut build_command, "cargo")
43                        .expect("error building contract");
44                }
45            }
46            Err(_) => {
47                panic!("error building contract");
48            }
49        }
50
51        self.finalize_build(build_args, output_path);
52
53        Ok(())
54    }
55
56    fn compose_build_command(&self, build_args: &BuildArgs) -> Command {
57        let mut command = Command::new("cargo");
58        command
59            .arg(self.settings.rustc_version.to_cli_arg())
60            .arg("build")
61            .arg(format!("--target={}", &self.settings.rustc_target))
62            .arg("--release")
63            .current_dir(self.wasm_crate_path());
64        if build_args.locked {
65            command.arg("--locked");
66        }
67        if let Some(target_dir_wasm) = &build_args.target_dir_wasm {
68            command.args(["--target-dir", target_dir_wasm]);
69        }
70        let rustflags = self.compose_rustflags(build_args);
71        if !rustflags.is_empty() {
72            command.env("RUSTFLAGS", rustflags);
73        }
74        command
75    }
76
77    fn compose_rustflags(&self, build_args: &BuildArgs) -> Rustflags {
78        let mut rustflags = Rustflags::default();
79
80        if !build_args.wasm_symbols {
81            rustflags.push_flag("-C link-arg=-s");
82        }
83
84        rustflags.push_flag(&format!(
85            "-C link-arg=-zstack-size={}",
86            self.settings.stack_size
87        ));
88
89        if build_args.emit_mir {
90            rustflags.push_flag("--emit=mir");
91        }
92
93        if build_args.emit_llvm_ir {
94            rustflags.push_flag("--emit=llvm-ir");
95        }
96        rustflags
97    }
98
99    fn is_target_installed(&self) -> bool {
100        build_target::is_target_installed(&self.settings.rustc_version, &self.settings.rustc_target)
101    }
102
103    fn install_wasm_target(&self) {
104        build_target::install_target(
105            Some(&self.settings.rustc_version),
106            &self.settings.rustc_target,
107        );
108    }
109
110    fn finalize_build(&self, build_args: &BuildArgs, output_path: &Path) {
111        self.copy_contracts_to_output(build_args, output_path);
112        self.run_wasm_opt(build_args, output_path);
113        self.run_wasm2wat(build_args, output_path);
114        let report = self.extract_wasm_info(build_args, output_path);
115        self.run_twiggy(build_args, output_path);
116        self.pack_mxsc_file(build_args, output_path, &report);
117    }
118
119    fn copy_contracts_to_output(&self, build_args: &BuildArgs, output_path: &Path) {
120        let source_wasm_path = self.wasm_compilation_output_path(&build_args.target_dir_wasm);
121        let output_wasm_path = output_path.join(self.wasm_output_name(build_args));
122        print_copy_contract(
123            &source_wasm_path.to_string_lossy(),
124            &output_wasm_path.to_string_lossy(),
125        );
126        fs::copy(&source_wasm_path, output_wasm_path).unwrap_or_else(|err| {
127            panic!(
128                "failed to copy compiled contract to output directory, source: {}, err: {err}",
129                source_wasm_path.display()
130            )
131        });
132    }
133
134    fn pack_mxsc_file(&self, build_args: &BuildArgs, output_path: &Path, wasm_report: &WasmReport) {
135        let output_wasm_path = output_path.join(self.wasm_output_name(build_args));
136        let compiled_bytes = fs::read(output_wasm_path).expect("failed to open compiled contract");
137        let output_mxsc_path = output_path.join(self.mxsc_file_output_name(build_args));
138        print_pack_mxsc_file(&output_mxsc_path.to_string_lossy());
139        print_contract_size(compiled_bytes.len());
140        let mut abi = ContractAbiJson::from(&self.abi);
141        let build_info = core::mem::take(&mut abi.build_info).unwrap();
142        let ei_check_json = EiCheckJson::new(&self.settings.check_ei, wasm_report.ei_check);
143        let report = ReportInfoJson::new(wasm_report, ei_check_json, compiled_bytes.len());
144        let mxsc_file_json = MxscFileJson {
145            build_info,
146            abi,
147            code: hex::encode(compiled_bytes),
148            report,
149        };
150
151        save_mxsc_file_json(&mxsc_file_json, output_mxsc_path);
152    }
153
154    /// Returns whether or not running wasm-opt should be attempted.
155    fn check_wasm_opt(&self, build_args: &BuildArgs) -> bool {
156        if let Some(config_wasm_opt_version) = &self.settings.wasm_opt_version {
157            let contract_name = &self.contract_name;
158
159            assert!(build_args.wasm_opt, "Contract {contract_name} requires wasm-opt version {config_wasm_opt_version}, and cannot be built without.");
160
161            let opt_version = wasm_opt::wasm_opt_version();
162
163            if opt_version.is_none() {
164                // also print warning before failure
165                print_wasm_opt_not_installed(wasm_opt::WASM_OPT_NAME);
166            }
167
168            let installed_wasm_opt_version = opt_version
169                .unwrap_or_else(|| panic!("Missing wasm-opt. Contract {contract_name} requires wasm-opt version {config_wasm_opt_version}, and cannot be built without."));
170
171            assert_eq!(config_wasm_opt_version, &installed_wasm_opt_version,
172                "Incorrect wasm-opt version installed. Contract {contract_name} requires wasm-opt version {config_wasm_opt_version}");
173
174            true
175        } else {
176            if !build_args.wasm_opt {
177                return false;
178            }
179
180            if wasm_opt::wasm_opt_version().is_none() {
181                print_wasm_opt_not_installed(wasm_opt::WASM_OPT_NAME);
182                return false;
183            }
184
185            true
186        }
187    }
188
189    fn run_wasm_opt(&self, build_args: &BuildArgs, output_path: &Path) {
190        let should_run_wasm_opt = self.check_wasm_opt(build_args);
191
192        if !should_run_wasm_opt {
193            return;
194        }
195
196        let output_wasm_path = output_path.join(self.wasm_output_name(build_args));
197
198        print_call_wasm_opt(&output_wasm_path.to_string_lossy());
199        tools::run_wasm_opt(&output_wasm_path.to_string_lossy());
200    }
201
202    fn run_wasm2wat(&self, build_args: &BuildArgs, output_path: &Path) {
203        if !build_args.wat {
204            return;
205        }
206
207        let output_wasm_path = output_path.join(self.wasm_output_name(build_args));
208        let output_wat_path = output_path.join(self.wat_output_name(build_args));
209
210        print_call_wasm2wat(
211            &output_wasm_path.to_string_lossy(),
212            &output_wat_path.to_string_lossy(),
213        );
214        tools::wasm_to_wat(
215            &output_wasm_path.to_string_lossy(),
216            &output_wat_path.to_string_lossy(),
217        );
218    }
219
220    fn extract_wasm_info(&self, build_args: &BuildArgs, output_path: &Path) -> WasmReport {
221        let output_wasm_path = output_path.join(self.wasm_output_name(build_args));
222
223        let abi = ContractAbiJson::from(&self.abi);
224        let mut endpoints: HashMap<&str, bool> = HashMap::new();
225
226        if abi.constructor.is_some() {
227            endpoints.insert("init", false);
228        }
229
230        if abi.upgrade_constructor.is_some() {
231            endpoints.insert("upgrade", false);
232        }
233
234        for endpoint in &abi.endpoints {
235            if let crate::abi_json::EndpointMutabilityAbiJson::Readonly = endpoint.mutability {
236                endpoints.insert(&endpoint.name, true);
237            } else {
238                endpoints.insert(&endpoint.name, false);
239            }
240        }
241
242        if !build_args.extract_imports {
243            return WasmInfo::extract_wasm_report(
244                &output_wasm_path,
245                build_args.extract_imports,
246                self.settings.check_ei.as_ref(),
247                &endpoints,
248                self.settings.opcode_version,
249            );
250        }
251
252        let output_imports_json_path = output_path.join(self.imports_json_output_name(build_args));
253
254        print_extract_imports(&output_imports_json_path.to_string_lossy());
255
256        let wasm_report = WasmInfo::extract_wasm_report(
257            &output_wasm_path,
258            true,
259            self.settings.check_ei.as_ref(),
260            &endpoints,
261            self.settings.opcode_version,
262        );
263
264        write_imports_output(&output_imports_json_path, wasm_report.imports.as_slice());
265        print_ei_check(&wasm_report, &self.settings.check_ei);
266
267        wasm_report
268    }
269}
270
271fn write_imports_output(dest_path: &PathBuf, import_names: &[String]) {
272    let json = serde_json::to_string_pretty(import_names).unwrap();
273    fs::write(dest_path, json).expect("failed to write imports json file");
274}
275
276fn print_ei_check(wasm_report: &WasmReport, check_ei: &Option<EIVersion>) {
277    if let Some(ei) = check_ei {
278        print_check_ei(ei.name());
279
280        if wasm_report.ei_check {
281            print_check_ei_ok();
282        } else {
283            for import_name in &wasm_report.imports {
284                if !ei.contains_vm_hook(import_name.as_str()) {
285                    print_invalid_vm_hook(import_name.as_str(), ei.name());
286                }
287            }
288
289            println!();
290        }
291    } else {
292        print_ignore_ei_check();
293    }
294}
295
296impl ContractVariant {
297    fn run_twiggy(&self, build_args: &BuildArgs, output_path: &Path) {
298        if build_args.has_twiggy_call() {
299            let output_wasm_path = output_path.join(self.wasm_output_name(build_args));
300
301            if build_args.twiggy_top {
302                let output_twiggy_top_path = output_path.join(self.twiggy_top_name(build_args));
303
304                tools::twiggy::run_twiggy_top(&output_wasm_path, &output_twiggy_top_path);
305            }
306            if build_args.twiggy_paths {
307                let output_twiggy_paths_path = output_path.join(self.twiggy_paths_name(build_args));
308
309                tools::twiggy::run_twiggy_paths(&output_wasm_path, &output_twiggy_paths_path);
310            }
311            if build_args.twiggy_monos {
312                let output_twiggy_monos_path = output_path.join(self.twiggy_monos_name(build_args));
313
314                tools::twiggy::run_twiggy_monos(&output_wasm_path, &output_twiggy_monos_path);
315            }
316            if build_args.twiggy_dominators {
317                let output_twiggy_dominators_path =
318                    output_path.join(self.twiggy_dominators_name(build_args));
319
320                tools::twiggy::run_twiggy_dominators(
321                    &output_wasm_path,
322                    &output_twiggy_dominators_path,
323                );
324            }
325        }
326    }
327}
328
329/// For convenience, for building rustflags.
330#[derive(Default)]
331struct Rustflags(String);
332
333impl Rustflags {
334    fn push_flag(&mut self, s: &str) {
335        if !self.0.is_empty() {
336            self.0.push(' ');
337        }
338        self.0.push_str(s);
339    }
340
341    fn is_empty(&self) -> bool {
342        self.0.is_empty()
343    }
344}
345
346impl AsRef<OsStr> for Rustflags {
347    fn as_ref(&self) -> &OsStr {
348        self.0.as_ref()
349    }
350}