gsubstrate_wasm_builder/lib.rs
1// Copyright (C) Parity Technologies (UK) Ltd.
2// SPDX-License-Identifier: Apache-2.0
3
4// See `README.md` for the upstream source and license reference.
5
6//! # Wasm builder is a utility for building a project as a Wasm binary
7//!
8//! The Wasm builder is a tool that integrates the process of building the WASM binary of your
9//! project into the main `cargo` build process.
10//!
11//! ## Project setup
12//!
13//! A project that should be compiled as a Wasm binary needs to:
14//!
15//! 1. Add a `build.rs` file.
16//! 2. Add `wasm-builder` as dependency into `build-dependencies`.
17//!
18//! The `build.rs` file needs to contain the following code:
19//!
20//! ```no_run
21//! use substrate_wasm_builder::WasmBuilder;
22//!
23//! fn main() {
24//! // Builds the WASM binary using the recommended defaults.
25//! // If you need more control, you can call `new` or `init_with_defaults`.
26//! WasmBuilder::build_using_defaults();
27//! }
28//! ```
29//!
30//! As the final step, you need to add the following to your project:
31//!
32//! ```ignore
33//! include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs"));
34//! ```
35//!
36//! This will include the generated Wasm binary as two constants `WASM_BINARY` and
37//! `WASM_BINARY_BLOATY`. The former is a compact Wasm binary and the latter is the Wasm binary as
38//! being generated by the compiler. Both variables have `Option<&'static [u8]>` as type.
39//!
40//! ### Feature
41//!
42//! Wasm builder supports to enable cargo features while building the Wasm binary. By default it
43//! will enable all features in the wasm build that are enabled for the native build except the
44//! `default` and `std` features. Besides that, wasm builder supports the special `runtime-wasm`
45//! feature. This `runtime-wasm` feature will be enabled by the wasm builder when it compiles the
46//! Wasm binary. If this feature is not present, it will not be enabled.
47//!
48//! ## Environment variables
49//!
50//! By using environment variables, you can configure which Wasm binaries are built and how:
51//!
52//! - `SUBSTRATE_RUNTIME_TARGET` - Sets the target for building runtime. Supported values are `wasm`
53//! or `riscv` (experimental, do not use it in production!). By default the target is equal to
54//! `wasm`.
55//! - `SKIP_WASM_BUILD` - Skips building any Wasm binary. This is useful when only native should be
56//! recompiled. If this is the first run and there doesn't exist a Wasm binary, this will set both
57//! variables to `None`.
58//! - `WASM_BUILD_TYPE` - Sets the build type for building Wasm binaries. Supported values are
59//! `release` or `debug`. By default the build type is equal to the build type used by the main
60//! build.
61//! - `FORCE_WASM_BUILD` - Can be set to force a Wasm build. On subsequent calls the value of the
62//! variable needs to change. As wasm-builder instructs `cargo` to watch for file changes this
63//! environment variable should only be required in certain circumstances.
64//! - `WASM_BUILD_RUSTFLAGS` - Extend `RUSTFLAGS` given to `cargo build` while building the wasm
65//! binary.
66//! - `WASM_BUILD_NO_COLOR` - Disable color output of the wasm build.
67//! - `WASM_TARGET_DIRECTORY` - Will copy any build Wasm binary to the given directory. The path
68//! needs to be absolute.
69//! - `WASM_BUILD_TOOLCHAIN` - The toolchain that should be used to build the Wasm binaries. The
70//! format needs to be the same as used by cargo, e.g. `nightly-2024-12-26`.
71//! - `WASM_BUILD_WORKSPACE_HINT` - Hint the workspace that is being built. This is normally not
72//! required as we walk up from the target directory until we find a `Cargo.toml`. If the target
73//! directory is changed for the build, this environment variable can be used to point to the
74//! actual workspace.
75//! - `WASM_BUILD_STD` - Sets whether the Rust's standard library crates (`core` and `alloc`) will
76//! also be built. This is necessary to make sure the standard library crates only use the exact
77//! WASM feature set that our executor supports. Enabled by default for RISC-V target and WASM
78//! target (but only if Rust < 1.84). Disabled by default for WASM target and Rust >= 1.84.
79//! - `CARGO_NET_OFFLINE` - If `true`, `--offline` will be passed to all processes launched to
80//! prevent network access. Useful in offline environments.
81//!
82//! Each project can be skipped individually by using the environment variable
83//! `SKIP_PROJECT_NAME_WASM_BUILD`. Where `PROJECT_NAME` needs to be replaced by the name of the
84//! cargo project, e.g. `kitchensink-runtime` will be `NODE_RUNTIME`.
85//!
86//! ## Prerequisites:
87//!
88//! Wasm builder requires the following prerequisites for building the Wasm binary:
89//! - Rust >= 1.68 and Rust < 1.84:
90//! - `wasm32-unknown-unknown` target
91//! - `rust-src` component
92//! - Rust >= 1.84:
93//! - `wasm32v1-none` target
94//!
95//! If a specific Rust is installed with `rustup`, it is important that the WASM
96//! target is installed as well. For example if installing the Rust from
97//! 26.12.2024 using `rustup install nightly-2024-12-26`, the WASM target
98//! (`wasm32-unknown-unknown` or `wasm32v1-none`) needs to be installed as well
99//! `rustup target add wasm32-unknown-unknown --toolchain nightly-2024-12-26`.
100//! To install the `rust-src` component, use `rustup component add rust-src
101//! --toolchain nightly-2024-12-26`.
102
103#![allow(
104 clippy::collapsible_if,
105 clippy::get_first,
106 clippy::lines_filter_map_ok,
107 clippy::needless_borrow,
108 clippy::needless_borrows_for_generic_args,
109 clippy::needless_doctest_main,
110 clippy::needless_return,
111 clippy::new_ret_no_self,
112 clippy::option_map_unit_fn,
113 clippy::too_many_arguments,
114 clippy::unnecessary_map_or,
115 clippy::useless_conversion,
116 clippy::zero_prefixed_literal
117)]
118
119use std::{
120 collections::BTreeSet,
121 env, fs,
122 io::BufRead,
123 path::{Path, PathBuf},
124 process::Command,
125};
126use version::Version;
127
128mod builder;
129#[cfg(feature = "metadata-hash")]
130mod metadata_hash;
131mod prerequisites;
132mod version;
133mod wasm_project;
134
135pub use builder::{WasmBuilder, WasmBuilderSelectProject};
136
137/// Environment variable that tells us to skip building the wasm binary.
138const SKIP_BUILD_ENV: &str = "SKIP_WASM_BUILD";
139
140/// Environment variable that tells us whether we should avoid network requests
141const OFFLINE: &str = "CARGO_NET_OFFLINE";
142
143/// Environment variable to force a certain build type when building the wasm binary.
144/// Expects "debug", "release" or "production" as value.
145///
146/// When unset the WASM binary uses the same build type as the main cargo build with
147/// the exception of a debug build: In this case the wasm build defaults to `release` in
148/// order to avoid a slowdown when not explicitly requested.
149const WASM_BUILD_TYPE_ENV: &str = "WASM_BUILD_TYPE";
150
151/// Environment variable to extend the `RUSTFLAGS` variable given to the wasm build.
152const WASM_BUILD_RUSTFLAGS_ENV: &str = "WASM_BUILD_RUSTFLAGS";
153
154/// Environment variable to set the target directory to copy the final wasm binary.
155///
156/// The directory needs to be an absolute path.
157const WASM_TARGET_DIRECTORY: &str = "WASM_TARGET_DIRECTORY";
158
159/// Environment variable to disable color output of the wasm build.
160const WASM_BUILD_NO_COLOR: &str = "WASM_BUILD_NO_COLOR";
161
162/// Environment variable to set the toolchain used to compile the wasm binary.
163const WASM_BUILD_TOOLCHAIN: &str = "WASM_BUILD_TOOLCHAIN";
164
165/// Environment variable that makes sure the WASM build is triggered.
166const FORCE_WASM_BUILD_ENV: &str = "FORCE_WASM_BUILD";
167
168/// Environment variable that hints the workspace we are building.
169const WASM_BUILD_WORKSPACE_HINT: &str = "WASM_BUILD_WORKSPACE_HINT";
170
171/// Environment variable to set whether we'll build `core`/`alloc`.
172const WASM_BUILD_STD: &str = "WASM_BUILD_STD";
173
174/// The target to use for the runtime. Valid values are `wasm` (default) or `riscv`.
175const RUNTIME_TARGET: &str = "SUBSTRATE_RUNTIME_TARGET";
176
177/// Write to the given `file` if the `content` is different.
178fn write_file_if_changed(file: impl AsRef<Path>, content: impl AsRef<str>) {
179 if fs::read_to_string(file.as_ref()).ok().as_deref() != Some(content.as_ref()) {
180 fs::write(file.as_ref(), content.as_ref())
181 .unwrap_or_else(|_| panic!("Writing `{}` can not fail!", file.as_ref().display()));
182 }
183}
184
185/// Copy `src` to `dst` if the `dst` does not exist or is different.
186fn copy_file_if_changed(src: PathBuf, dst: PathBuf) {
187 let src_file = fs::read_to_string(&src).ok();
188 let dst_file = fs::read_to_string(&dst).ok();
189
190 if src_file != dst_file {
191 fs::copy(&src, &dst).unwrap_or_else(|_| {
192 panic!(
193 "Copying `{}` to `{}` can not fail; qed",
194 src.display(),
195 dst.display()
196 )
197 });
198 }
199}
200
201/// Get a cargo command that should be used to invoke the compilation.
202fn get_cargo_command(target: RuntimeTarget) -> CargoCommand {
203 let env_cargo =
204 CargoCommand::new(&env::var("CARGO").expect("`CARGO` env variable is always set by cargo"));
205 let default_cargo = CargoCommand::new("cargo");
206 let wasm_toolchain = env::var(WASM_BUILD_TOOLCHAIN).ok();
207
208 // First check if the user requested a specific toolchain
209 if let Some(cmd) =
210 wasm_toolchain.map(|t| CargoCommand::new_with_args("rustup", &["run", &t, "cargo"]))
211 {
212 cmd
213 } else if env_cargo.supports_substrate_runtime_env(target) {
214 env_cargo
215 } else if default_cargo.supports_substrate_runtime_env(target) {
216 default_cargo
217 } else {
218 // If no command before provided us with a cargo that supports our Substrate wasm env, we
219 // try to search one with rustup. If that fails as well, we return the default cargo and let
220 // the perquisites check fail.
221 get_rustup_command(target).unwrap_or(default_cargo)
222 }
223}
224
225/// Get the newest rustup command that supports compiling a runtime.
226///
227/// Stable versions are always favored over nightly versions even if the nightly versions are
228/// newer.
229fn get_rustup_command(target: RuntimeTarget) -> Option<CargoCommand> {
230 let output = Command::new("rustup")
231 .args(&["toolchain", "list"])
232 .output()
233 .ok()?
234 .stdout;
235 let lines = output.as_slice().lines();
236
237 let mut versions = Vec::new();
238 for line in lines.filter_map(|l| l.ok()) {
239 // Split by a space to get rid of e.g. " (default)" at the end.
240 let rustup_version = line.split(" ").next().unwrap();
241 let cmd = CargoCommand::new_with_args("rustup", &["run", &rustup_version, "cargo"]);
242
243 if !cmd.supports_substrate_runtime_env(target) {
244 continue;
245 }
246
247 let Some(cargo_version) = cmd.version() else {
248 continue;
249 };
250
251 versions.push((cargo_version, rustup_version.to_string()));
252 }
253
254 // Sort by the parsed version to get the latest version (greatest version) at the end of the
255 // vec.
256 versions.sort_by_key(|v| v.0);
257 let version = &versions.last()?.1;
258
259 Some(CargoCommand::new_with_args(
260 "rustup",
261 &["run", &version, "cargo"],
262 ))
263}
264
265/// Wraps a specific command which represents a cargo invocation.
266#[derive(Debug, Clone)]
267struct CargoCommand {
268 program: String,
269 args: Vec<String>,
270 version: Option<Version>,
271 target_list: Option<BTreeSet<String>>,
272}
273
274impl CargoCommand {
275 fn new(program: &str) -> Self {
276 let version = Self::extract_version(program, &[]);
277 let target_list = Self::extract_target_list(program, &[]);
278
279 CargoCommand {
280 program: program.into(),
281 args: Vec::new(),
282 version,
283 target_list,
284 }
285 }
286
287 fn new_with_args(program: &str, args: &[&str]) -> Self {
288 let version = Self::extract_version(program, args);
289 let target_list = Self::extract_target_list(program, args);
290
291 CargoCommand {
292 program: program.into(),
293 args: args.iter().map(ToString::to_string).collect(),
294 version,
295 target_list,
296 }
297 }
298
299 fn command(&self) -> Command {
300 let mut cmd = Command::new(&self.program);
301 cmd.args(&self.args);
302 cmd
303 }
304
305 fn extract_version(program: &str, args: &[&str]) -> Option<Version> {
306 let version = Command::new(program)
307 .args(args)
308 .arg("--version")
309 .output()
310 .ok()
311 .and_then(|o| String::from_utf8(o.stdout).ok())?;
312
313 Version::extract(&version)
314 }
315
316 fn extract_target_list(program: &str, args: &[&str]) -> Option<BTreeSet<String>> {
317 // This is technically an unstable option, but we don't care because we only need this
318 // to build RISC-V runtimes, and those currently require a specific nightly toolchain
319 // anyway, so it's totally fine for this to fail in other cases.
320 let list = Command::new(program)
321 .args(args)
322 .args(&["rustc", "-Z", "unstable-options", "--print", "target-list"])
323 // Make sure if we're called from within a `build.rs` the host toolchain won't override
324 // a rustup toolchain we've picked.
325 .env_remove("RUSTC")
326 .output()
327 .ok()
328 .and_then(|o| String::from_utf8(o.stdout).ok())?;
329
330 Some(list.trim().split("\n").map(ToString::to_string).collect())
331 }
332
333 /// Returns the version of this cargo command or `None` if it failed to extract the version.
334 fn version(&self) -> Option<Version> {
335 self.version
336 }
337
338 /// Returns whether this version of the toolchain supports nightly features.
339 fn supports_nightly_features(&self) -> bool {
340 self.version.map_or(false, |version| version.is_nightly)
341 || env::var("RUSTC_BOOTSTRAP").is_ok()
342 }
343
344 /// Check if the supplied cargo command supports our runtime environment.
345 fn supports_substrate_runtime_env(&self, target: RuntimeTarget) -> bool {
346 match target {
347 RuntimeTarget::Wasm => self.supports_substrate_runtime_env_wasm(),
348 RuntimeTarget::Riscv => self.supports_substrate_runtime_env_riscv(),
349 }
350 }
351
352 /// Check if the supplied cargo command supports our RISC-V runtime environment.
353 fn supports_substrate_runtime_env_riscv(&self) -> bool {
354 let Some(target_list) = self.target_list.as_ref() else {
355 return false;
356 };
357 // This is our custom target which currently doesn't exist on any upstream toolchain,
358 // so if it exists it's guaranteed to be our custom toolchain and have have everything
359 // we need, so any further version checks are unnecessary at this point.
360 target_list.contains("riscv32ema-unknown-none-elf")
361 }
362
363 /// Check if the supplied cargo command supports our Substrate wasm environment.
364 ///
365 /// This means that either the cargo version is at minimum 1.68.0 or this is a nightly cargo.
366 ///
367 /// Assumes that cargo version matches the rustc version.
368 fn supports_substrate_runtime_env_wasm(&self) -> bool {
369 // `RUSTC_BOOTSTRAP` tells a stable compiler to behave like a nightly. So, when this env
370 // variable is set, we can assume that whatever rust compiler we have, it is a nightly
371 // compiler. For "more" information, see:
372 // https://github.com/rust-lang/rust/blob/fa0f7d0080d8e7e9eb20aa9cbf8013f96c81287f/src/libsyntax/feature_gate/check.rs#L891
373 if env::var("RUSTC_BOOTSTRAP").is_ok() {
374 return true;
375 }
376
377 let Some(version) = self.version() else {
378 return false;
379 };
380
381 // Check if major and minor are greater or equal than 1.68 or this is a nightly.
382 version.major > 1 || (version.major == 1 && version.minor >= 68) || version.is_nightly
383 }
384
385 /// Returns whether this version of the toolchain supports the `wasm32v1-none` target.
386 fn supports_wasm32v1_none_target(&self) -> bool {
387 self.version.map_or(false, |version| {
388 // Check if major and minor are greater or equal than 1.84.
389 version.major > 1 || (version.major == 1 && version.minor >= 84)
390 })
391 }
392}
393
394/// Wraps a [`CargoCommand`] and the version of `rustc` the cargo command uses.
395#[derive(Clone)]
396struct CargoCommandVersioned {
397 command: CargoCommand,
398 version: String,
399}
400
401impl CargoCommandVersioned {
402 fn new(command: CargoCommand, version: String) -> Self {
403 Self { command, version }
404 }
405
406 /// Returns the `rustc` version.
407 fn rustc_version(&self) -> &str {
408 &self.version
409 }
410}
411
412impl std::ops::Deref for CargoCommandVersioned {
413 type Target = CargoCommand;
414
415 fn deref(&self) -> &CargoCommand {
416 &self.command
417 }
418}
419
420/// Returns `true` when color output is enabled.
421fn color_output_enabled() -> bool {
422 env::var(crate::WASM_BUILD_NO_COLOR).is_err()
423}
424
425/// Fetches a boolean environment variable. Will exit the process if the value is invalid.
426fn get_bool_environment_variable(name: &str) -> Option<bool> {
427 let value = env::var_os(name)?;
428
429 // We're comparing `OsString`s here so we can't use a `match`.
430 if value == "1" {
431 Some(true)
432 } else if value == "0" {
433 Some(false)
434 } else {
435 build_helper::warning!(
436 "the '{}' environment variable has an invalid value; it must be either '1' or '0'",
437 name
438 );
439 std::process::exit(1);
440 }
441}
442
443/// Returns whether we need to also compile the standard library when compiling the runtime.
444fn build_std_required(cargo_command: &CargoCommand) -> bool {
445 crate::get_bool_environment_variable(crate::WASM_BUILD_STD).unwrap_or_else(|| {
446 match runtime_target() {
447 RuntimeTarget::Wasm => !cargo_command.supports_wasm32v1_none_target(),
448 RuntimeTarget::Riscv => true,
449 }
450 })
451}
452
453#[derive(Copy, Clone, PartialEq, Eq)]
454enum RuntimeTarget {
455 Wasm,
456 Riscv,
457}
458
459impl RuntimeTarget {
460 fn rustc_target(self, cargo_command: &CargoCommand) -> &'static str {
461 match self {
462 RuntimeTarget::Wasm => {
463 if cargo_command.supports_wasm32v1_none_target() {
464 "wasm32v1-none"
465 } else {
466 "wasm32-unknown-unknown"
467 }
468 }
469 RuntimeTarget::Riscv => "riscv32ema-unknown-none-elf",
470 }
471 }
472
473 fn build_subdirectory(self) -> &'static str {
474 // Keep the build directories separate so that when switching between
475 // the targets we won't trigger unnecessary rebuilds.
476 match self {
477 RuntimeTarget::Wasm => "wbuild",
478 RuntimeTarget::Riscv => "rbuild",
479 }
480 }
481}
482
483fn runtime_target() -> RuntimeTarget {
484 let Some(value) = env::var_os(RUNTIME_TARGET) else {
485 return RuntimeTarget::Wasm;
486 };
487
488 if value == "wasm" {
489 RuntimeTarget::Wasm
490 } else if value == "riscv" {
491 RuntimeTarget::Riscv
492 } else {
493 build_helper::warning!(
494 "the '{RUNTIME_TARGET}' environment variable has an invalid value; it must be either 'wasm' or 'riscv'"
495 );
496 std::process::exit(1);
497 }
498}