use crate::{
anyhow::{ensure, Context, Result},
camino, clap, default_build_command, metadata,
};
use derive_more::Debug;
use std::{fs, path::PathBuf, process};
use wasm_bindgen_cli_support::Bindgen;
pub trait Transformer {
fn transform(&self, source: &Path, dest: &Path) -> Result<bool>;
}
use std::path::Path;
impl Transformer for () {
fn transform(&self, _source: &Path, _dest: &Path) -> Result<bool> {
Ok(false)
}
}
#[non_exhaustive]
#[derive(Debug, clap::Parser)]
#[clap(
about = "Generate the distributed package.",
long_about = "Generate the distributed package.\n\
It will build and package the project for WASM."
)]
pub struct Dist {
#[clap(short, long)]
pub quiet: bool,
#[clap(short, long)]
pub jobs: Option<String>,
#[clap(long)]
pub profile: Option<String>,
#[clap(long)]
pub release: bool,
#[clap(long)]
pub features: Vec<String>,
#[clap(long)]
pub all_features: bool,
#[clap(long)]
pub no_default_features: bool,
#[clap(short, long)]
pub verbose: bool,
#[clap(long)]
pub color: Option<String>,
#[clap(long)]
pub frozen: bool,
#[clap(long)]
pub locked: bool,
#[clap(long)]
pub offline: bool,
#[clap(long)]
pub ignore_rust_version: bool,
#[clap(long)]
pub example: Option<String>,
#[clap(skip = default_build_command())]
pub build_command: process::Command,
#[clap(skip)]
pub dist_dir: Option<PathBuf>,
#[clap(skip)]
pub assets_dir: Option<PathBuf>,
#[clap(skip)]
pub app_name: Option<String>,
#[clap(skip)]
#[debug(skip)]
pub transformers: Vec<Box<dyn Transformer>>,
#[cfg(feature = "wasm-opt")]
#[clap(skip)]
pub wasm_opt: Option<crate::WasmOpt>,
}
impl Dist {
pub fn build_command(mut self, command: process::Command) -> Self {
self.build_command = command;
self
}
pub fn dist_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.dist_dir = Some(path.into());
self
}
pub fn assets_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.assets_dir = Some(path.into());
self
}
pub fn app_name(mut self, app_name: impl Into<String>) -> Self {
self.app_name = Some(app_name.into());
self
}
pub fn transformer(mut self, transformer: impl Transformer + 'static) -> Self {
self.transformers.push(Box::new(transformer));
self
}
#[cfg(feature = "wasm-opt")]
#[cfg_attr(docsrs, doc(cfg(feature = "wasm-opt")))]
pub fn optimize_wasm(mut self, wasm_opt: crate::WasmOpt) -> Self {
self.wasm_opt = Some(wasm_opt);
self
}
pub fn example(mut self, example: impl Into<String>) -> Self {
self.example = Some(example.into());
self
}
pub fn default_debug_dir() -> camino::Utf8PathBuf {
metadata().target_directory.join("debug").join("dist")
}
pub fn default_release_dir() -> camino::Utf8PathBuf {
metadata().target_directory.join("release").join("dist")
}
#[cfg_attr(
feature = "wasm-opt",
doc = "Wasm optimizations can be achieved using [`WasmOpt`](crate::WasmOpt) if the feature `wasm-opt` is enabled."
)]
#[cfg_attr(
not(feature = "wasm-opt"),
doc = "Wasm optimizations can be achieved using `WasmOpt` if the feature `wasm-opt` is enabled."
)]
pub fn build(self, package_name: &str) -> Result<PathBuf> {
log::trace!("Getting package's metadata");
let metadata = metadata();
let dist_dir = self.dist_dir.unwrap_or_else(|| {
if self.release {
Self::default_release_dir().into()
} else {
Self::default_debug_dir().into()
}
});
log::trace!("Initializing dist process");
let mut build_command = self.build_command;
build_command.current_dir(&metadata.workspace_root);
if self.quiet {
build_command.arg("--quiet");
}
if let Some(number) = self.jobs {
build_command.args(["--jobs", &number]);
}
if let Some(profile) = self.profile {
build_command.args(["--profile", &profile]);
}
if self.release {
build_command.arg("--release");
}
for feature in &self.features {
build_command.args(["--features", feature]);
}
if self.all_features {
build_command.arg("--all-features");
}
if self.no_default_features {
build_command.arg("--no-default-features");
}
if self.verbose {
build_command.arg("--verbose");
}
if let Some(color) = self.color {
build_command.args(["--color", &color]);
}
if self.frozen {
build_command.arg("--frozen");
}
if self.locked {
build_command.arg("--locked");
}
if self.offline {
build_command.arg("--offline");
}
if self.ignore_rust_version {
build_command.arg("--ignore-rust-version");
}
build_command.args(["--package", package_name]);
if let Some(example) = &self.example {
build_command.args(["--example", example]);
}
let build_dir = metadata
.target_directory
.join("wasm32-unknown-unknown")
.join(if self.release { "release" } else { "debug" });
let input_path = if let Some(example) = &self.example {
build_dir
.join("examples")
.join(example.replace('-', "_"))
.with_extension("wasm")
} else {
build_dir
.join(package_name.replace('-', "_"))
.with_extension("wasm")
};
if input_path.exists() {
log::trace!("Removing existing target directory");
fs::remove_file(&input_path).context("cannot remove existing target")?;
}
log::trace!("Spawning build process");
ensure!(
build_command
.status()
.context("could not start cargo")?
.success(),
"cargo command failed"
);
let app_name = self.app_name.unwrap_or_else(|| "app".to_string());
log::trace!("Generating Wasm output");
let mut output = Bindgen::new()
.omit_default_module_path(false)
.input_path(input_path)
.out_name(&app_name)
.web(true)
.expect("web have panic")
.debug(!self.release)
.generate_output()
.context("could not generate Wasm bindgen file")?;
if dist_dir.exists() {
log::trace!("Removing already existing dist directory");
fs::remove_dir_all(&dist_dir)?;
}
log::trace!("Writing outputs to dist directory");
output.emit(&dist_dir)?;
let assets_dir = if let Some(assets_dir) = self.assets_dir {
Some(assets_dir)
} else if let Some(package) = metadata.packages.iter().find(|p| p.name == package_name) {
let path = package
.manifest_path
.parent()
.context("package manifest has no parent directory")?
.join("assets")
.as_std_path()
.to_path_buf();
Some(path)
} else {
log::debug!(
"package `{package_name}` not found in workspace metadata, skipping assets"
);
None
};
match assets_dir {
Some(assets_dir) if assets_dir.exists() => {
log::trace!("Copying assets directory into dist directory");
copy_assets(&assets_dir, &dist_dir, &self.transformers)?;
}
Some(assets_dir) => {
log::debug!(
"assets directory `{}` does not exist, skipping",
assets_dir.display()
);
}
None => {}
}
#[cfg(feature = "wasm-opt")]
if let Some(wasm_opt) = self.wasm_opt {
if self.release {
let wasm_path = dist_dir.join(format!("{app_name}_bg.wasm"));
wasm_opt.optimize(&wasm_path)?;
} else {
log::debug!("skipping wasm-opt: not a release build");
}
}
log::info!("Successfully built in {}", dist_dir.display());
Ok(dist_dir)
}
}
impl Default for Dist {
fn default() -> Dist {
Dist {
quiet: Default::default(),
jobs: Default::default(),
profile: Default::default(),
release: Default::default(),
features: Default::default(),
all_features: Default::default(),
no_default_features: Default::default(),
verbose: Default::default(),
color: Default::default(),
frozen: Default::default(),
locked: Default::default(),
offline: Default::default(),
ignore_rust_version: Default::default(),
example: Default::default(),
build_command: default_build_command(),
dist_dir: Default::default(),
assets_dir: Default::default(),
app_name: Default::default(),
transformers: vec![],
#[cfg(feature = "wasm-opt")]
wasm_opt: None,
}
}
}
fn copy_assets(
assets_dir: &Path,
dist_dir: &Path,
transformers: &[Box<dyn Transformer>],
) -> Result<()> {
let walker = walkdir::WalkDir::new(assets_dir);
for entry in walker {
let entry = entry
.with_context(|| format!("cannot walk into directory `{}`", assets_dir.display()))?;
let source = entry.path();
let dest = dist_dir.join(source.strip_prefix(assets_dir).unwrap());
if !source.is_file() {
continue;
}
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("cannot create directory `{}`", parent.display()))?;
}
let mut handled = false;
for transformer in transformers {
if transformer.transform(source, &dest)? {
handled = true;
break;
}
}
if !handled {
fs::copy(source, &dest).with_context(|| {
format!("cannot copy `{}` to `{}`", source.display(), dest.display())
})?;
}
}
Ok(())
}