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/// Returns true for tier-3 wasm targets that have no rustup-prebuilt sysroot
78/// and must be built via `-Z build-std`. Currently this is the wasm64 family
79/// (`wasm64-unknown-unknown`, future `wasm64-*` variants).
80pub fn is_tier3_wasm(target_triple: &str) -> bool {
81    target_triple.starts_with("wasm64")
82}
83
84/// Run `cargo build` for Wasm with config derived from the given `BuildProfile`.
85pub fn cargo_build_wasm(
86    path: &Path,
87    profile: BuildProfile,
88    extra_options: &[String],
89    target_triple: &str,
90    panic_unwind: bool,
91) -> Result<String> {
92    let msg = if panic_unwind {
93        format!("{}Compiling to Wasm (with panic=unwind)...", emoji::CYCLONE)
94    } else {
95        format!("{}Compiling to Wasm...", emoji::CYCLONE)
96    };
97    PBAR.info(&msg);
98
99    let mut cmd = Command::new("cargo");
100    cmd.current_dir(path);
101    // `+nightly` must be the first argument to cargo.
102    if panic_unwind {
103        cmd.arg("+nightly");
104    }
105    cmd.arg("build").arg("--lib");
106
107    if PBAR.quiet() {
108        cmd.arg("--quiet");
109    }
110
111    match profile {
112        BuildProfile::Profiling => {
113            // Once there are DWARF debug info consumers, force enable debug
114            // info, because builds that use the release cargo profile disables
115            // debug info.
116            //
117            // cmd.env("RUSTFLAGS", "-g");
118            cmd.arg("--release");
119        }
120        BuildProfile::Release => {
121            cmd.arg("--release");
122        }
123        BuildProfile::Dev => {
124            // Plain cargo builds use the dev cargo profile, which includes
125            // debug info by default.
126        }
127        BuildProfile::Custom(arg) => {
128            cmd.arg("--profile").arg(arg);
129        }
130    }
131
132    cmd.env("CARGO_BUILD_TARGET", target_triple);
133
134    if panic_unwind {
135        cmd.arg("-Z").arg("build-std=std,panic_unwind");
136
137        // Append `-Cpanic=unwind` to any user-provided RUSTFLAGS.
138        let existing = std::env::var("RUSTFLAGS").unwrap_or_default();
139        let combined = if existing.is_empty() {
140            "-Cpanic=unwind".to_string()
141        } else {
142            format!("{existing} -Cpanic=unwind")
143        };
144        cmd.env("RUSTFLAGS", combined);
145    }
146
147    // The `cargo` command is executed inside the directory at `path`, so relative paths set via extra options won't work.
148    // To remedy the situation, all detected paths are converted to absolute paths.
149    let mut handle_path = false;
150    let extra_options_with_absolute_paths = extra_options
151        .iter()
152        .map(|option| -> Result<String> {
153            let value = if handle_path && Path::new(option).is_relative() {
154                std::env::current_dir()?
155                    .join(option)
156                    .to_str()
157                    .ok_or_else(|| anyhow!("path contains non-UTF-8 characters"))?
158                    .to_string()
159            } else {
160                option.to_string()
161            };
162            handle_path = matches!(&**option, "--target-dir" | "--out-dir" | "--manifest-path");
163            Ok(value)
164        })
165        .collect::<Result<Vec<_>>>()?;
166    cmd.args(extra_options_with_absolute_paths);
167
168    cmd.arg("--message-format=json");
169
170    let mut cargo_process = cmd.stdout(Stdio::piped()).spawn()?;
171
172    let final_artifact =
173        Message::parse_stream(BufReader::new(cargo_process.stdout.as_mut().unwrap()))
174            .filter_map(|msg| {
175                match msg {
176                    Ok(Message::CompilerArtifact(artifact)) => return Some(artifact),
177                    Ok(Message::CompilerMessage(msg)) => eprintln!("{msg}"),
178                    Ok(Message::TextLine(text)) => eprintln!("{text}"),
179                    Err(err) => log::error!("Couldn't parse cargo message: {err}"),
180                    _ => {} // ignore messages irrelevant to the user
181                }
182                None
183            })
184            .last();
185
186    if !cargo_process
187        .wait()
188        .context("Failed to wait for cargo build process")?
189        .success()
190    {
191        bail!("`cargo build` failed, see the output above for details");
192    }
193
194    let wasm_files: Vec<_> = final_artifact
195        .context("Expected at least one compiler artifact in the output of `cargo build`")?
196        .filenames
197        .into_iter()
198        .filter(|path| path.extension() == Some("wasm"))
199        .collect();
200
201    match <[_; 1]>::try_from(wasm_files) {
202        Ok([filename]) => Ok(filename.into_string()),
203        Err(filenames) => {
204            bail!(
205                "Expected exactly one .wasm file in the compiler artifact, but found {filenames:?}"
206            )
207        }
208    }
209}
210
211/// Runs `cargo build --tests` targeting `wasm32-unknown-unknown`.
212///
213/// This generates the `Cargo.lock` file that we use in order to know which version of
214/// wasm-bindgen-cli to use when running tests.
215///
216/// Note that the command to build tests and the command to run tests must use the same parameters, i.e. features to be
217/// disabled / enabled must be consistent for both `cargo build` and `cargo test`.
218///
219/// # Parameters
220///
221/// * `path`: Path to the crate directory to build tests.
222/// * `debug`: Whether to build tests in `debug` mode.
223/// * `extra_options`: Additional parameters to pass to `cargo` when building tests.
224/// * `target_triple`: The wasm target triple to build for (e.g.
225///   `wasm32-unknown-unknown` or `wasm64-unknown-unknown`).
226/// * `panic_unwind`: Whether to build tests with `panic=unwind` via the nightly
227///   toolchain and `-Z build-std`.
228pub fn cargo_build_wasm_tests(
229    path: &Path,
230    debug: bool,
231    extra_options: &[String],
232    target_triple: &str,
233    panic_unwind: bool,
234) -> Result<()> {
235    let mut cmd = Command::new("cargo");
236    cmd.current_dir(path);
237    // `+nightly` must be the first argument to cargo.
238    if panic_unwind {
239        cmd.arg("+nightly");
240    }
241    cmd.arg("build").arg("--tests");
242
243    if PBAR.quiet() {
244        cmd.arg("--quiet");
245    }
246
247    if !debug {
248        cmd.arg("--release");
249    }
250
251    cmd.env("CARGO_BUILD_TARGET", target_triple);
252
253    if panic_unwind {
254        cmd.arg("-Z").arg("build-std=std,panic_unwind");
255
256        let existing = std::env::var("RUSTFLAGS").unwrap_or_default();
257        let combined = if existing.is_empty() {
258            "-Cpanic=unwind".to_string()
259        } else {
260            format!("{existing} -Cpanic=unwind")
261        };
262        cmd.env("RUSTFLAGS", combined);
263    }
264
265    cmd.args(extra_options);
266
267    child::run(cmd, "cargo build").context("Compilation of your program failed")?;
268    Ok(())
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn tier3_wasm_detection() {
277        assert!(is_tier3_wasm("wasm64-unknown-unknown"));
278        assert!(is_tier3_wasm("wasm64-wasi"));
279        assert!(!is_tier3_wasm("wasm32-unknown-unknown"));
280        assert!(!is_tier3_wasm("wasm32-wasi"));
281        assert!(!is_tier3_wasm("wasm32-unknown-emscripten"));
282        assert!(!is_tier3_wasm("x86_64-unknown-linux-gnu"));
283    }
284}