referrerpolicy=no-referrer-when-downgrade

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}