use std::iter::Iterator;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::{ffi::OsStr, str::FromStr};
use anyhow::{anyhow, bail, Context, Result};
use async_process::{Command, Stdio};
use async_std::fs;
use async_std::task::{spawn, JoinHandle};
use futures::channel::mpsc::Sender;
use nipper::Document;
use super::{LinkAttrs, TrunkLinkPipelineOutput};
use super::{ATTR_HREF, SNIPPETS_DIR};
use crate::common::{copy_dir_recursive, path_exists};
use crate::config::{CargoMetadata, RtcBuild};
pub struct RustApp {
id: Option<usize>,
cfg: Arc<RtcBuild>,
cargo_features: Option<String>,
manifest: CargoMetadata,
ignore_chan: Option<Sender<PathBuf>>,
bin: Option<String>,
keep_debug: bool,
no_demangle: bool,
wasm_opt: WasmOptLevel,
}
impl RustApp {
pub const TYPE_RUST_APP: &'static str = "rust";
pub async fn new(cfg: Arc<RtcBuild>, html_dir: Arc<PathBuf>, ignore_chan: Option<Sender<PathBuf>>, attrs: LinkAttrs, id: usize) -> Result<Self> {
let manifest_href = attrs
.get(ATTR_HREF)
.map(|attr| {
let mut path = PathBuf::new();
path.extend(attr.split('/'));
if !path.is_absolute() {
path = html_dir.join(path);
}
if !path.ends_with("Cargo.toml") {
path = path.join("Cargo.toml");
}
path
})
.unwrap_or_else(|| html_dir.join("Cargo.toml"));
let bin = attrs.get("data-bin").map(|val| val.to_string());
let cargo_features = attrs.get("data-cargo-features").map(|val| val.to_string());
let keep_debug = attrs.contains_key("data-keep-debug");
let no_demangle = attrs.contains_key("data-no-demangle");
let wasm_opt = attrs.get("data-wasm-opt").map(|val| val.parse()).transpose()?.unwrap_or_default();
let manifest = CargoMetadata::new(&manifest_href).await?;
let id = Some(id);
Ok(Self {
id,
cfg,
cargo_features,
manifest,
ignore_chan,
bin,
keep_debug,
no_demangle,
wasm_opt,
})
}
pub async fn new_default(cfg: Arc<RtcBuild>, html_dir: Arc<PathBuf>, ignore_chan: Option<Sender<PathBuf>>) -> Result<Self> {
let path = html_dir.join("Cargo.toml");
let manifest = CargoMetadata::new(&path).await?;
Ok(Self {
id: None,
cfg,
cargo_features: None,
manifest,
ignore_chan,
bin: None,
keep_debug: false,
no_demangle: false,
wasm_opt: WasmOptLevel::default(),
})
}
#[tracing::instrument(level = "trace", skip(self))]
pub fn spawn(self) -> JoinHandle<Result<TrunkLinkPipelineOutput>> {
spawn(self.build())
}
#[tracing::instrument(level = "trace", skip(self))]
async fn build(mut self) -> Result<TrunkLinkPipelineOutput> {
let (wasm, hashed_name) = self.cargo_build().await?;
let output = self.wasm_bindgen_build(wasm.as_ref(), &hashed_name).await?;
self.wasm_opt_build(&output.wasm_output).await?;
Ok(TrunkLinkPipelineOutput::RustApp(output))
}
#[tracing::instrument(level = "trace", skip(self))]
async fn cargo_build(&mut self) -> Result<(PathBuf, String)> {
tracing::info!("building {}", &self.manifest.package.name);
let mut args = vec![
"build",
"--target=wasm32-unknown-unknown",
"--manifest-path",
&self.manifest.manifest_path,
];
if self.cfg.release {
args.push("--release");
}
if let Some(bin) = &self.bin {
args.push("--bin");
args.push(bin);
}
if let Some(cargo_features) = &self.cargo_features {
args.push("--features");
args.push(cargo_features);
}
let build_res = run_command("cargo", &args).await.context("error during cargo build execution");
if let Some(chan) = &mut self.ignore_chan {
let _ = chan.try_send(self.manifest.metadata.target_directory.clone());
}
let _ = build_res?;
tracing::info!("fetching cargo artifacts");
args.push("--message-format=json");
let artifacts_out = Command::new("cargo")
.args(args.as_slice())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("error spawning cargo build artifacts task")?
.output()
.await
.context("error getting cargo build artifacts info")?;
if !artifacts_out.status.success() {
eprintln!("{}", String::from_utf8_lossy(&artifacts_out.stderr));
bail!("bad status returned from cargo artifacts request");
}
let reader = std::io::BufReader::new(artifacts_out.stdout.as_slice());
let artifact = cargo_metadata::Message::parse_stream(reader)
.filter_map(|msg| if let Ok(msg) = msg { Some(msg) } else { None })
.fold(Ok(None), |acc, msg| match msg {
cargo_metadata::Message::CompilerArtifact(art) if art.package_id == self.manifest.package.id => Ok(Some(art)),
cargo_metadata::Message::BuildFinished(finished) if !finished.success => Err(anyhow!("error while fetching cargo artifact info")),
_ => acc,
})?
.context("cargo artifacts not found for target crate")?;
let wasm = artifact
.filenames
.into_iter()
.find(|path| path.extension().map(|ext| ext == "wasm").unwrap_or(false))
.context("could not find WASM output after cargo build")?;
tracing::info!("processing WASM");
let wasm_bytes = fs::read(&wasm).await.context("error reading wasm file for hash generation")?;
let hashed_name = format!("index-{:x}", seahash::hash(&wasm_bytes));
Ok((wasm, hashed_name))
}
#[tracing::instrument(level = "trace", skip(self, wasm, hashed_name))]
async fn wasm_bindgen_build(&self, wasm: &Path, hashed_name: &str) -> Result<RustAppOutput> {
tracing::info!("calling wasm-bindgen");
let mode_segment = if self.cfg.release { "release" } else { "debug" };
let bindgen_out = self.manifest.metadata.target_directory.join("wasm-bindgen").join(mode_segment);
fs::create_dir_all(bindgen_out.as_path())
.await
.context("error creating wasm-bindgen output dir")?;
let arg_out_path = format!("--out-dir={}", bindgen_out.display());
let arg_out_name = format!("--out-name={}", &hashed_name);
let target_wasm = wasm.to_string_lossy().to_string();
let mut args = vec!["--target=web", &arg_out_path, &arg_out_name, "--no-typescript", &target_wasm];
if self.keep_debug {
args.push("--keep-debug");
}
if self.no_demangle {
args.push("--no-demangle");
}
run_command("wasm-bindgen", &args).await?;
tracing::info!("copying generated wasm-bindgen artifacts");
let hashed_js_name = format!("{}.js", &hashed_name);
let hashed_wasm_name = format!("{}_bg.wasm", &hashed_name);
let js_loader_path = bindgen_out.join(&hashed_js_name);
let js_loader_path_dist = self.cfg.staging_dist.join(&hashed_js_name);
let wasm_path = bindgen_out.join(&hashed_wasm_name);
let wasm_path_dist = self.cfg.staging_dist.join(&hashed_wasm_name);
fs::copy(js_loader_path, js_loader_path_dist)
.await
.context("error copying JS loader file to stage dir")?;
fs::copy(wasm_path, wasm_path_dist)
.await
.context("error copying wasm file to stage dir")?;
let snippets_dir = bindgen_out.join(SNIPPETS_DIR);
if path_exists(&snippets_dir).await? {
copy_dir_recursive(bindgen_out.join(SNIPPETS_DIR), self.cfg.staging_dist.join(SNIPPETS_DIR))
.await
.context("error copying snippets dir to stage dir")?;
}
Ok(RustAppOutput {
id: self.id,
cfg: self.cfg.clone(),
js_output: hashed_js_name,
wasm_output: hashed_wasm_name,
})
}
#[tracing::instrument(level = "trace", skip(self, hashed_name))]
async fn wasm_opt_build(&self, hashed_name: &str) -> Result<()> {
if self.wasm_opt == WasmOptLevel::Off {
return Ok(());
}
tracing::info!("calling wasm-opt");
let mode_segment = if self.cfg.release { "release" } else { "debug" };
let output = self.manifest.metadata.target_directory.join("wasm-opt").join(mode_segment);
fs::create_dir_all(&output).await.context("error creating wasm-opt output dir")?;
let output = output.join(hashed_name);
let arg_output = format!("--output={}", output.display());
let arg_opt_level = format!("-O{}", self.wasm_opt.as_ref());
let target_wasm = self.cfg.staging_dist.join(hashed_name).to_string_lossy().to_string();
let args = vec![&arg_output, &arg_opt_level, &target_wasm];
run_command("wasm-opt", &args).await?;
tracing::info!("copying generated wasm-opt artifacts");
fs::copy(output, self.cfg.staging_dist.join(hashed_name))
.await
.context("error copying wasm file to dist dir")?;
Ok(())
}
}
pub struct RustAppOutput {
pub cfg: Arc<RtcBuild>,
pub id: Option<usize>,
pub js_output: String,
pub wasm_output: String,
}
impl RustAppOutput {
pub async fn finalize(self, dom: &mut Document) -> Result<()> {
let (base, js, wasm, head, body) = (&self.cfg.public_url, &self.js_output, &self.wasm_output, "html head", "html body");
let preload = format!(
r#"
<link rel="preload" href="{base}{wasm}" as="fetch" type="application/wasm" crossorigin>
<link rel="modulepreload" href="{base}{js}">"#,
base = base,
js = js,
wasm = wasm,
);
dom.select(head).append_html(preload);
let script = format!(
r#"<script type="module">import init from '{base}{js}';init('{base}{wasm}');</script>"#,
base = base,
js = js,
wasm = wasm,
);
match self.id {
Some(id) => dom.select(&super::trunk_id_selector(id)).replace_with_html(script),
None => dom.select(body).append_html(script),
}
Ok(())
}
}
#[tracing::instrument(level = "trace", skip(name, args))]
async fn run_command(name: &str, args: &[impl AsRef<OsStr>]) -> Result<()> {
let status = Command::new(name)
.args(args)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.with_context(|| format!("error spawning {} call", name))?
.status()
.await
.with_context(|| format!("error during {} call", name))?;
if !status.success() {
bail!("{} call returned a bad status", name);
}
Ok(())
}
#[derive(PartialEq, Eq)]
enum WasmOptLevel {
Default,
Off,
One,
Two,
Three,
Four,
S,
Z,
}
impl FromStr for WasmOptLevel {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"" => Self::Default,
"0" => Self::Off,
"1" => Self::One,
"2" => Self::Two,
"3" => Self::Three,
"4" => Self::Four,
"s" | "S" => Self::S,
"z" | "Z" => Self::Z,
_ => bail!("unknown wasm-opt level `{}`", s),
})
}
}
impl AsRef<str> for WasmOptLevel {
fn as_ref(&self) -> &str {
match self {
Self::Default => "",
Self::Off => "0",
Self::One => "1",
Self::Two => "2",
Self::Three => "3",
Self::Four => "4",
Self::S => "s",
Self::Z => "z",
}
}
}
impl Default for WasmOptLevel {
fn default() -> Self {
Self::Off
}
}