use std::path::{Path, PathBuf};
use std::process::Command;
use crate::project::Plugin;
pub const TARGET: &str = "wasm32-wasip2";
#[derive(Clone, Debug)]
pub struct Built {
pub component: PathBuf,
pub browser: Result<PathBuf, String>,
}
pub(crate) fn cargo() -> Command {
Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()))
}
fn artifact(dir: &Path, release: bool) -> Result<PathBuf, String> {
let output = cargo()
.args(["metadata", "--format-version", "1", "--no-deps"])
.current_dir(dir)
.output()
.map_err(|error| format!("cargo metadata: {error}"))?;
if !output.status.success() {
return Err(format!(
"cargo metadata failed: {}",
String::from_utf8_lossy(&output.stderr)
));
}
let metadata: serde_json::Value = serde_json::from_slice(&output.stdout)
.map_err(|error| format!("cargo metadata: {error}"))?;
let manifest = dir
.join("Cargo.toml")
.canonicalize()
.map_err(|error| format!("{}: {error}", dir.join("Cargo.toml").display()))?;
let package = metadata["packages"]
.as_array()
.into_iter()
.flatten()
.find(|package| {
package["manifest_path"]
.as_str()
.and_then(|path| Path::new(path).canonicalize().ok())
.is_some_and(|path| path == manifest)
})
.ok_or_else(|| format!("{} is not a package cargo knows", manifest.display()))?;
let name = package["targets"]
.as_array()
.into_iter()
.flatten()
.find(|target| {
target["kind"]
.as_array()
.is_some_and(|kinds| kinds.iter().any(|kind| kind == "cdylib"))
})
.and_then(|target| target["name"].as_str())
.ok_or_else(|| "the crate has no cdylib library (crate-type = [\"cdylib\"])".to_string())?;
let target_dir = metadata["target_directory"]
.as_str()
.ok_or("cargo metadata has no target directory")?;
Ok(Path::new(target_dir)
.join(TARGET)
.join(if release { "release" } else { "debug" })
.join(format!("{}.wasm", name.replace('-', "_"))))
}
fn find_on_path(program: &str) -> Option<PathBuf> {
std::env::var_os("PATH").and_then(|paths| {
std::env::split_paths(&paths)
.map(|dir| dir.join(program))
.find(|path| path.is_file())
})
}
pub fn build(plugin: &Plugin, release: bool) -> Result<Built, String> {
let manifest = plugin.manifest()?;
let mut command = cargo();
command
.args(["build", "--lib", "--target", TARGET])
.current_dir(&plugin.dir);
if release {
command.arg("--release");
}
let status = command
.status()
.map_err(|error| format!("cargo build: {error}"))?;
if !status.success() {
return Err(format!("cargo build failed ({status})"));
}
let built = artifact(&plugin.dir, release)?;
plugin
.stage_manifest()
.map_err(|error| format!("{}: {error}", plugin.bundle.display()))?;
let component = plugin.bundle.join(&manifest.module);
std::fs::copy(&built, &component)
.map_err(|error| format!("{} -> {}: {error}", built.display(), component.display()))?;
let browser = match find_on_path("jco") {
None => {
Err("skipped: `jco` is not installed (npm install -g @bytecodealliance/jco)".into())
}
Some(jco) => {
let out = plugin.bundle.join("browser");
let status = Command::new(jco)
.arg("transpile")
.arg(&component)
.arg("--out-dir")
.arg(&out)
.args(["--name", &manifest.id])
.status()
.map_err(|error| format!("jco: {error}"))?;
if status.success() {
Ok(out)
} else {
Err(format!("jco transpile failed ({status})"))
}
}
};
Ok(Built { component, browser })
}