Skip to main content

wasm_pack/command/
build.rs

1//! Implementation of the `wasm-pack build` command.
2
3use crate::bindgen;
4use crate::build;
5use crate::cache;
6use crate::command::utils::{create_pkg_dir, get_crate_path};
7use crate::emoji;
8use crate::install::{self, InstallMode, Tool};
9use crate::license;
10use crate::lockfile::Lockfile;
11use crate::manifest;
12use crate::readme;
13use crate::wasm_opt;
14use crate::PBAR;
15use anyhow::{anyhow, bail, Error, Result};
16use binary_install::Cache;
17use clap::Args;
18use log::info;
19use path_clean::PathClean;
20use std::fmt;
21use std::path::PathBuf;
22use std::str::FromStr;
23use std::time::Instant;
24
25/// Everything required to configure and run the `wasm-pack build` command.
26#[allow(missing_docs)]
27pub struct Build {
28    pub crate_path: PathBuf,
29    pub crate_data: manifest::CrateData,
30    pub scope: Option<String>,
31    pub disable_dts: bool,
32    pub no_gitignore: bool,
33    pub weak_refs: bool,
34    pub reference_types: bool,
35    pub target: Target,
36    pub no_pack: bool,
37    pub no_opt: bool,
38    pub profile: BuildProfile,
39    pub mode: InstallMode,
40    pub out_dir: PathBuf,
41    pub out_name: Option<String>,
42    pub bindgen: Option<install::Status>,
43    pub cache: Cache,
44    pub extra_options: Vec<String>,
45    pub panic_unwind: bool,
46    target_triple: String,
47    wasm_path: Option<String>,
48}
49
50/// What sort of output we're going to be generating and flags we're invoking
51/// `wasm-bindgen` with.
52#[derive(Clone, Copy, Debug)]
53pub enum Target {
54    /// Default output mode or `--target bundler`, indicates output will be
55    /// used with a bundle in a later step.
56    Bundler,
57    /// Correspond to `--target web` where the output is natively usable as an
58    /// ES module in a browser and the wasm is manually instantiated.
59    Web,
60    /// Correspond to `--target nodejs` where the output is natively usable as
61    /// a Node.js module loaded with `require`.
62    Nodejs,
63    /// Correspond to `--target no-modules` where the output is natively usable
64    /// in a browser but pollutes the global namespace and must be manually
65    /// instantiated.
66    NoModules,
67    /// Correspond to `--target deno` where the output is natively usable as
68    /// a Deno module loaded with `import`.
69    Deno,
70}
71
72impl Default for Target {
73    fn default() -> Target {
74        Target::Bundler
75    }
76}
77
78impl fmt::Display for Target {
79    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
80        let s = match self {
81            Target::Bundler => "bundler",
82            Target::Web => "web",
83            Target::Nodejs => "nodejs",
84            Target::NoModules => "no-modules",
85            Target::Deno => "deno",
86        };
87        write!(f, "{}", s)
88    }
89}
90
91impl FromStr for Target {
92    type Err = Error;
93    fn from_str(s: &str) -> Result<Self> {
94        match s {
95            "bundler" | "browser" => Ok(Target::Bundler),
96            "web" => Ok(Target::Web),
97            "nodejs" => Ok(Target::Nodejs),
98            "no-modules" => Ok(Target::NoModules),
99            "deno" => Ok(Target::Deno),
100            _ => bail!("Unknown target: {}", s),
101        }
102    }
103}
104
105/// The build profile controls whether optimizations, debug info, and assertions
106/// are enabled or disabled.
107#[derive(Clone, Debug)]
108pub enum BuildProfile {
109    /// Enable assertions and debug info. Disable optimizations.
110    Dev,
111    /// Enable optimizations. Disable assertions and debug info.
112    Release,
113    /// Enable optimizations and debug info. Disable assertions.
114    Profiling,
115    /// User-defined profile with --profile flag
116    Custom(String),
117}
118
119/// Everything required to configure and run the `wasm-pack build` command.
120#[derive(Debug, Args)]
121#[command(allow_hyphen_values = true, trailing_var_arg = true)]
122pub struct BuildOptions {
123    /// The path to the Rust crate. If not set, searches up the path from the current directory.
124    #[clap()]
125    pub path: Option<PathBuf>,
126
127    /// The npm scope to use in package.json, if any.
128    #[clap(long = "scope", short = 's')]
129    pub scope: Option<String>,
130
131    #[clap(long = "mode", short = 'm', default_value = "normal")]
132    /// Sets steps to be run. [possible values: no-install, normal, force]
133    pub mode: InstallMode,
134
135    #[clap(long = "no-typescript")]
136    /// By default a *.d.ts file is generated for the generated JS file, but
137    /// this flag will disable generating this TypeScript file.
138    pub disable_dts: bool,
139
140    #[clap(long = "no-gitignore")]
141    /// Skip generating `.gitignore` file in the output directory.
142    pub no_gitignore: bool,
143
144    #[clap(long = "weak-refs")]
145    /// Enable usage of the JS weak references proposal.
146    pub weak_refs: bool,
147
148    #[clap(long = "reference-types")]
149    /// Enable usage of WebAssembly reference types.
150    pub reference_types: bool,
151
152    #[clap(long = "target", short = 't', default_value = "bundler")]
153    /// Sets the target environment. [possible values: bundler, nodejs, web, no-modules, deno]
154    pub target: Target,
155
156    #[clap(long = "debug")]
157    /// Deprecated. Renamed to `--dev`.
158    pub debug: bool,
159
160    #[clap(long = "dev")]
161    /// Create a development build. Enable debug info, and disable
162    /// optimizations.
163    pub dev: bool,
164
165    #[clap(long = "release")]
166    /// Create a release build. Enable optimizations and disable debug info.
167    pub release: bool,
168
169    #[clap(long = "profiling")]
170    /// Create a profiling build. Enable optimizations and debug info.
171    pub profiling: bool,
172
173    #[clap(long = "profile")]
174    /// User-defined profile with --profile flag
175    pub profile: Option<String>,
176
177    #[clap(long = "out-dir", short = 'd', default_value = "pkg")]
178    /// Sets the output directory with a relative path.
179    pub out_dir: String,
180
181    #[clap(long = "out-name")]
182    /// Sets the output file names. Defaults to package name.
183    pub out_name: Option<String>,
184
185    #[clap(long = "no-pack", alias = "no-package")]
186    /// Option to not generate a package.json
187    pub no_pack: bool,
188
189    #[clap(long = "no-opt", alias = "no-optimization")]
190    /// Option to skip optimization with wasm-opt
191    pub no_opt: bool,
192
193    #[clap(long = "panic-unwind")]
194    /// Build with panic=unwind. Requires the nightly Rust toolchain; uses
195    /// `-Z build-std` to rebuild `std` with `-Cpanic=unwind` so panics can be
196    /// caught at FFI boundaries instead of aborting the WebAssembly instance.
197    /// The nightly toolchain, `rust-src` component, and nightly
198    /// `wasm32-unknown-unknown` target will be installed via `rustup` if not
199    /// already present.
200    pub panic_unwind: bool,
201
202    /// List of extra options to pass to `cargo build`
203    pub extra_options: Vec<String>,
204}
205
206impl Default for BuildOptions {
207    fn default() -> Self {
208        Self {
209            path: None,
210            scope: None,
211            mode: InstallMode::default(),
212            disable_dts: false,
213            no_gitignore: false,
214            weak_refs: false,
215            reference_types: false,
216            target: Target::default(),
217            debug: false,
218            dev: false,
219            no_pack: false,
220            no_opt: false,
221            release: false,
222            profiling: false,
223            profile: None,
224            out_dir: String::new(),
225            out_name: None,
226            panic_unwind: false,
227            extra_options: Vec::new(),
228        }
229    }
230}
231
232type BuildStep = fn(&mut Build) -> Result<()>;
233
234impl Build {
235    /// Construct a build command from the given options.
236    pub fn try_from_opts(mut build_opts: BuildOptions) -> Result<Self> {
237        if let Some(path) = &build_opts.path {
238            if path.to_string_lossy().starts_with("--") {
239                let path = build_opts.path.take().unwrap();
240                build_opts
241                    .extra_options
242                    .insert(0, path.to_string_lossy().into_owned());
243            }
244        }
245        let crate_path = get_crate_path(build_opts.path)?;
246        let crate_data = manifest::CrateData::new(&crate_path, build_opts.out_name.clone())?;
247        let out_dir = crate_path.join(PathBuf::from(build_opts.out_dir)).clean();
248
249        let dev = build_opts.dev || build_opts.debug;
250        let profile = match (
251            dev,
252            build_opts.release,
253            build_opts.profiling,
254            build_opts.profile,
255        ) {
256            (false, false, false, None) | (false, true, false, None) => BuildProfile::Release,
257            (true, false, false, None) => BuildProfile::Dev,
258            (false, false, true, None) => BuildProfile::Profiling,
259            (false, false, false, Some(profile)) => BuildProfile::Custom(profile),
260            // Unfortunately, `clap` doesn't expose clap's `conflicts_with`
261            // functionality yet, so we have to implement it ourselves.
262            _ => bail!("Can only supply one of the --dev, --release, --profiling, or --profile 'name' flags"),
263        };
264
265        let extra_options = build_opts.extra_options;
266
267        // Resolve the cargo target triple in the same precedence order cargo
268        // uses, so wasm-pack and cargo always agree on what's being built:
269        //   1. `--target` in extra cargo arguments (after `--`)
270        //   2. `CARGO_BUILD_TARGET` env var
271        //   3. `[build] target = "..."` in `.cargo/config.toml` (walking up
272        //      from the crate dir, then `$CARGO_HOME/config.toml`)
273        //   4. fallback to `wasm32-unknown-unknown`
274        let target_triple = {
275            let mut iter = extra_options.iter();
276            let from_args = iter
277                .by_ref()
278                .find(|o| o.as_str() == "--target")
279                .and_then(|_| iter.next())
280                .cloned();
281            from_args
282                .or_else(|| std::env::var("CARGO_BUILD_TARGET").ok())
283                .or_else(|| read_cargo_build_target(&crate_path))
284                .unwrap_or_else(|| "wasm32-unknown-unknown".to_string())
285        };
286
287        Ok(Build {
288            crate_path,
289            crate_data,
290            scope: build_opts.scope,
291            disable_dts: build_opts.disable_dts,
292            no_gitignore: build_opts.no_gitignore,
293            weak_refs: build_opts.weak_refs,
294            reference_types: build_opts.reference_types,
295            target: build_opts.target,
296            no_pack: build_opts.no_pack,
297            no_opt: build_opts.no_opt,
298            profile,
299            mode: build_opts.mode,
300            out_dir,
301            out_name: build_opts.out_name,
302            bindgen: None,
303            cache: cache::get_wasm_pack_cache()?,
304            target_triple,
305            extra_options,
306            panic_unwind: build_opts.panic_unwind,
307            wasm_path: None,
308        })
309    }
310
311    /// Configures the global binary cache used for this build
312    pub fn set_cache(&mut self, cache: Cache) {
313        self.cache = cache;
314    }
315
316    /// Execute this `Build` command.
317    pub fn run(&mut self) -> Result<()> {
318        let process_steps = Build::get_process_steps(self.mode, self.no_pack, self.no_opt);
319
320        let started = Instant::now();
321
322        for (_, process_step) in process_steps {
323            process_step(self)?;
324        }
325
326        let duration = crate::command::utils::elapsed(started.elapsed());
327        info!("Done in {}.", &duration);
328        info!(
329            "Your wasm pkg is ready to publish at {}.",
330            self.out_dir.display()
331        );
332
333        PBAR.info(&format!("{} Done in {}", emoji::SPARKLE, &duration));
334
335        PBAR.info(&format!(
336            "{} Your wasm pkg is ready to publish at {}.",
337            emoji::PACKAGE,
338            self.out_dir.display()
339        ));
340        Ok(())
341    }
342
343    fn get_process_steps(
344        mode: InstallMode,
345        no_pack: bool,
346        no_opt: bool,
347    ) -> Vec<(&'static str, BuildStep)> {
348        macro_rules! steps {
349            ($($name:ident),+) => {
350                {
351                let mut steps: Vec<(&'static str, BuildStep)> = Vec::new();
352                    $(steps.push((stringify!($name), Build::$name));)*
353                        steps
354                    }
355                };
356            ($($name:ident,)*) => (steps![$($name),*])
357        }
358        let mut steps = Vec::new();
359        match &mode {
360            InstallMode::Force => {}
361            _ => {
362                steps.extend(steps![
363                    step_check_rustc_version,
364                    step_check_crate_config,
365                    step_check_for_wasm_target,
366                ]);
367            }
368        }
369
370        steps.extend(steps![
371            step_build_wasm,
372            step_create_dir,
373            step_install_wasm_bindgen,
374            step_run_wasm_bindgen,
375        ]);
376
377        if !no_opt {
378            steps.extend(steps![step_run_wasm_opt]);
379        }
380
381        if !no_pack {
382            steps.extend(steps![
383                step_create_json,
384                step_copy_readme,
385                step_copy_license,
386            ]);
387        }
388
389        steps
390    }
391
392    fn step_check_rustc_version(&mut self) -> Result<()> {
393        // The stable rustc version is irrelevant when --panic-unwind is set,
394        // since cargo will be invoked via `+nightly`.
395        if self.panic_unwind {
396            info!("Skipping rustc version check (using nightly via --panic-unwind).");
397            return Ok(());
398        }
399        info!("Checking rustc version...");
400        let version = build::check_rustc_version()?;
401        let msg = format!("rustc version is {}.", version);
402        info!("{}", &msg);
403        Ok(())
404    }
405
406    fn step_check_crate_config(&mut self) -> Result<()> {
407        info!("Checking crate configuration...");
408        self.crate_data.check_crate_config()?;
409        info!("Crate is correctly configured.");
410        Ok(())
411    }
412
413    fn step_check_for_wasm_target(&mut self) -> Result<()> {
414        if self.panic_unwind {
415            info!("Checking nightly toolchain prerequisites for panic=unwind...");
416            build::wasm_target::check_nightly_prerequisites()?;
417            info!("Nightly prerequisites check was successful.");
418            return Ok(());
419        }
420        info!("Checking for wasm-target...");
421        build::wasm_target::check_for_wasm_target(&self.target_triple)?;
422        info!("Checking for wasm-target was successful.");
423        Ok(())
424    }
425
426    fn step_build_wasm(&mut self) -> Result<()> {
427        info!("Building wasm...");
428        let wasm_path = build::cargo_build_wasm(
429            &self.crate_path,
430            self.profile.clone(),
431            &self.extra_options,
432            &self.target_triple,
433            self.panic_unwind,
434        )?;
435        info!("wasm built at {wasm_path:#?}.");
436        self.wasm_path = Some(wasm_path);
437        Ok(())
438    }
439
440    fn step_create_dir(&mut self) -> Result<()> {
441        info!("Creating a pkg directory...");
442        create_pkg_dir(&self.out_dir, self.no_gitignore)?;
443        info!("Created a pkg directory at {:#?}.", &self.crate_path);
444        Ok(())
445    }
446
447    fn step_create_json(&mut self) -> Result<()> {
448        self.crate_data.write_package_json(
449            &self.out_dir,
450            &self.scope,
451            self.disable_dts,
452            self.target,
453        )?;
454        info!(
455            "Wrote a package.json at {:#?}.",
456            &self.out_dir.join("package.json")
457        );
458        Ok(())
459    }
460
461    fn step_copy_readme(&mut self) -> Result<()> {
462        info!("Copying readme from crate...");
463        readme::copy_from_crate(&self.crate_data, &self.crate_path, &self.out_dir)?;
464        info!("Copied readme from crate to {:#?}.", &self.out_dir);
465        Ok(())
466    }
467
468    fn step_copy_license(&mut self) -> Result<()> {
469        info!("Copying license from crate...");
470        license::copy_from_crate(&self.crate_data, &self.crate_path, &self.out_dir)?;
471        info!("Copied license from crate to {:#?}.", &self.out_dir);
472        Ok(())
473    }
474
475    fn step_install_wasm_bindgen(&mut self) -> Result<()> {
476        info!("Identifying wasm-bindgen dependency...");
477        let lockfile = Lockfile::new(&self.crate_data)?;
478        let bindgen_version = lockfile.require_wasm_bindgen()?;
479        info!("Installing wasm-bindgen-cli...");
480        let bindgen = install::download_prebuilt_or_cargo_install(
481            Tool::WasmBindgen,
482            &self.cache,
483            bindgen_version,
484            self.mode.install_permitted(),
485        )?;
486        self.bindgen = Some(bindgen);
487        info!("Installing wasm-bindgen-cli was successful.");
488        Ok(())
489    }
490
491    fn step_run_wasm_bindgen(&mut self) -> Result<()> {
492        info!("Building the wasm bindings...");
493        bindgen::wasm_bindgen_build(
494            self.wasm_path.as_ref().unwrap(),
495            &self.crate_data,
496            self.bindgen.as_ref().unwrap(),
497            &self.out_dir,
498            &self.out_name,
499            self.disable_dts,
500            self.weak_refs,
501            self.reference_types,
502            self.target,
503            self.profile.clone(),
504        )?;
505        info!("wasm bindings were built at {:#?}.", &self.out_dir);
506        Ok(())
507    }
508
509    fn step_run_wasm_opt(&mut self) -> Result<()> {
510        let mut args = match self
511            .crate_data
512            .configured_profile(self.profile.clone())
513            .wasm_opt_args()
514        {
515            Some(args) => args,
516            None => return Ok(()),
517        };
518        if self.reference_types {
519            args.push("--enable-reference-types".into());
520        }
521        if build::is_tier3_wasm(&self.target_triple) {
522            args.push("--enable-memory64".into());
523        }
524        if self.panic_unwind {
525            args.push("--enable-exception-handling".into());
526        }
527        info!("executing wasm-opt with {:?}", args);
528        wasm_opt::run(
529            &self.cache,
530            &self.out_dir,
531            &args,
532            self.mode.install_permitted(),
533        ).map_err(|e| {
534            anyhow!(
535                "{}\nTo disable `wasm-opt`, add `wasm-opt = false` to your package metadata in your `Cargo.toml`.", e
536            )
537        })
538    }
539}
540
541/// Read the cargo `[build] target` setting from `.cargo/config.toml`.
542///
543/// Mirrors cargo's own discovery: walk up from `crate_path` checking
544/// `.cargo/config.toml` at each ancestor (workspace-aware) and finally
545/// check `$CARGO_HOME/config.toml` for user-level defaults. The first
546/// file that declares `[build] target = "..."` wins.
547pub(crate) fn read_cargo_build_target(crate_path: &std::path::Path) -> Option<String> {
548    for dir in crate_path.ancestors() {
549        if let Some(target) = parse_build_target(&dir.join(".cargo/config.toml")) {
550            return Some(target);
551        }
552    }
553    // Cargo falls back to $CARGO_HOME/config.toml (default ~/.cargo/config.toml)
554    // for user-wide settings. Honour the same precedence.
555    let cargo_home = std::env::var_os("CARGO_HOME")
556        .map(std::path::PathBuf::from)
557        .or_else(|| dirs::home_dir().map(|h| h.join(".cargo")))?;
558    parse_build_target(&cargo_home.join("config.toml"))
559}
560
561/// Parse `[build] target = "..."` from a single config file, if it
562/// exists and is well-formed. Returns `None` for missing files or
563/// configs that don't declare a target.
564fn parse_build_target(path: &std::path::Path) -> Option<String> {
565    let cfg = std::fs::read_to_string(path).ok()?;
566    let parsed: toml::Value = toml::from_str(&cfg).ok()?;
567    parsed
568        .get("build")?
569        .get("target")?
570        .as_str()
571        .map(str::to_owned)
572}
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577
578    #[test]
579    fn read_cargo_build_target_walks_up_to_workspace_root() {
580        let tmp = tempfile::TempDir::new().unwrap();
581        let root = tmp.path();
582        // Workspace root holds the .cargo/config.toml.
583        std::fs::create_dir_all(root.join(".cargo")).unwrap();
584        std::fs::write(
585            root.join(".cargo/config.toml"),
586            "[build]\ntarget = \"wasm64-unknown-unknown\"\n",
587        )
588        .unwrap();
589        // Crate lives two levels deeper with no config of its own.
590        let crate_path = root.join("crates/foo");
591        std::fs::create_dir_all(&crate_path).unwrap();
592
593        assert_eq!(
594            read_cargo_build_target(&crate_path),
595            Some("wasm64-unknown-unknown".to_string())
596        );
597    }
598
599    #[test]
600    fn read_cargo_build_target_prefers_crate_over_workspace() {
601        let tmp = tempfile::TempDir::new().unwrap();
602        let root = tmp.path();
603        std::fs::create_dir_all(root.join(".cargo")).unwrap();
604        std::fs::write(
605            root.join(".cargo/config.toml"),
606            "[build]\ntarget = \"wasm32-unknown-unknown\"\n",
607        )
608        .unwrap();
609        let crate_path = root.join("crates/foo");
610        std::fs::create_dir_all(crate_path.join(".cargo")).unwrap();
611        std::fs::write(
612            crate_path.join(".cargo/config.toml"),
613            "[build]\ntarget = \"wasm64-unknown-unknown\"\n",
614        )
615        .unwrap();
616
617        // Crate-level config should win.
618        assert_eq!(
619            read_cargo_build_target(&crate_path),
620            Some("wasm64-unknown-unknown".to_string())
621        );
622    }
623
624    #[test]
625    fn read_cargo_build_target_missing_returns_none() {
626        let tmp = tempfile::TempDir::new().unwrap();
627        // Use a deliberately-unreachable CARGO_HOME so the test is hermetic
628        // (otherwise it would race with developer state).
629        std::env::set_var("CARGO_HOME", tmp.path().join("nonexistent"));
630        assert_eq!(read_cargo_build_target(tmp.path()), None);
631    }
632}