use anyhow::{Context, Result};
use std::{
env, fs,
path::{Path, PathBuf},
};
use toml::value::Table;
use crate::{
crate_info::CrateInfo,
optimize::{OptType, Optimizer},
};
pub struct WasmProject {
original_dir: PathBuf,
out_dir: PathBuf,
target_dir: PathBuf,
wasm_target_dir: PathBuf,
file_base_name: Option<String>,
profile: String,
}
impl WasmProject {
pub fn new() -> Self {
let original_dir: PathBuf = env::var("CARGO_MANIFEST_DIR")
.expect("`CARGO_MANIFEST_DIR` is always set in build scripts")
.into();
let out_dir: PathBuf = env::var("OUT_DIR")
.expect("`OUT_DIR` is always set in build scripts")
.into();
let profile = out_dir
.components()
.rev()
.take_while(|c| c.as_os_str() != "target")
.collect::<Vec<_>>()
.into_iter()
.rev()
.take_while(|c| c.as_os_str() != "build")
.last()
.expect("Path should have subdirs in the `target` dir")
.as_os_str()
.to_string_lossy()
.into();
let mut target_dir = out_dir.clone();
while target_dir.pop() {
if target_dir.ends_with("target") {
break;
}
}
let mut wasm_target_dir = target_dir.clone();
wasm_target_dir.push("wasm32-unknown-unknown");
wasm_target_dir.push(&profile);
target_dir.push("wasm-projects");
target_dir.push(&profile);
WasmProject {
original_dir,
out_dir,
target_dir,
wasm_target_dir,
file_base_name: None,
profile,
}
}
pub fn manifest_path(&self) -> PathBuf {
self.out_dir.join("Cargo.toml")
}
pub fn target_dir(&self) -> PathBuf {
self.target_dir.clone()
}
pub fn profile(&self) -> &str {
&self.profile
}
pub fn generate(&mut self) -> Result<()> {
let original_manifest = self.original_dir.join("Cargo.toml");
let crate_info = CrateInfo::from_manifest(&original_manifest)?;
self.file_base_name = Some(crate_info.snake_case_name.clone());
let mut package = Table::new();
package.insert("name".into(), format!("{}-wasm", &crate_info.name).into());
package.insert("version".into(), crate_info.version.into());
package.insert("edition".into(), "2021".into());
let mut lib = Table::new();
lib.insert("name".into(), crate_info.snake_case_name.into());
lib.insert("crate-type".into(), vec!["cdylib".to_string()].into());
let mut release_profile = Table::new();
release_profile.insert("lto".into(), true.into());
release_profile.insert("opt-level".into(), "s".into());
let mut profile = Table::new();
profile.insert("dev".into(), release_profile.clone().into());
profile.insert("release".into(), release_profile.into());
let mut crate_package = Table::new();
crate_package.insert("package".into(), crate_info.name.into());
crate_package.insert(
"path".into(),
self.original_dir.display().to_string().into(),
);
crate_package.insert("default-features".into(), false.into());
let mut dependencies = Table::new();
dependencies.insert("orig-project".into(), crate_package.into());
let mut features = Table::new();
for feature in crate_info.features.keys() {
if feature != "default" {
features.insert(
feature.into(),
vec![format!("orig-project/{feature}")].into(),
);
}
}
let mut cargo_toml = Table::new();
cargo_toml.insert("package".into(), package.into());
cargo_toml.insert("lib".into(), lib.into());
cargo_toml.insert("dependencies".into(), dependencies.into());
cargo_toml.insert("profile".into(), profile.into());
cargo_toml.insert("features".into(), features.into());
cargo_toml.insert("workspace".into(), Table::new().into());
fs::write(self.manifest_path(), toml::to_string_pretty(&cargo_toml)?)?;
let src_dir = self.out_dir.join("src");
fs::create_dir_all(&src_dir)?;
fs::write(
src_dir.join("lib.rs"),
"#![no_std] pub use orig_project::*;",
)?;
let from_lock = self.original_dir.join("Cargo.lock");
let to_lock = self.out_dir.join("Cargo.lock");
let _ = fs::copy(from_lock, to_lock);
Ok(())
}
pub fn postprocess(&self) -> Result<()> {
let file_base_name = self
.file_base_name
.as_ref()
.expect("Run `WasmProject::create_project()` first");
let from_path = self
.target_dir
.join(format!("wasm32-unknown-unknown/{}", self.profile))
.join(format!("{}.wasm", &file_base_name));
fs::create_dir_all(&self.target_dir)?;
fs::create_dir_all(&self.wasm_target_dir)?;
let [to_path, to_opt_path, to_meta_path] = [".wasm", ".opt.wasm", ".meta.wasm"]
.map(|ext| self.wasm_target_dir.join([file_base_name, ext].concat()));
fs::copy(&from_path, &to_path).context("unable to copy WASM file")?;
let _ = crate::optimize::optimize_wasm(to_path.clone(), "s", false);
Self::generate_wasm(from_path, &to_opt_path, &to_meta_path)?;
let wasm_binary_path = self.original_dir.join(".binpath");
let mut relative_path = pathdiff::diff_paths(&to_path, &self.original_dir)
.expect("Unable to calculate relative path");
relative_path.set_extension("");
fs::write(wasm_binary_path, format!("{}", relative_path.display()))
.context("unable to write `.binpath`")?;
let wasm_binary_rs = self.out_dir.join("wasm_binary.rs");
fs::write(
wasm_binary_rs,
format!(
r#"#[allow(unused)]
pub const WASM_BINARY: &[u8] = include_bytes!("{}");
#[allow(unused)]
pub const WASM_BINARY_OPT: &[u8] = include_bytes!("{}");
#[allow(unused)]
pub const WASM_BINARY_META: &[u8] = include_bytes!("{}");
"#,
display_path(to_path),
display_path(to_opt_path),
display_path(to_meta_path),
),
)
.context("unable to write `wasm_binary.rs`")?;
Ok(())
}
fn generate_wasm(from: PathBuf, to_opt: &Path, to_meta: &Path) -> Result<()> {
let mut optimizer = Optimizer::new(from)?;
optimizer.insert_stack_and_export();
optimizer.strip_custom_sections();
let opt = optimizer.optimize(OptType::Opt)?;
fs::write(to_opt, opt)?;
let meta = optimizer.optimize(OptType::Meta)?;
fs::write(to_meta, meta)?;
Ok(())
}
}
fn display_path(path: PathBuf) -> String {
path.display().to_string().replace('\\', "/")
}