1use crate::{
2 anyhow::{ensure, Context, Result},
3 camino, clap, default_build_command, metadata,
4};
5use derive_more::Debug;
6use std::{fs, path::PathBuf, process};
7use wasm_bindgen_cli_support::Bindgen;
8
9pub trait Transformer {
60 fn transform(&self, source: &Path, dest: &Path) -> Result<bool>;
68}
69
70use std::path::Path;
71
72impl Transformer for () {
73 fn transform(&self, _source: &Path, _dest: &Path) -> Result<bool> {
74 Ok(false)
75 }
76}
77
78#[non_exhaustive]
115#[derive(Debug, clap::Parser)]
116#[clap(
117 about = "Generate the distributed package.",
118 long_about = "Generate the distributed package.\n\
119 It will build and package the project for WASM."
120)]
121pub struct Dist {
122 #[clap(short, long)]
124 pub quiet: bool,
125 #[clap(short, long)]
127 pub jobs: Option<String>,
128 #[clap(long)]
130 pub profile: Option<String>,
131 #[clap(long)]
133 pub release: bool,
134 #[clap(long)]
136 pub features: Vec<String>,
137 #[clap(long)]
139 pub all_features: bool,
140 #[clap(long)]
142 pub no_default_features: bool,
143 #[clap(short, long)]
145 pub verbose: bool,
146 #[clap(long)]
148 pub color: Option<String>,
149 #[clap(long)]
151 pub frozen: bool,
152 #[clap(long)]
154 pub locked: bool,
155 #[clap(long)]
157 pub offline: bool,
158 #[clap(long)]
160 pub ignore_rust_version: bool,
161 #[clap(long)]
163 pub example: Option<String>,
164
165 #[clap(skip = default_build_command())]
167 pub build_command: process::Command,
168 #[clap(skip)]
170 pub dist_dir: Option<PathBuf>,
171 #[clap(skip)]
175 pub assets_dir: Option<PathBuf>,
176 #[clap(skip)]
178 pub app_name: Option<String>,
179 #[clap(skip)]
185 #[debug(skip)]
186 pub transformers: Vec<Box<dyn Transformer>>,
187
188 #[cfg(feature = "wasm-opt")]
192 #[clap(skip)]
193 pub wasm_opt: Option<crate::WasmOpt>,
194}
195
196impl Dist {
197 pub fn build_command(mut self, command: process::Command) -> Self {
201 self.build_command = command;
202 self
203 }
204
205 pub fn dist_dir(mut self, path: impl Into<PathBuf>) -> Self {
210 self.dist_dir = Some(path.into());
211 self
212 }
213
214 pub fn assets_dir(mut self, path: impl Into<PathBuf>) -> Self {
218 self.assets_dir = Some(path.into());
219 self
220 }
221
222 pub fn app_name(mut self, app_name: impl Into<String>) -> Self {
226 self.app_name = Some(app_name.into());
227 self
228 }
229
230 pub fn transformer(mut self, transformer: impl Transformer + 'static) -> Self {
234 self.transformers.push(Box::new(transformer));
235 self
236 }
237
238 #[cfg(feature = "wasm-opt")]
273 #[cfg_attr(docsrs, doc(cfg(feature = "wasm-opt")))]
274 pub fn optimize_wasm(mut self, wasm_opt: crate::WasmOpt) -> Self {
275 self.wasm_opt = Some(wasm_opt);
276 self
277 }
278
279 pub fn example(mut self, example: impl Into<String>) -> Self {
281 self.example = Some(example.into());
282 self
283 }
284
285 pub fn default_debug_dir() -> camino::Utf8PathBuf {
287 metadata().target_directory.join("debug").join("dist")
288 }
289
290 pub fn default_release_dir() -> camino::Utf8PathBuf {
292 metadata().target_directory.join("release").join("dist")
293 }
294
295 #[cfg_attr(
301 feature = "wasm-opt",
302 doc = "Wasm optimizations can be achieved using [`WasmOpt`](crate::WasmOpt) if the feature `wasm-opt` is enabled."
303 )]
304 #[cfg_attr(
305 not(feature = "wasm-opt"),
306 doc = "Wasm optimizations can be achieved using `WasmOpt` if the feature `wasm-opt` is enabled."
307 )]
308 pub fn build(self, package_name: &str) -> Result<PathBuf> {
309 log::trace!("Getting package's metadata");
310 let metadata = metadata();
311
312 let dist_dir = self.dist_dir.unwrap_or_else(|| {
313 if self.release {
314 Self::default_release_dir().into()
315 } else {
316 Self::default_debug_dir().into()
317 }
318 });
319
320 log::trace!("Initializing dist process");
321 let mut build_command = self.build_command;
322
323 build_command.current_dir(&metadata.workspace_root);
324
325 if self.quiet {
326 build_command.arg("--quiet");
327 }
328
329 if let Some(number) = self.jobs {
330 build_command.args(["--jobs", &number]);
331 }
332
333 if let Some(profile) = self.profile {
334 build_command.args(["--profile", &profile]);
335 }
336
337 if self.release {
338 build_command.arg("--release");
339 }
340
341 for feature in &self.features {
342 build_command.args(["--features", feature]);
343 }
344
345 if self.all_features {
346 build_command.arg("--all-features");
347 }
348
349 if self.no_default_features {
350 build_command.arg("--no-default-features");
351 }
352
353 if self.verbose {
354 build_command.arg("--verbose");
355 }
356
357 if let Some(color) = self.color {
358 build_command.args(["--color", &color]);
359 }
360
361 if self.frozen {
362 build_command.arg("--frozen");
363 }
364
365 if self.locked {
366 build_command.arg("--locked");
367 }
368
369 if self.offline {
370 build_command.arg("--offline");
371 }
372
373 if self.ignore_rust_version {
374 build_command.arg("--ignore-rust-version");
375 }
376
377 build_command.args(["--package", package_name]);
378
379 if let Some(example) = &self.example {
380 build_command.args(["--example", example]);
381 } else {
382 build_command.arg("--lib");
383 }
384
385 let build_dir = metadata
386 .target_directory
387 .join("wasm32-unknown-unknown")
388 .join(if self.release { "release" } else { "debug" });
389 let input_path = if let Some(example) = &self.example {
390 build_dir
391 .join("examples")
392 .join(example.replace('-', "_"))
393 .with_extension("wasm")
394 } else {
395 build_dir
396 .join(package_name.replace('-', "_"))
397 .with_extension("wasm")
398 };
399
400 if input_path.exists() {
401 log::trace!("Removing existing target directory");
402 fs::remove_file(&input_path).context("cannot remove existing target")?;
403 }
404
405 log::trace!("Spawning build process");
406 ensure!(
407 build_command
408 .status()
409 .context("could not start cargo")?
410 .success(),
411 "cargo command failed"
412 );
413
414 let app_name = self.app_name.unwrap_or_else(|| "app".to_string());
415
416 log::trace!("Generating Wasm output");
417 let mut output = Bindgen::new()
418 .omit_default_module_path(false)
419 .input_path(input_path)
420 .out_name(&app_name)
421 .web(true)
422 .expect("web have panic")
423 .debug(!self.release)
424 .generate_output()
425 .context("could not generate Wasm bindgen file")?;
426
427 if dist_dir.exists() {
428 log::trace!("Removing already existing dist directory");
429 fs::remove_dir_all(&dist_dir)?;
430 }
431
432 log::trace!("Writing outputs to dist directory");
433 output.emit(&dist_dir)?;
434
435 let assets_dir = if let Some(assets_dir) = self.assets_dir {
436 Some(assets_dir)
437 } else if let Some(package) = metadata.packages.iter().find(|p| p.name == package_name) {
438 let path = package
439 .manifest_path
440 .parent()
441 .context("package manifest has no parent directory")?
442 .join("assets")
443 .as_std_path()
444 .to_path_buf();
445 Some(path)
446 } else {
447 log::debug!(
448 "package `{package_name}` not found in workspace metadata, skipping assets"
449 );
450 None
451 };
452
453 match assets_dir {
454 Some(assets_dir) if assets_dir.exists() => {
455 log::trace!("Copying assets directory into dist directory");
456 copy_assets(&assets_dir, &dist_dir, &self.transformers)?;
457 }
458 Some(assets_dir) => {
459 log::debug!(
460 "assets directory `{}` does not exist, skipping",
461 assets_dir.display()
462 );
463 }
464 None => {}
465 }
466
467 #[cfg(feature = "wasm-opt")]
468 if let Some(wasm_opt) = self.wasm_opt {
469 if self.release {
470 let wasm_path = dist_dir.join(format!("{app_name}_bg.wasm"));
471 wasm_opt.optimize(&wasm_path)?;
472 } else {
473 log::debug!("skipping wasm-opt: not a release build");
474 }
475 }
476
477 log::info!("Successfully built in {}", dist_dir.display());
478
479 Ok(dist_dir)
480 }
481}
482
483impl Default for Dist {
484 fn default() -> Dist {
485 Dist {
486 quiet: Default::default(),
487 jobs: Default::default(),
488 profile: Default::default(),
489 release: Default::default(),
490 features: Default::default(),
491 all_features: Default::default(),
492 no_default_features: Default::default(),
493 verbose: Default::default(),
494 color: Default::default(),
495 frozen: Default::default(),
496 locked: Default::default(),
497 offline: Default::default(),
498 ignore_rust_version: Default::default(),
499 example: Default::default(),
500 build_command: default_build_command(),
501 dist_dir: Default::default(),
502 assets_dir: Default::default(),
503 app_name: Default::default(),
504 transformers: vec![],
505 #[cfg(feature = "wasm-opt")]
506 wasm_opt: None,
507 }
508 }
509}
510
511fn copy_assets(
512 assets_dir: &Path,
513 dist_dir: &Path,
514 transformers: &[Box<dyn Transformer>],
515) -> Result<()> {
516 let walker = walkdir::WalkDir::new(assets_dir);
517 for entry in walker {
518 let entry = entry
519 .with_context(|| format!("cannot walk into directory `{}`", assets_dir.display()))?;
520 let source = entry.path();
521 let dest = dist_dir.join(source.strip_prefix(assets_dir).unwrap());
522
523 if !source.is_file() {
524 continue;
525 }
526
527 if let Some(parent) = dest.parent() {
528 fs::create_dir_all(parent)
529 .with_context(|| format!("cannot create directory `{}`", parent.display()))?;
530 }
531
532 let mut handled = false;
533 for transformer in transformers {
534 if transformer.transform(source, &dest)? {
535 handled = true;
536 break;
537 }
538 }
539
540 if !handled {
541 fs::copy(source, &dest).with_context(|| {
542 format!("cannot copy `{}` to `{}`", source.display(), dest.display())
543 })?;
544 }
545 }
546
547 Ok(())
548}