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) -> Result<String> {
83    let msg = format!("{}Compiling to Wasm...", emoji::CYCLONE);
84    PBAR.info(&msg);
85
86    let mut cmd = Command::new("cargo");
87    cmd.current_dir(path).arg("build").arg("--lib");
88
89    if PBAR.quiet() {
90        cmd.arg("--quiet");
91    }
92
93    match profile {
94        BuildProfile::Profiling => {
95            // Once there are DWARF debug info consumers, force enable debug
96            // info, because builds that use the release cargo profile disables
97            // debug info.
98            //
99            // cmd.env("RUSTFLAGS", "-g");
100            cmd.arg("--release");
101        }
102        BuildProfile::Release => {
103            cmd.arg("--release");
104        }
105        BuildProfile::Dev => {
106            // Plain cargo builds use the dev cargo profile, which includes
107            // debug info by default.
108        }
109        BuildProfile::Custom(arg) => {
110            cmd.arg("--profile").arg(arg);
111        }
112    }
113
114    // If user has specified a custom --target in Cargo options, we shouldn't override it.
115    // Otherwise, default to wasm32-unknown-unknown.
116    cmd.env("CARGO_BUILD_TARGET", "wasm32-unknown-unknown");
117
118    // The `cargo` command is executed inside the directory at `path`, so relative paths set via extra options won't work.
119    // To remedy the situation, all detected paths are converted to absolute paths.
120    let mut handle_path = false;
121    let extra_options_with_absolute_paths = extra_options
122        .iter()
123        .map(|option| -> Result<String> {
124            let value = if handle_path && Path::new(option).is_relative() {
125                std::env::current_dir()?
126                    .join(option)
127                    .to_str()
128                    .ok_or_else(|| anyhow!("path contains non-UTF-8 characters"))?
129                    .to_string()
130            } else {
131                option.to_string()
132            };
133            handle_path = matches!(&**option, "--target-dir" | "--out-dir" | "--manifest-path");
134            Ok(value)
135        })
136        .collect::<Result<Vec<_>>>()?;
137    cmd.args(extra_options_with_absolute_paths);
138
139    cmd.arg("--message-format=json");
140
141    let mut cargo_process = cmd.stdout(Stdio::piped()).spawn()?;
142
143    let final_artifact =
144        Message::parse_stream(BufReader::new(cargo_process.stdout.as_mut().unwrap()))
145            .filter_map(|msg| {
146                match msg {
147                    Ok(Message::CompilerArtifact(artifact)) => return Some(artifact),
148                    Ok(Message::CompilerMessage(msg)) => eprintln!("{msg}"),
149                    Ok(Message::TextLine(text)) => eprintln!("{text}"),
150                    Err(err) => log::error!("Couldn't parse cargo message: {err}"),
151                    _ => {} // ignore messages irrelevant to the user
152                }
153                None
154            })
155            .last();
156
157    if !cargo_process
158        .wait()
159        .context("Failed to wait for cargo build process")?
160        .success()
161    {
162        bail!("`cargo build` failed, see the output above for details");
163    }
164
165    let wasm_files: Vec<_> = final_artifact
166        .context("Expected at least one compiler artifact in the output of `cargo build`")?
167        .filenames
168        .into_iter()
169        .filter(|path| path.extension() == Some("wasm"))
170        .collect();
171
172    match <[_; 1]>::try_from(wasm_files) {
173        Ok([filename]) => Ok(filename.into_string()),
174        Err(filenames) => {
175            bail!(
176                "Expected exactly one .wasm file in the compiler artifact, but found {filenames:?}"
177            )
178        }
179    }
180}
181
182/// Runs `cargo build --tests` targeting `wasm32-unknown-unknown`.
183///
184/// This generates the `Cargo.lock` file that we use in order to know which version of
185/// wasm-bindgen-cli to use when running tests.
186///
187/// Note that the command to build tests and the command to run tests must use the same parameters, i.e. features to be
188/// disabled / enabled must be consistent for both `cargo build` and `cargo test`.
189///
190/// # Parameters
191///
192/// * `path`: Path to the crate directory to build tests.
193/// * `debug`: Whether to build tests in `debug` mode.
194/// * `extra_options`: Additional parameters to pass to `cargo` when building tests.
195pub fn cargo_build_wasm_tests(path: &Path, debug: bool, extra_options: &[String]) -> Result<()> {
196    let mut cmd = Command::new("cargo");
197
198    cmd.current_dir(path).arg("build").arg("--tests");
199
200    if PBAR.quiet() {
201        cmd.arg("--quiet");
202    }
203
204    if !debug {
205        cmd.arg("--release");
206    }
207
208    cmd.env("CARGO_BUILD_TARGET", "wasm32-unknown-unknown");
209
210    cmd.args(extra_options);
211
212    child::run(cmd, "cargo build").context("Compilation of your program failed")?;
213    Ok(())
214}