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 weak_refs: bool,
33    pub reference_types: bool,
34    pub target: Target,
35    pub no_pack: bool,
36    pub no_opt: bool,
37    pub profile: BuildProfile,
38    pub mode: InstallMode,
39    pub out_dir: PathBuf,
40    pub out_name: Option<String>,
41    pub bindgen: Option<install::Status>,
42    pub cache: Cache,
43    pub extra_options: Vec<String>,
44    pub panic_unwind: bool,
45    target_triple: String,
46    wasm_path: Option<String>,
47}
48
49/// What sort of output we're going to be generating and flags we're invoking
50/// `wasm-bindgen` with.
51#[derive(Clone, Copy, Debug)]
52pub enum Target {
53    /// Default output mode or `--target bundler`, indicates output will be
54    /// used with a bundle in a later step.
55    Bundler,
56    /// Correspond to `--target web` where the output is natively usable as an
57    /// ES module in a browser and the wasm is manually instantiated.
58    Web,
59    /// Correspond to `--target nodejs` where the output is natively usable as
60    /// a Node.js module loaded with `require`.
61    Nodejs,
62    /// Correspond to `--target no-modules` where the output is natively usable
63    /// in a browser but pollutes the global namespace and must be manually
64    /// instantiated.
65    NoModules,
66    /// Correspond to `--target deno` where the output is natively usable as
67    /// a Deno module loaded with `import`.
68    Deno,
69}
70
71impl Default for Target {
72    fn default() -> Target {
73        Target::Bundler
74    }
75}
76
77impl fmt::Display for Target {
78    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
79        let s = match self {
80            Target::Bundler => "bundler",
81            Target::Web => "web",
82            Target::Nodejs => "nodejs",
83            Target::NoModules => "no-modules",
84            Target::Deno => "deno",
85        };
86        write!(f, "{}", s)
87    }
88}
89
90impl FromStr for Target {
91    type Err = Error;
92    fn from_str(s: &str) -> Result<Self> {
93        match s {
94            "bundler" | "browser" => Ok(Target::Bundler),
95            "web" => Ok(Target::Web),
96            "nodejs" => Ok(Target::Nodejs),
97            "no-modules" => Ok(Target::NoModules),
98            "deno" => Ok(Target::Deno),
99            _ => bail!("Unknown target: {}", s),
100        }
101    }
102}
103
104/// The build profile controls whether optimizations, debug info, and assertions
105/// are enabled or disabled.
106#[derive(Clone, Debug)]
107pub enum BuildProfile {
108    /// Enable assertions and debug info. Disable optimizations.
109    Dev,
110    /// Enable optimizations. Disable assertions and debug info.
111    Release,
112    /// Enable optimizations and debug info. Disable assertions.
113    Profiling,
114    /// User-defined profile with --profile flag
115    Custom(String),
116}
117
118/// Everything required to configure and run the `wasm-pack build` command.
119#[derive(Debug, Args)]
120#[command(allow_hyphen_values = true, trailing_var_arg = true)]
121pub struct BuildOptions {
122    /// The path to the Rust crate. If not set, searches up the path from the current directory.
123    #[clap()]
124    pub path: Option<PathBuf>,
125
126    /// The npm scope to use in package.json, if any.
127    #[clap(long = "scope", short = 's')]
128    pub scope: Option<String>,
129
130    #[clap(long = "mode", short = 'm', default_value = "normal")]
131    /// Sets steps to be run. [possible values: no-install, normal, force]
132    pub mode: InstallMode,
133
134    #[clap(long = "no-typescript")]
135    /// By default a *.d.ts file is generated for the generated JS file, but
136    /// this flag will disable generating this TypeScript file.
137    pub disable_dts: bool,
138
139    #[clap(long = "weak-refs")]
140    /// Enable usage of the JS weak references proposal.
141    pub weak_refs: bool,
142
143    #[clap(long = "reference-types")]
144    /// Enable usage of WebAssembly reference types.
145    pub reference_types: bool,
146
147    #[clap(long = "target", short = 't', default_value = "bundler")]
148    /// Sets the target environment. [possible values: bundler, nodejs, web, no-modules, deno]
149    pub target: Target,
150
151    #[clap(long = "debug")]
152    /// Deprecated. Renamed to `--dev`.
153    pub debug: bool,
154
155    #[clap(long = "dev")]
156    /// Create a development build. Enable debug info, and disable
157    /// optimizations.
158    pub dev: bool,
159
160    #[clap(long = "release")]
161    /// Create a release build. Enable optimizations and disable debug info.
162    pub release: bool,
163
164    #[clap(long = "profiling")]
165    /// Create a profiling build. Enable optimizations and debug info.
166    pub profiling: bool,
167
168    #[clap(long = "profile")]
169    /// User-defined profile with --profile flag
170    pub profile: Option<String>,
171
172    #[clap(long = "out-dir", short = 'd', default_value = "pkg")]
173    /// Sets the output directory with a relative path.
174    pub out_dir: String,
175
176    #[clap(long = "out-name")]
177    /// Sets the output file names. Defaults to package name.
178    pub out_name: Option<String>,
179
180    #[clap(long = "no-pack", alias = "no-package")]
181    /// Option to not generate a package.json
182    pub no_pack: bool,
183
184    #[clap(long = "no-opt", alias = "no-optimization")]
185    /// Option to skip optimization with wasm-opt
186    pub no_opt: bool,
187
188    #[clap(long = "panic-unwind")]
189    /// Build with panic=unwind. Requires the nightly Rust toolchain; uses
190    /// `-Z build-std` to rebuild `std` with `-Cpanic=unwind` so panics can be
191    /// caught at FFI boundaries instead of aborting the WebAssembly instance.
192    /// The nightly toolchain, `rust-src` component, and nightly
193    /// `wasm32-unknown-unknown` target will be installed via `rustup` if not
194    /// already present.
195    pub panic_unwind: bool,
196
197    /// List of extra options to pass to `cargo build`
198    pub extra_options: Vec<String>,
199}
200
201impl Default for BuildOptions {
202    fn default() -> Self {
203        Self {
204            path: None,
205            scope: None,
206            mode: InstallMode::default(),
207            disable_dts: false,
208            weak_refs: false,
209            reference_types: false,
210            target: Target::default(),
211            debug: false,
212            dev: false,
213            no_pack: false,
214            no_opt: false,
215            release: false,
216            profiling: false,
217            profile: None,
218            out_dir: String::new(),
219            out_name: None,
220            panic_unwind: false,
221            extra_options: Vec::new(),
222        }
223    }
224}
225
226type BuildStep = fn(&mut Build) -> Result<()>;
227
228impl Build {
229    /// Construct a build command from the given options.
230    pub fn try_from_opts(mut build_opts: BuildOptions) -> Result<Self> {
231        if let Some(path) = &build_opts.path {
232            if path.to_string_lossy().starts_with("--") {
233                let path = build_opts.path.take().unwrap();
234                build_opts
235                    .extra_options
236                    .insert(0, path.to_string_lossy().into_owned());
237            }
238        }
239        let crate_path = get_crate_path(build_opts.path)?;
240        let crate_data = manifest::CrateData::new(&crate_path, build_opts.out_name.clone())?;
241        let out_dir = crate_path.join(PathBuf::from(build_opts.out_dir)).clean();
242
243        let dev = build_opts.dev || build_opts.debug;
244        let profile = match (
245            dev,
246            build_opts.release,
247            build_opts.profiling,
248            build_opts.profile,
249        ) {
250            (false, false, false, None) | (false, true, false, None) => BuildProfile::Release,
251            (true, false, false, None) => BuildProfile::Dev,
252            (false, false, true, None) => BuildProfile::Profiling,
253            (false, false, false, Some(profile)) => BuildProfile::Custom(profile),
254            // Unfortunately, `clap` doesn't expose clap's `conflicts_with`
255            // functionality yet, so we have to implement it ourselves.
256            _ => bail!("Can only supply one of the --dev, --release, --profiling, or --profile 'name' flags"),
257        };
258
259        let extra_options = build_opts.extra_options;
260
261        let target_triple = {
262            let mut extra_options_iter = extra_options.iter();
263            if extra_options_iter
264                .by_ref()
265                .any(|option| option == "--target")
266            {
267                extra_options_iter.next().map(|s| s.as_str())
268            } else {
269                None
270            }
271            .unwrap_or("wasm32-unknown-unknown")
272        };
273
274        Ok(Build {
275            crate_path,
276            crate_data,
277            scope: build_opts.scope,
278            disable_dts: build_opts.disable_dts,
279            weak_refs: build_opts.weak_refs,
280            reference_types: build_opts.reference_types,
281            target: build_opts.target,
282            no_pack: build_opts.no_pack,
283            no_opt: build_opts.no_opt,
284            profile,
285            mode: build_opts.mode,
286            out_dir,
287            out_name: build_opts.out_name,
288            bindgen: None,
289            cache: cache::get_wasm_pack_cache()?,
290            target_triple: target_triple.to_owned(),
291            extra_options,
292            panic_unwind: build_opts.panic_unwind,
293            wasm_path: None,
294        })
295    }
296
297    /// Configures the global binary cache used for this build
298    pub fn set_cache(&mut self, cache: Cache) {
299        self.cache = cache;
300    }
301
302    /// Execute this `Build` command.
303    pub fn run(&mut self) -> Result<()> {
304        let process_steps = Build::get_process_steps(self.mode, self.no_pack, self.no_opt);
305
306        let started = Instant::now();
307
308        for (_, process_step) in process_steps {
309            process_step(self)?;
310        }
311
312        let duration = crate::command::utils::elapsed(started.elapsed());
313        info!("Done in {}.", &duration);
314        info!(
315            "Your wasm pkg is ready to publish at {}.",
316            self.out_dir.display()
317        );
318
319        PBAR.info(&format!("{} Done in {}", emoji::SPARKLE, &duration));
320
321        PBAR.info(&format!(
322            "{} Your wasm pkg is ready to publish at {}.",
323            emoji::PACKAGE,
324            self.out_dir.display()
325        ));
326        Ok(())
327    }
328
329    fn get_process_steps(
330        mode: InstallMode,
331        no_pack: bool,
332        no_opt: bool,
333    ) -> Vec<(&'static str, BuildStep)> {
334        macro_rules! steps {
335            ($($name:ident),+) => {
336                {
337                let mut steps: Vec<(&'static str, BuildStep)> = Vec::new();
338                    $(steps.push((stringify!($name), Build::$name));)*
339                        steps
340                    }
341                };
342            ($($name:ident,)*) => (steps![$($name),*])
343        }
344        let mut steps = Vec::new();
345        match &mode {
346            InstallMode::Force => {}
347            _ => {
348                steps.extend(steps![
349                    step_check_rustc_version,
350                    step_check_crate_config,
351                    step_check_for_wasm_target,
352                ]);
353            }
354        }
355
356        steps.extend(steps![
357            step_build_wasm,
358            step_create_dir,
359            step_install_wasm_bindgen,
360            step_run_wasm_bindgen,
361        ]);
362
363        if !no_opt {
364            steps.extend(steps![step_run_wasm_opt]);
365        }
366
367        if !no_pack {
368            steps.extend(steps![
369                step_create_json,
370                step_copy_readme,
371                step_copy_license,
372            ]);
373        }
374
375        steps
376    }
377
378    fn step_check_rustc_version(&mut self) -> Result<()> {
379        // The stable rustc version is irrelevant when --panic-unwind is set,
380        // since cargo will be invoked via `+nightly`.
381        if self.panic_unwind {
382            info!("Skipping rustc version check (using nightly via --panic-unwind).");
383            return Ok(());
384        }
385        info!("Checking rustc version...");
386        let version = build::check_rustc_version()?;
387        let msg = format!("rustc version is {}.", version);
388        info!("{}", &msg);
389        Ok(())
390    }
391
392    fn step_check_crate_config(&mut self) -> Result<()> {
393        info!("Checking crate configuration...");
394        self.crate_data.check_crate_config()?;
395        info!("Crate is correctly configured.");
396        Ok(())
397    }
398
399    fn step_check_for_wasm_target(&mut self) -> Result<()> {
400        if self.panic_unwind {
401            info!("Checking nightly toolchain prerequisites for panic=unwind...");
402            build::wasm_target::check_nightly_prerequisites()?;
403            info!("Nightly prerequisites check was successful.");
404            return Ok(());
405        }
406        info!("Checking for wasm-target...");
407        build::wasm_target::check_for_wasm_target(&self.target_triple)?;
408        info!("Checking for wasm-target was successful.");
409        Ok(())
410    }
411
412    fn step_build_wasm(&mut self) -> Result<()> {
413        info!("Building wasm...");
414        let wasm_path = build::cargo_build_wasm(
415            &self.crate_path,
416            self.profile.clone(),
417            &self.extra_options,
418            &self.target_triple,
419            self.panic_unwind,
420        )?;
421        info!("wasm built at {wasm_path:#?}.");
422        self.wasm_path = Some(wasm_path);
423        Ok(())
424    }
425
426    fn step_create_dir(&mut self) -> Result<()> {
427        info!("Creating a pkg directory...");
428        create_pkg_dir(&self.out_dir)?;
429        info!("Created a pkg directory at {:#?}.", &self.crate_path);
430        Ok(())
431    }
432
433    fn step_create_json(&mut self) -> Result<()> {
434        self.crate_data.write_package_json(
435            &self.out_dir,
436            &self.scope,
437            self.disable_dts,
438            self.target,
439        )?;
440        info!(
441            "Wrote a package.json at {:#?}.",
442            &self.out_dir.join("package.json")
443        );
444        Ok(())
445    }
446
447    fn step_copy_readme(&mut self) -> Result<()> {
448        info!("Copying readme from crate...");
449        readme::copy_from_crate(&self.crate_data, &self.crate_path, &self.out_dir)?;
450        info!("Copied readme from crate to {:#?}.", &self.out_dir);
451        Ok(())
452    }
453
454    fn step_copy_license(&mut self) -> Result<()> {
455        info!("Copying license from crate...");
456        license::copy_from_crate(&self.crate_data, &self.crate_path, &self.out_dir)?;
457        info!("Copied license from crate to {:#?}.", &self.out_dir);
458        Ok(())
459    }
460
461    fn step_install_wasm_bindgen(&mut self) -> Result<()> {
462        info!("Identifying wasm-bindgen dependency...");
463        let lockfile = Lockfile::new(&self.crate_data)?;
464        let bindgen_version = lockfile.require_wasm_bindgen()?;
465        info!("Installing wasm-bindgen-cli...");
466        let bindgen = install::download_prebuilt_or_cargo_install(
467            Tool::WasmBindgen,
468            &self.cache,
469            bindgen_version,
470            self.mode.install_permitted(),
471        )?;
472        self.bindgen = Some(bindgen);
473        info!("Installing wasm-bindgen-cli was successful.");
474        Ok(())
475    }
476
477    fn step_run_wasm_bindgen(&mut self) -> Result<()> {
478        info!("Building the wasm bindings...");
479        bindgen::wasm_bindgen_build(
480            self.wasm_path.as_ref().unwrap(),
481            &self.crate_data,
482            self.bindgen.as_ref().unwrap(),
483            &self.out_dir,
484            &self.out_name,
485            self.disable_dts,
486            self.weak_refs,
487            self.reference_types,
488            self.target,
489            self.profile.clone(),
490        )?;
491        info!("wasm bindings were built at {:#?}.", &self.out_dir);
492        Ok(())
493    }
494
495    fn step_run_wasm_opt(&mut self) -> Result<()> {
496        let mut args = match self
497            .crate_data
498            .configured_profile(self.profile.clone())
499            .wasm_opt_args()
500        {
501            Some(args) => args,
502            None => return Ok(()),
503        };
504        if self.reference_types {
505            args.push("--enable-reference-types".into());
506        }
507        if self.target_triple.starts_with("wasm64") {
508            args.push("--enable-memory64".into());
509        }
510        info!("executing wasm-opt with {:?}", args);
511        wasm_opt::run(
512            &self.cache,
513            &self.out_dir,
514            &args,
515            self.mode.install_permitted(),
516        ).map_err(|e| {
517            anyhow!(
518                "{}\nTo disable `wasm-opt`, add `wasm-opt = false` to your package metadata in your `Cargo.toml`.", e
519            )
520        })
521    }
522}