gear_wasm_builder/
lib.rs

1// This file is part of Gear.
2
3// Copyright (C) 2022-2025 Gear Technologies Inc.
4// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
5
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19#![doc(html_logo_url = "https://gear-tech.io/logo.png")]
20#![doc(html_favicon_url = "https://gear-tech.io/favicon.ico")]
21#![cfg_attr(docsrs, feature(doc_cfg))]
22
23pub use gear_wasm_optimizer::{self as optimize, CargoCommand};
24pub use wasm_project::{PreProcessor, PreProcessorResult, PreProcessorTarget};
25
26use crate::wasm_project::WasmProject;
27use anyhow::Result;
28use regex::Regex;
29use std::{env, path::PathBuf, process};
30
31mod builder_error;
32pub mod code_validator;
33mod crate_info;
34mod multiple_crate_versions;
35mod smart_fs;
36mod wasm_project;
37
38pub const TARGET: &str = env!("TARGET");
39
40/// WASM building tool.
41pub struct WasmBuilder {
42    wasm_project: WasmProject,
43    cargo: CargoCommand,
44    excluded_features: Vec<&'static str>,
45}
46
47impl WasmBuilder {
48    /// Create a new `WasmBuilder`.
49    pub fn new() -> Self {
50        WasmBuilder::create(WasmProject::new())
51    }
52
53    fn create(wasm_project: WasmProject) -> Self {
54        WasmBuilder {
55            wasm_project,
56            cargo: CargoCommand::new(),
57            excluded_features: vec![],
58        }
59    }
60
61    /// Exclude features from the build.
62    pub fn exclude_features(mut self, features: impl Into<Vec<&'static str>>) -> Self {
63        self.excluded_features = features.into();
64        self
65    }
66
67    /// Add pre-processor for wasm file.
68    pub fn with_pre_processor(mut self, pre_processor: Box<dyn PreProcessor>) -> Self {
69        self.wasm_project.add_preprocessor(pre_processor);
70        self
71    }
72
73    /// Add check of recommended toolchain.
74    pub fn with_recommended_toolchain(mut self) -> Self {
75        self.cargo.set_check_recommended_toolchain(true);
76        self
77    }
78
79    /// Force the recommended toolchain to be used, but do not check whether the
80    /// current toolchain is recommended.
81    ///
82    /// NOTE: For internal use only, not recommended for production programs.
83    ///
84    /// An example usage can be found in `examples/out-of-memory/build.rs`.
85    #[doc(hidden)]
86    pub fn with_forced_recommended_toolchain(mut self) -> Self {
87        self.cargo.set_force_recommended_toolchain(true);
88        self
89    }
90
91    /// Build the program and produce an output WASM binary.
92    ///
93    /// Returns `None` if `__GEAR_WASM_BUILDER_NO_BUILD` flag is set.
94    /// Returns `Some(_)` with a tuple of paths to wasm & opt wasm file
95    /// if the build was successful.
96    pub fn build(self) -> Option<(PathBuf, PathBuf)> {
97        if env::var("__GEAR_WASM_BUILDER_NO_BUILD").is_ok() || is_intellij_sync() {
98            _ = self.wasm_project.provide_dummy_wasm_binary_if_not_exist();
99            return None;
100        }
101
102        match self.build_project() {
103            Err(e) => {
104                eprintln!("error: {e}");
105                e.chain()
106                    .skip(1)
107                    .for_each(|cause| eprintln!("|      {cause}"));
108                process::exit(1);
109            }
110            Ok(r) => r,
111        }
112    }
113
114    fn build_project(mut self) -> Result<Option<(PathBuf, PathBuf)>> {
115        self.wasm_project.generate()?;
116
117        self.cargo
118            .set_manifest_path(self.wasm_project.manifest_path());
119        self.cargo.set_target_dir(self.wasm_project.target_dir());
120        let profile = self.wasm_project.profile();
121        let profile = if profile == "debug" { "dev" } else { profile };
122        self.cargo.set_profile(profile.to_string());
123        self.cargo.set_features(&self.enabled_features()?);
124
125        self.cargo.run()?;
126        self.wasm_project.postprocess()
127    }
128
129    fn manifest_path(&self) -> Result<String> {
130        let manifest_path = env::var("CARGO_MANIFEST_DIR")?;
131        Ok(manifest_path)
132    }
133
134    /// Returns features enabled for the current build.
135    fn enabled_features(&self) -> Result<Vec<String>> {
136        let project_features = self.wasm_project.features();
137        let enabled_features_iter = env::vars().filter_map(|(key, _)| {
138            key.strip_prefix("CARGO_FEATURE_")
139                .map(|feature| feature.to_lowercase())
140        });
141        let mut matched_features = Vec::new();
142        let mut unmatched_features = Vec::new();
143        for enabled_feature in enabled_features_iter {
144            // Features coming via the CARGO_FEATURE_<feature> environment variable are in
145            // normilized form, i.e. all dashes are replaced with underscores.
146            let enabled_feature_regex =
147                Regex::new(&format!("^{}$", enabled_feature.replace('_', "[-_]")))?;
148            if self
149                .excluded_features
150                .iter()
151                .any(|excluded_feature| enabled_feature_regex.is_match(excluded_feature))
152            {
153                continue;
154            }
155            if let Some(project_feature) = project_features
156                .iter()
157                .find(|project_feature| enabled_feature_regex.is_match(project_feature))
158            {
159                matched_features.push(project_feature.clone());
160            } else {
161                unmatched_features.push(enabled_feature);
162            }
163        }
164
165        // It may turn out that crate with a build script is built as a dependency of
166        // another crate with build script in the same process (runtime -> pallet-gear
167        // -> examples). In that case, all the CARGO_FEATURE_<feature>
168        // environment variables are propagated down to the dependent crate
169        // which might not have the corresponding features at all.
170        // In such situation, we just warn about unmatched features for diagnostic
171        // purposes and ignore them as cargo itself checks initial set of
172        // features before they reach the build script.
173        if !unmatched_features.is_empty() && unmatched_features != ["default"] {
174            println!(
175                "cargo:warning=Package {}: features `{}` are not available and will be ignored",
176                self.manifest_path()?,
177                unmatched_features.join(", ")
178            );
179        }
180
181        // NOTE: Filter out feature `gcli`.
182        //
183        // dependency feature `gcli` could be captured here
184        // but it is not needed for the build.
185        //
186        // TODO: Filter dep features in this function (#3588)
187        Ok(matched_features
188            .into_iter()
189            .filter(|feature| feature != "gcli")
190            .collect())
191    }
192}
193
194impl Default for WasmBuilder {
195    fn default() -> Self {
196        Self::new()
197    }
198}
199
200fn is_intellij_sync() -> bool {
201    // Intellij Rust uses rustc wrapper during project sync
202    env::var("RUSTC_WRAPPER")
203        .unwrap_or_default()
204        .contains("intellij")
205}
206
207// The `std` feature is excluded by default because it is usually used for
208// building so called WASM wrapper which is a static library exposing the built
209// WASM.
210const FEATURES_TO_EXCLUDE_BY_DEFAULT: &[&str] = &["std"];
211
212/// Shorthand function to be used in `build.rs`.
213///
214/// See [WasmBuilder::build()].
215pub fn build() -> Option<(PathBuf, PathBuf)> {
216    WasmBuilder::new()
217        .exclude_features(FEATURES_TO_EXCLUDE_BY_DEFAULT.to_vec())
218        .build()
219}
220
221/// Shorthand function to be used in `build.rs`.
222///
223/// See [WasmBuilder::build()].
224pub fn recommended_nightly() -> Option<(PathBuf, PathBuf)> {
225    WasmBuilder::new()
226        .exclude_features(FEATURES_TO_EXCLUDE_BY_DEFAULT.to_vec())
227        .with_recommended_toolchain()
228        .build()
229}