wasm_js/command/
build.rs

1use crate::bindgen;
2use crate::build;
3use crate::install::{self, InstallMode, Tool};
4use crate::js_bin::WasmJsWriter;
5use crate::lockfile::Lockfile;
6use crate::manifest;
7use crate::utils::*;
8use crate::wasm_opt;
9use crate::PBAR;
10
11use anyhow::{anyhow, bail, Result};
12use binary_install::Cache;
13use clap::Args;
14use log::info;
15use path_clean::PathClean;
16use std::fs;
17use std::fs::File;
18use std::io::BufWriter;
19use std::io::Write;
20use std::path::Path;
21use std::path::PathBuf;
22use std::time::Instant;
23
24/// Everything required to configure and run the `wasm-js build` command.
25pub struct Build {
26    pub crate_path: PathBuf,
27    pub crate_data: manifest::CrateData,
28    pub weak_refs: bool,
29    pub reference_types: bool,
30    pub no_opt: bool,
31    pub profile: BuildProfile,
32    pub mode: InstallMode,
33    pub out_dir: PathBuf,
34    pub out_name: Option<String>,
35    pub bindgen: Option<install::Status>,
36    pub cache: Cache,
37    pub extra_options: Vec<String>,
38}
39
40/// The build profile controls whether optimizations, debug info, and assertions
41/// are enabled or disabled.
42#[derive(Clone, Debug)]
43pub enum BuildProfile {
44    /// Enable assertions and debug info. Disable optimizations.
45    Dev,
46    /// Enable optimizations. Disable assertions and debug info.
47    Release,
48    /// Enable optimizations and debug info. Disable assertions.
49    Profiling,
50    /// User-defined profile with --profile flag
51    Custom(String),
52}
53
54/// Everything required to configure and run the build command.
55#[derive(Debug, Args)]
56#[command(allow_hyphen_values = true, trailing_var_arg = true)]
57pub struct BuildOptions {
58    /// The path to the Rust crate. If not set, searches up the path from the current directory.
59    #[clap()]
60    pub path: Option<PathBuf>,
61
62    #[clap(long = "mode", short = 'm', default_value = "normal")]
63    /// Sets steps to be run. [possible values: no-install, normal, force]
64    pub mode: InstallMode,
65
66    #[clap(long = "weak-refs")]
67    /// Enable usage of the JS weak references proposal.
68    pub weak_refs: bool,
69
70    #[clap(long = "reference-types")]
71    /// Enable usage of WebAssembly reference types.
72    pub reference_types: bool,
73
74    #[clap(long = "debug")]
75    /// Deprecated. Renamed to `--dev`.
76    pub debug: bool,
77
78    #[clap(long = "dev")]
79    /// Create a development build. Enable debug info, and disable
80    /// optimizations.
81    pub dev: bool,
82
83    #[clap(long = "release")]
84    /// Create a release build. Enable optimizations and disable debug info.
85    pub release: bool,
86
87    #[clap(long = "profiling")]
88    /// Create a profiling build. Enable optimizations and debug info.
89    pub profiling: bool,
90
91    #[clap(long = "profile")]
92    /// User-defined profile with --profile flag
93    pub profile: Option<String>,
94
95    #[clap(long = "out-dir", short = 'd', default_value = "dist")]
96    /// Sets the output directory with a relative path.
97    pub out_dir: String,
98
99    #[clap(long = "out-name")]
100    /// Sets the output file names. Defaults to package name.
101    pub out_name: Option<String>,
102
103    #[clap(long = "no-opt", alias = "no-optimization")]
104    /// Option to skip optimization with wasm-opt
105    pub no_opt: bool,
106
107    /// List of extra options to pass to `cargo build`
108    pub extra_options: Vec<String>,
109}
110
111impl Default for BuildOptions {
112    fn default() -> Self {
113        Self {
114            path: None,
115            mode: InstallMode::default(),
116            weak_refs: false,
117            reference_types: false,
118            debug: false,
119            dev: false,
120            no_opt: false,
121            release: false,
122            profiling: false,
123            profile: None,
124            out_dir: String::new(),
125            out_name: None,
126            extra_options: Vec::new(),
127        }
128    }
129}
130
131impl Build {
132    /// Construct a build command from the given options.
133    pub fn try_from_opts(args: &crate::Cli, build_opts: &BuildOptions) -> Result<Self> {
134        let mut extra_options: Vec<String> = build_opts.extra_options.clone();
135        let mut path_arg = build_opts.path.clone();
136        if let Some(path) = &build_opts.path {
137            if path.to_string_lossy().starts_with("--") {
138                let path = path_arg.take().unwrap();
139                extra_options.insert(0, path.to_string_lossy().into_owned());
140            }
141        }
142        let crate_path = get_crate_path(path_arg)?;
143        let crate_data = manifest::CrateData::new(&crate_path, build_opts.out_name.clone())?;
144        let out_dir = crate_path.join(PathBuf::from(&build_opts.out_dir)).clean();
145
146        let dev = build_opts.dev || build_opts.debug;
147        let profile = match (
148            dev,
149            build_opts.release,
150            build_opts.profiling,
151            &build_opts.profile,
152        ) {
153            (false, false, false, None) | (false, true, false, None) => BuildProfile::Release,
154            (true, false, false, None) => BuildProfile::Dev,
155            (false, false, true, None) => BuildProfile::Profiling,
156            (false, false, false, Some(profile)) => BuildProfile::Custom(profile.clone()),
157            // Unfortunately, `clap` doesn't expose clap's `conflicts_with`
158            // functionality yet, so we have to implement it ourselves.
159            _ => bail!("Can only supply one of the --dev, --release, --profiling, or --profile 'name' flags"),
160        };
161
162        Ok(Build {
163            crate_path,
164            crate_data,
165            weak_refs: build_opts.weak_refs,
166            reference_types: build_opts.reference_types,
167            no_opt: build_opts.no_opt,
168            profile,
169            mode: build_opts.mode,
170            out_dir,
171            out_name: build_opts.out_name.clone(),
172            bindgen: None,
173            cache: get_install_cache(&args.install_cache)?,
174            extra_options: extra_options,
175        })
176    }
177
178    /// Configures the global binary cache used for this build
179    pub fn set_cache(&mut self, cache: Cache) {
180        self.cache = cache;
181    }
182
183    /// Execute this `Build` command.
184    pub fn run(&mut self) -> Result<()> {
185        let started = Instant::now();
186
187        if self.mode != InstallMode::Force {
188            self.step_check_rustc_version()?;
189            self.step_check_crate_config()?;
190            self.step_check_for_wasm_target()?;
191        }
192
193        self.step_build_wasm()?;
194        self.step_create_dir()?;
195        self.step_install_wasm_bindgen()?;
196        let temp_dir = self.step_run_wasm_bindgen()?;
197
198        if !self.no_opt {
199            self.step_run_wasm_opt()?;
200        }
201        self.step_transform_wasm(&temp_dir)?;
202
203        let duration = elapsed(started.elapsed());
204        info!("Done in {}.", &duration);
205        info!("Javascript files created in {}.", self.out_dir.display());
206
207        PBAR.info(&format!("Done in {}", &duration));
208
209        PBAR.info(&format!(
210            "Javascript files created in {}.",
211            self.out_dir.display()
212        ));
213        Ok(())
214    }
215
216    fn step_check_rustc_version(&mut self) -> Result<()> {
217        info!("Checking rustc version...");
218        let version = build::check_rustc_version()?;
219        let msg = format!("rustc version is {}.", version);
220        info!("{}", &msg);
221        Ok(())
222    }
223
224    fn step_check_crate_config(&mut self) -> Result<()> {
225        info!("Checking crate configuration...");
226        self.crate_data.check_crate_config()?;
227        info!("Crate is correctly configured.");
228        Ok(())
229    }
230
231    fn step_check_for_wasm_target(&mut self) -> Result<()> {
232        info!("Checking for wasm-target...");
233        build::wasm_target::check_for_wasm32_target()?;
234        info!("Checking for wasm-target was successful.");
235        Ok(())
236    }
237
238    fn step_build_wasm(&mut self) -> Result<()> {
239        info!("Building wasm...");
240        build::cargo_build_wasm(&self.crate_path, self.profile.clone(), &self.extra_options)?;
241
242        info!(
243            "wasm built at {:#?}.",
244            &self
245                .crate_path
246                .join("target")
247                .join("wasm32-unknown-unknown")
248                .join("release")
249        );
250        Ok(())
251    }
252
253    fn step_create_dir(&mut self) -> Result<()> {
254        info!("Creating a dist directory...");
255        create_output_dir(&self.out_dir)?;
256        info!("Created a dist directory at {:#?}.", &self.crate_path);
257        Ok(())
258    }
259
260    fn step_install_wasm_bindgen(&mut self) -> Result<()> {
261        info!("Identifying wasm-bindgen dependency...");
262        let lockfile = Lockfile::new(&self.crate_data)?;
263        let bindgen_version = lockfile.require_wasm_bindgen()?;
264        info!("Installing wasm-bindgen-cli...");
265        let bindgen = install::download_prebuilt_or_cargo_install(
266            Tool::WasmBindgen,
267            &self.cache,
268            bindgen_version,
269            self.mode.install_permitted(),
270        )?;
271        self.bindgen = Some(bindgen);
272        info!("Installing wasm-bindgen-cli was successful.");
273        Ok(())
274    }
275
276    fn step_run_wasm_bindgen(&mut self) -> Result<PathBuf> {
277        info!("Building the wasm bindings...");
278        let temp_dir = bindgen::wasm_bindgen_build(
279            &self.crate_data,
280            self.bindgen.as_ref().unwrap(),
281            &self.out_name,
282            self.weak_refs,
283            self.reference_types,
284            self.profile.clone(),
285            &self.extra_options,
286        )?;
287        info!("wasm bindings were built at {:#?}.", &temp_dir);
288        Ok(temp_dir)
289    }
290
291    fn step_run_wasm_opt(&mut self) -> Result<()> {
292        let mut args = match self
293            .crate_data
294            .configured_profile(self.profile.clone())
295            .wasm_opt_args()
296        {
297            Some(args) => args,
298            None => return Ok(()),
299        };
300        if self.reference_types {
301            args.push("--enable-reference-types".into());
302        }
303        info!("executing wasm-opt with {:?}", args);
304        wasm_opt::run(
305            &self.cache,
306            &self.out_dir,
307            &args,
308            self.mode.install_permitted(),
309        ).map_err(|e| {
310            anyhow!(
311                "{}\nTo disable `wasm-opt`, add `wasm-opt = false` to your package metadata in your `Cargo.toml`.", e
312            )
313        })
314    }
315
316    fn step_transform_wasm(&mut self, temp_dir: &Path) -> Result<()> {
317        let name_prefix = self.crate_data.name_prefix();
318        let wasm_filename = format!("{}_bg.wasm", name_prefix);
319        let imports_filename = format!("{}_bg.js", name_prefix);
320        let types_filename = format!("{}.d.ts", name_prefix);
321        let module_filename = format!("{}.js", name_prefix);
322        let imports_module = format!("./{}", imports_filename);
323        // convert wasm to JS
324        {
325            let mut outfile = File::create(self.out_dir.join(module_filename))?;
326            {
327                let mut outbw = BufWriter::new(&mut outfile);
328                let mut wasm_writer = WasmJsWriter::new(&mut outbw, &imports_module);
329                let input_path = temp_dir.join(wasm_filename);
330                read_and_compress(&mut wasm_writer, &input_path)?;
331                wasm_writer.flush()?;
332            }
333            outfile.sync_all()?;
334        }
335        fs::copy(
336            temp_dir.join(&imports_filename),
337            self.out_dir.join(&imports_filename),
338        )?;
339        // transform types file
340        {
341            let types_text = fs::read(temp_dir.join(&types_filename))?;
342            let mut outfile = File::create(self.out_dir.join(&types_filename))?;
343            {
344                let mut outbw = BufWriter::new(&mut outfile);
345                outbw.write_all(
346                    "/* tslint:disable */\n/* eslint-disable */\ndeclare namespace WasmDecls {\n"
347                        .to_os_bytes()
348                        .as_ref(),
349                )?;
350                outbw.write_all(&types_text)?;
351                outbw.write_all(
352                    "\n}\nexport type WasmExports = typeof WasmDecls;\nexport function getWasm(): Promise<WasmExports>;\n"
353                        .to_os_bytes()
354                        .as_ref(),
355                )?;
356                outbw.flush()?;
357            }
358            outfile.sync_all()?;
359        }
360
361        for file in self.out_dir.read_dir()? {
362            let file = file?;
363            let path = file.path();
364            let extension = path.extension().and_then(|s| s.to_str());
365            if extension == Some("wasm") {}
366        }
367
368        Ok(())
369    }
370}