Skip to main content

wasm_pack/build/
mod.rs

1//! Building a Rust crate into a `.wasm` binary.
2
3use crate::child;
4use crate::command::build::BuildProfile;
5use crate::emoji;
6use crate::manifest::Crate;
7use crate::PBAR;
8use anyhow::{anyhow, bail, Context, Result};
9use cargo_metadata::Message;
10use std::io::BufReader;
11use std::path::Path;
12use std::process::{Command, Stdio};
13use std::str;
14
15pub mod wasm_target;
16
17/// Used when comparing the currently installed
18/// wasm-pack version with the latest on crates.io.
19pub struct WasmPackVersion {
20    /// The currently installed wasm-pack version.
21    pub local: String,
22    /// The latest version of wasm-pack that's released at
23    /// crates.io.
24    pub latest: String,
25}
26
27/// Ensure that `rustc` is present and that it is >= 1.30.0
28pub fn check_rustc_version() -> Result<String> {
29    let local_minor_version = rustc_minor_version();
30    match local_minor_version {
31        Some(mv) => {
32            if mv < 30 {
33                bail!(
34                    "Your version of Rust, '1.{}', is not supported. Please install Rust version 1.30.0 or higher.",
35                    mv.to_string()
36                )
37            } else {
38                Ok(mv.to_string())
39            }
40        }
41        None => bail!("We can't figure out what your Rust version is- which means you might not have Rust installed. Please install Rust version 1.30.0 or higher."),
42    }
43}
44
45// from https://github.com/alexcrichton/proc-macro2/blob/79e40a113b51836f33214c6d00228934b41bd4ad/build.rs#L44-L61
46fn rustc_minor_version() -> Option<u32> {
47    macro_rules! otry {
48        ($e:expr) => {
49            match $e {
50                Some(e) => e,
51                None => return None,
52            }
53        };
54    }
55    let output = otry!(Command::new("rustc").arg("--version").output().ok());
56    let version = otry!(str::from_utf8(&output.stdout).ok());
57    let mut pieces = version.split('.');
58    if pieces.next() != Some("rustc 1") {
59        return None;
60    }
61    otry!(pieces.next()).parse().ok()
62}
63
64/// Checks and returns local and latest versions of wasm-pack
65pub fn check_wasm_pack_versions() -> Result<WasmPackVersion> {
66    match wasm_pack_local_version() {
67        Some(local) => Ok(WasmPackVersion {local, latest: Crate::return_wasm_pack_latest_version()?.unwrap_or_else(|| "".to_string())}),
68        None => bail!("We can't figure out what your wasm-pack version is, make sure the installation path is correct.")
69    }
70}
71
72fn wasm_pack_local_version() -> Option<String> {
73    let output = env!("CARGO_PKG_VERSION");
74    Some(output.to_string())
75}
76
77/// Run `cargo build` for Wasm with config derived from the given `BuildProfile`.
78pub fn cargo_build_wasm(
79    path: &Path,
80    profile: BuildProfile,
81    extra_options: &[String],
82    target_triple: &str,
83    panic_unwind: bool,
84) -> Result<String> {
85    let msg = if panic_unwind {
86        format!("{}Compiling to Wasm (with panic=unwind)...", emoji::CYCLONE)
87    } else {
88        format!("{}Compiling to Wasm...", emoji::CYCLONE)
89    };
90    PBAR.info(&msg);
91
92    let mut cmd = Command::new("cargo");
93    cmd.current_dir(path);
94    // `+nightly` must be the first argument to cargo.
95    if panic_unwind {
96        cmd.arg("+nightly");
97    }
98    cmd.arg("build").arg("--lib");
99
100    if PBAR.quiet() {
101        cmd.arg("--quiet");
102    }
103
104    match profile {
105        BuildProfile::Profiling => {
106            // Once there are DWARF debug info consumers, force enable debug
107            // info, because builds that use the release cargo profile disables
108            // debug info.
109            //
110            // cmd.env("RUSTFLAGS", "-g");
111            cmd.arg("--release");
112        }
113        BuildProfile::Release => {
114            cmd.arg("--release");
115        }
116        BuildProfile::Dev => {
117            // Plain cargo builds use the dev cargo profile, which includes
118            // debug info by default.
119        }
120        BuildProfile::Custom(arg) => {
121            cmd.arg("--profile").arg(arg);
122        }
123    }
124
125    cmd.env("CARGO_BUILD_TARGET", target_triple);
126
127    if panic_unwind {
128        cmd.arg("-Z").arg("build-std=std,panic_unwind");
129
130        // Append `-Cpanic=unwind` to any user-provided RUSTFLAGS.
131        let existing = std::env::var("RUSTFLAGS").unwrap_or_default();
132        let combined = if existing.is_empty() {
133            "-Cpanic=unwind".to_string()
134        } else {
135            format!("{existing} -Cpanic=unwind")
136        };
137        cmd.env("RUSTFLAGS", combined);
138    }
139
140    // The `cargo` command is executed inside the directory at `path`, so relative paths set via extra options won't work.
141    // To remedy the situation, all detected paths are converted to absolute paths.
142    let mut handle_path = false;
143    let extra_options_with_absolute_paths = extra_options
144        .iter()
145        .map(|option| -> Result<String> {
146            let value = if handle_path && Path::new(option).is_relative() {
147                std::env::current_dir()?
148                    .join(option)
149                    .to_str()
150                    .ok_or_else(|| anyhow!("path contains non-UTF-8 characters"))?
151                    .to_string()
152            } else {
153                option.to_string()
154            };
155            handle_path = matches!(&**option, "--target-dir" | "--out-dir" | "--manifest-path");
156            Ok(value)
157        })
158        .collect::<Result<Vec<_>>>()?;
159    cmd.args(extra_options_with_absolute_paths);
160
161    cmd.arg("--message-format=json");
162
163    let mut cargo_process = cmd.stdout(Stdio::piped()).spawn()?;
164
165    let final_artifact =
166        Message::parse_stream(BufReader::new(cargo_process.stdout.as_mut().unwrap()))
167            .filter_map(|msg| {
168                match msg {
169                    Ok(Message::CompilerArtifact(artifact)) => return Some(artifact),
170                    Ok(Message::CompilerMessage(msg)) => eprintln!("{msg}"),
171                    Ok(Message::TextLine(text)) => eprintln!("{text}"),
172                    Err(err) => log::error!("Couldn't parse cargo message: {err}"),
173                    _ => {} // ignore messages irrelevant to the user
174                }
175                None
176            })
177            .last();
178
179    if !cargo_process
180        .wait()
181        .context("Failed to wait for cargo build process")?
182        .success()
183    {
184        bail!("`cargo build` failed, see the output above for details");
185    }
186
187    let wasm_files: Vec<_> = final_artifact
188        .context("Expected at least one compiler artifact in the output of `cargo build`")?
189        .filenames
190        .into_iter()
191        .filter(|path| path.extension() == Some("wasm"))
192        .collect();
193
194    match <[_; 1]>::try_from(wasm_files) {
195        Ok([filename]) => Ok(filename.into_string()),
196        Err(filenames) => {
197            bail!(
198                "Expected exactly one .wasm file in the compiler artifact, but found {filenames:?}"
199            )
200        }
201    }
202}
203
204/// Runs `cargo build --tests` targeting `wasm32-unknown-unknown`.
205///
206/// This generates the `Cargo.lock` file that we use in order to know which version of
207/// wasm-bindgen-cli to use when running tests.
208///
209/// Note that the command to build tests and the command to run tests must use the same parameters, i.e. features to be
210/// disabled / enabled must be consistent for both `cargo build` and `cargo test`.
211///
212/// # Parameters
213///
214/// * `path`: Path to the crate directory to build tests.
215/// * `debug`: Whether to build tests in `debug` mode.
216/// * `extra_options`: Additional parameters to pass to `cargo` when building tests.
217/// * `target_triple`: The wasm target triple to build for (e.g.
218///   `wasm32-unknown-unknown` or `wasm64-unknown-unknown`).
219/// * `panic_unwind`: Whether to build tests with `panic=unwind` via the nightly
220///   toolchain and `-Z build-std`.
221pub fn cargo_build_wasm_tests(
222    path: &Path,
223    debug: bool,
224    extra_options: &[String],
225    target_triple: &str,
226    panic_unwind: bool,
227) -> Result<()> {
228    let mut cmd = Command::new("cargo");
229    cmd.current_dir(path);
230    // `+nightly` must be the first argument to cargo.
231    if panic_unwind {
232        cmd.arg("+nightly");
233    }
234    cmd.arg("build").arg("--tests");
235
236    if PBAR.quiet() {
237        cmd.arg("--quiet");
238    }
239
240    if !debug {
241        cmd.arg("--release");
242    }
243
244    cmd.env("CARGO_BUILD_TARGET", target_triple);
245
246    if panic_unwind {
247        cmd.arg("-Z").arg("build-std=std,panic_unwind");
248
249        let existing = std::env::var("RUSTFLAGS").unwrap_or_default();
250        let combined = if existing.is_empty() {
251            "-Cpanic=unwind".to_string()
252        } else {
253            format!("{existing} -Cpanic=unwind")
254        };
255        cmd.env("RUSTFLAGS", combined);
256    }
257
258    cmd.args(extra_options);
259
260    child::run(cmd, "cargo build").context("Compilation of your program failed")?;
261    Ok(())
262}