substrate_wasm_builder/builder.rs
1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18use std::{
19 env,
20 path::{Path, PathBuf},
21 process,
22};
23
24use crate::RuntimeTarget;
25
26/// Extra information when generating the `metadata-hash`.
27#[cfg(feature = "metadata-hash")]
28pub(crate) struct MetadataExtraInfo {
29 pub decimals: u8,
30 pub token_symbol: String,
31}
32
33/// Returns the manifest dir from the `CARGO_MANIFEST_DIR` env.
34fn get_manifest_dir() -> PathBuf {
35 env::var("CARGO_MANIFEST_DIR")
36 .expect("`CARGO_MANIFEST_DIR` is always set for `build.rs` files; qed")
37 .into()
38}
39
40/// First step of the [`WasmBuilder`] to select the project to build.
41pub struct WasmBuilderSelectProject {
42 /// This parameter just exists to make it impossible to construct
43 /// this type outside of this crate.
44 _ignore: (),
45}
46
47impl WasmBuilderSelectProject {
48 /// Use the current project as project for building the WASM binary.
49 ///
50 /// # Panics
51 ///
52 /// Panics if the `CARGO_MANIFEST_DIR` variable is not set. This variable
53 /// is always set by `Cargo` in `build.rs` files.
54 pub fn with_current_project(self) -> WasmBuilder {
55 WasmBuilder {
56 rust_flags: Vec::new(),
57 file_name: None,
58 project_cargo_toml: get_manifest_dir().join("Cargo.toml"),
59 features_to_enable: Vec::new(),
60 disable_runtime_version_section_check: false,
61 export_heap_base: false,
62 import_memory: false,
63 #[cfg(feature = "metadata-hash")]
64 enable_metadata_hash: None,
65 }
66 }
67
68 /// Use the given `path` as project for building the WASM binary.
69 ///
70 /// Returns an error if the given `path` does not points to a `Cargo.toml`.
71 pub fn with_project(self, path: impl Into<PathBuf>) -> Result<WasmBuilder, &'static str> {
72 let path = path.into();
73
74 if path.ends_with("Cargo.toml") && path.exists() {
75 Ok(WasmBuilder {
76 rust_flags: Vec::new(),
77 file_name: None,
78 project_cargo_toml: path,
79 features_to_enable: Vec::new(),
80 disable_runtime_version_section_check: false,
81 export_heap_base: false,
82 import_memory: false,
83 #[cfg(feature = "metadata-hash")]
84 enable_metadata_hash: None,
85 })
86 } else {
87 Err("Project path must point to the `Cargo.toml` of the project")
88 }
89 }
90}
91
92/// The builder for building a wasm binary.
93///
94/// The builder itself is separated into multiple structs to make the setup type safe.
95///
96/// Building a wasm binary:
97///
98/// 1. Call [`WasmBuilder::new`] to create a new builder.
99/// 2. Select the project to build using the methods of [`WasmBuilderSelectProject`].
100/// 3. Set additional `RUST_FLAGS` or a different name for the file containing the WASM code using
101/// methods of [`WasmBuilder`].
102/// 4. Build the WASM binary using [`Self::build`].
103pub struct WasmBuilder {
104 /// Flags that should be appended to `RUST_FLAGS` env variable.
105 rust_flags: Vec<String>,
106 /// The name of the file that is being generated in `OUT_DIR`.
107 ///
108 /// Defaults to `wasm_binary.rs`.
109 file_name: Option<String>,
110 /// The path to the `Cargo.toml` of the project that should be built
111 /// for wasm.
112 project_cargo_toml: PathBuf,
113 /// Features that should be enabled when building the wasm binary.
114 features_to_enable: Vec<String>,
115 /// Should the builder not check that the `runtime_version` section exists in the wasm binary?
116 disable_runtime_version_section_check: bool,
117
118 /// Whether `__heap_base` should be exported (WASM-only).
119 export_heap_base: bool,
120 /// Whether `--import-memory` should be added to the link args (WASM-only).
121 import_memory: bool,
122
123 /// Whether to enable the metadata hash generation.
124 #[cfg(feature = "metadata-hash")]
125 enable_metadata_hash: Option<MetadataExtraInfo>,
126}
127
128impl WasmBuilder {
129 /// Create a new instance of the builder.
130 pub fn new() -> WasmBuilderSelectProject {
131 WasmBuilderSelectProject { _ignore: () }
132 }
133
134 /// Build the WASM binary using the recommended default values.
135 ///
136 /// This is the same as calling:
137 /// ```no_run
138 /// substrate_wasm_builder::WasmBuilder::new()
139 /// .with_current_project()
140 /// .import_memory()
141 /// .export_heap_base()
142 /// .build();
143 /// ```
144 pub fn build_using_defaults() {
145 WasmBuilder::new()
146 .with_current_project()
147 .import_memory()
148 .export_heap_base()
149 .build();
150 }
151
152 /// Init the wasm builder with the recommended default values.
153 ///
154 /// In contrast to [`Self::build_using_defaults`] it does not build the WASM binary directly.
155 ///
156 /// This is the same as calling:
157 /// ```no_run
158 /// substrate_wasm_builder::WasmBuilder::new()
159 /// .with_current_project()
160 /// .import_memory()
161 /// .export_heap_base();
162 /// ```
163 pub fn init_with_defaults() -> Self {
164 WasmBuilder::new().with_current_project().import_memory().export_heap_base()
165 }
166
167 /// Enable exporting `__heap_base` as global variable in the WASM binary.
168 ///
169 /// This adds `-C link-arg=--export=__heap_base` to `RUST_FLAGS`.
170 pub fn export_heap_base(mut self) -> Self {
171 self.export_heap_base = true;
172 self
173 }
174
175 /// Set the name of the file that will be generated in `OUT_DIR`.
176 ///
177 /// This file needs to be included to get access to the build WASM binary.
178 ///
179 /// If this function is not called, `file_name` defaults to `wasm_binary.rs`
180 pub fn set_file_name(mut self, file_name: impl Into<String>) -> Self {
181 self.file_name = Some(file_name.into());
182 self
183 }
184
185 /// Instruct the linker to import the memory into the WASM binary.
186 ///
187 /// This adds `-C link-arg=--import-memory` to `RUST_FLAGS`.
188 pub fn import_memory(mut self) -> Self {
189 self.import_memory = true;
190 self
191 }
192
193 /// Append the given `flag` to `RUST_FLAGS`.
194 ///
195 /// `flag` is appended as is, so it needs to be a valid flag.
196 pub fn append_to_rust_flags(mut self, flag: impl Into<String>) -> Self {
197 self.rust_flags.push(flag.into());
198 self
199 }
200
201 /// Enable the given feature when building the wasm binary.
202 ///
203 /// `feature` needs to be a valid feature that is defined in the project `Cargo.toml`.
204 pub fn enable_feature(mut self, feature: impl Into<String>) -> Self {
205 self.features_to_enable.push(feature.into());
206 self
207 }
208
209 /// Enable generation of the metadata hash.
210 ///
211 /// This will compile the runtime once, fetch the metadata, build the metadata hash and
212 /// then compile again with the env `RUNTIME_METADATA_HASH` set. For more information
213 /// about the metadata hash see [RFC78](https://polkadot-fellows.github.io/RFCs/approved/0078-merkleized-metadata.html).
214 ///
215 /// - `token_symbol`: The symbol of the main native token of the chain.
216 /// - `decimals`: The number of decimals of the main native token.
217 #[cfg(feature = "metadata-hash")]
218 pub fn enable_metadata_hash(mut self, token_symbol: impl Into<String>, decimals: u8) -> Self {
219 self.enable_metadata_hash =
220 Some(MetadataExtraInfo { token_symbol: token_symbol.into(), decimals });
221
222 self
223 }
224
225 /// Disable the check for the `runtime_version` wasm section.
226 ///
227 /// By default the `wasm-builder` will ensure that the `runtime_version` section will
228 /// exists in the build wasm binary. This `runtime_version` section is used to get the
229 /// `RuntimeVersion` without needing to call into the wasm binary. However, for some
230 /// use cases (like tests) you may want to disable this check.
231 pub fn disable_runtime_version_section_check(mut self) -> Self {
232 self.disable_runtime_version_section_check = true;
233 self
234 }
235
236 /// Build the WASM binary.
237 pub fn build(mut self) {
238 let target = RuntimeTarget::new();
239
240 if target == RuntimeTarget::Wasm {
241 if self.export_heap_base {
242 self.rust_flags.push("-C link-arg=--export=__heap_base".into());
243 }
244
245 if self.import_memory {
246 self.rust_flags.push("-C link-arg=--import-memory".into());
247 }
248 }
249
250 let out_dir = PathBuf::from(env::var("OUT_DIR").expect("`OUT_DIR` is set by cargo!"));
251 let file_path =
252 out_dir.join(self.file_name.clone().unwrap_or_else(|| "wasm_binary.rs".into()));
253
254 if check_skip_build() {
255 // If we skip the build, we still want to make sure to be called when an env variable
256 // changes
257 generate_rerun_if_changed_instructions();
258
259 provide_dummy_wasm_binary_if_not_exist(&file_path);
260
261 return
262 }
263
264 build_project(
265 target,
266 file_path,
267 self.project_cargo_toml,
268 self.rust_flags.join(" "),
269 self.features_to_enable,
270 self.file_name,
271 !self.disable_runtime_version_section_check,
272 #[cfg(feature = "metadata-hash")]
273 self.enable_metadata_hash,
274 );
275
276 // As last step we need to generate our `rerun-if-changed` stuff. If a build fails, we don't
277 // want to spam the output!
278 generate_rerun_if_changed_instructions();
279 }
280}
281
282/// Generate the name of the skip build environment variable for the current crate.
283fn generate_crate_skip_build_env_name() -> String {
284 format!(
285 "SKIP_{}_WASM_BUILD",
286 env::var("CARGO_PKG_NAME")
287 .expect("Package name is set")
288 .to_uppercase()
289 .replace('-', "_"),
290 )
291}
292
293/// Checks if the build of the WASM binary should be skipped.
294fn check_skip_build() -> bool {
295 env::var(crate::SKIP_BUILD_ENV).is_ok() ||
296 env::var(generate_crate_skip_build_env_name()).is_ok() ||
297 // If we are running in docs.rs, let's skip building.
298 // https://docs.rs/about/builds#detecting-docsrs
299 env::var("DOCS_RS").is_ok()
300}
301
302/// Provide a dummy WASM binary if there doesn't exist one.
303fn provide_dummy_wasm_binary_if_not_exist(file_path: &Path) {
304 if !file_path.exists() {
305 crate::write_file_if_changed(
306 file_path,
307 "pub const WASM_BINARY_PATH: Option<&str> = None;\
308 pub const WASM_BINARY: Option<&[u8]> = None;\
309 pub const WASM_BINARY_BLOATY: Option<&[u8]> = None;",
310 );
311 }
312}
313
314/// Generate the `rerun-if-changed` instructions for cargo to make sure that the WASM binary is
315/// rebuilt when needed.
316fn generate_rerun_if_changed_instructions() {
317 // Make sure that the `build.rs` is called again if one of the following env variables changes.
318 println!("cargo:rerun-if-env-changed={}", crate::SKIP_BUILD_ENV);
319 println!("cargo:rerun-if-env-changed={}", crate::FORCE_WASM_BUILD_ENV);
320 println!("cargo:rerun-if-env-changed={}", generate_crate_skip_build_env_name());
321}
322
323/// Build the currently built project as wasm binary.
324///
325/// The current project is determined by using the `CARGO_MANIFEST_DIR` environment variable.
326///
327/// `file_name` - The name + path of the file being generated. The file contains the
328/// constant `WASM_BINARY`, which contains the built wasm binary.
329///
330/// `project_cargo_toml` - The path to the `Cargo.toml` of the project that should be built.
331///
332/// `default_rustflags` - Default `RUSTFLAGS` that will always be set for the build.
333///
334/// `features_to_enable` - Features that should be enabled for the project.
335///
336/// `wasm_binary_name` - The optional wasm binary name that is extended with
337/// `.compact.compressed.wasm`. If `None`, the project name will be used.
338///
339/// `check_for_runtime_version_section` - Should the wasm binary be checked for the
340/// `runtime_version` section?
341fn build_project(
342 target: RuntimeTarget,
343 file_name: PathBuf,
344 project_cargo_toml: PathBuf,
345 default_rustflags: String,
346 features_to_enable: Vec<String>,
347 wasm_binary_name: Option<String>,
348 check_for_runtime_version_section: bool,
349 #[cfg(feature = "metadata-hash")] enable_metadata_hash: Option<MetadataExtraInfo>,
350) {
351 // Init jobserver as soon as possible
352 crate::wasm_project::get_jobserver();
353 let cargo_cmd = match crate::prerequisites::check(target) {
354 Ok(cmd) => cmd,
355 Err(err_msg) => {
356 eprintln!("{err_msg}");
357 process::exit(1);
358 },
359 };
360
361 let (wasm_binary, bloaty) = crate::wasm_project::create_and_compile(
362 target,
363 &project_cargo_toml,
364 &default_rustflags,
365 cargo_cmd,
366 features_to_enable,
367 wasm_binary_name,
368 check_for_runtime_version_section,
369 #[cfg(feature = "metadata-hash")]
370 enable_metadata_hash,
371 );
372
373 let (wasm_binary, wasm_binary_bloaty) = if let Some(wasm_binary) = wasm_binary {
374 (wasm_binary.wasm_binary_path_escaped(), bloaty.bloaty_path_escaped())
375 } else {
376 (bloaty.bloaty_path_escaped(), bloaty.bloaty_path_escaped())
377 };
378
379 crate::write_file_if_changed(
380 file_name,
381 format!(
382 r#"
383 pub const WASM_BINARY_PATH: Option<&str> = Some("{wasm_binary_path}");
384 pub const WASM_BINARY: Option<&[u8]> = Some(include_bytes!("{wasm_binary}"));
385 pub const WASM_BINARY_BLOATY: Option<&[u8]> = Some(include_bytes!("{wasm_binary_bloaty}"));
386 "#,
387 wasm_binary_path = wasm_binary,
388 wasm_binary = wasm_binary,
389 wasm_binary_bloaty = wasm_binary_bloaty,
390 ),
391 );
392}