Skip to main content

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}