1use 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#[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#[derive(Clone, Copy, Debug)]
53pub enum Target {
54 Bundler,
57 Web,
60 Nodejs,
63 NoModules,
67 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#[derive(Clone, Debug)]
108pub enum BuildProfile {
109 Dev,
111 Release,
113 Profiling,
115 Custom(String),
117}
118
119#[derive(Debug, Args)]
121#[command(allow_hyphen_values = true, trailing_var_arg = true)]
122pub struct BuildOptions {
123 #[clap()]
125 pub path: Option<PathBuf>,
126
127 #[clap(long = "scope", short = 's')]
129 pub scope: Option<String>,
130
131 #[clap(long = "mode", short = 'm', default_value = "normal")]
132 pub mode: InstallMode,
134
135 #[clap(long = "no-typescript")]
136 pub disable_dts: bool,
139
140 #[clap(long = "no-gitignore")]
141 pub no_gitignore: bool,
143
144 #[clap(long = "weak-refs")]
145 pub weak_refs: bool,
147
148 #[clap(long = "reference-types")]
149 pub reference_types: bool,
151
152 #[clap(long = "target", short = 't', default_value = "bundler")]
153 pub target: Target,
155
156 #[clap(long = "debug")]
157 pub debug: bool,
159
160 #[clap(long = "dev")]
161 pub dev: bool,
164
165 #[clap(long = "release")]
166 pub release: bool,
168
169 #[clap(long = "profiling")]
170 pub profiling: bool,
172
173 #[clap(long = "profile")]
174 pub profile: Option<String>,
176
177 #[clap(long = "out-dir", short = 'd', default_value = "pkg")]
178 pub out_dir: String,
180
181 #[clap(long = "out-name")]
182 pub out_name: Option<String>,
184
185 #[clap(long = "no-pack", alias = "no-package")]
186 pub no_pack: bool,
188
189 #[clap(long = "no-opt", alias = "no-optimization")]
190 pub no_opt: bool,
192
193 #[clap(long = "panic-unwind")]
194 pub panic_unwind: bool,
201
202 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 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 _ => 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 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 pub fn set_cache(&mut self, cache: Cache) {
313 self.cache = cache;
314 }
315
316 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 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
541pub(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 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
561fn 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 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 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 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 std::env::set_var("CARGO_HOME", tmp.path().join("nonexistent"));
630 assert_eq!(read_cargo_build_target(tmp.path()), None);
631 }
632}