mod errors;
pub use crate::errors::*;
use cargo_metadata::MetadataCommand;
use owo_colors::OwoColorize;
use std::collections::HashMap;
use std::io::prelude::*;
use std::path::Path;
use std::process::{Command, Stdio};
use toml::map::Map;
use toml::Value;
fn plugin_name(path: impl AsRef<Path>) -> Result<String, BuildError> {
let path = path.as_ref();
let metadata = MetadataCommand::new()
.manifest_path(path.join("Cargo.toml"))
.exec()?;
let root = metadata.root_package().ok_or(BuildError::MissingMetadata)?;
Ok(root.name.clone())
}
pub fn wasm_filename(path: impl AsRef<Path>) -> Result<String, BuildError> {
let plugin_name = plugin_name(path)?;
Ok(format!("{}.wasm", plugin_name.replace('-', "_")))
}
fn adapt_wasm_output(wasm_bytes: Vec<u8>, adapter_bytes: Vec<u8>) -> Result<Vec<u8>, BuildError> {
let component = wit_component::ComponentEncoder::default()
.module(&wasm_bytes)
.map_err(|err| BuildError::Adapter(err.to_string()))?
.validate(true)
.adapter("wasi_snapshot_preview1", &adapter_bytes)
.map_err(|err| BuildError::Adapter(err.to_string()))?
.encode()
.map_err(|err| BuildError::Adapter(err.to_string()))?;
Ok(component.to_vec())
}
fn installed_targets() -> Result<HashMap<String, bool>, BuildError> {
let mut command = Command::new("rustup")
.args(["target", "list"])
.stdout(Stdio::piped())
.spawn()?;
let reader = std::io::BufReader::new(command.stdout.take().unwrap());
let mut targets = HashMap::new();
for line in reader.lines() {
let line = line?;
let installed = line.contains("(installed)");
let target = line.replace("(installed)", "").trim().to_string();
targets.insert(target, installed);
}
let exit_status = command.wait()?;
if !exit_status.success() {
return Err(BuildError::SubprocessError);
}
Ok(targets)
}
fn install_wasm32_wasi_target() -> Result<(), BuildError> {
let mut command = Command::new("rustup")
.args(["target", "add", "wasm32-wasi"])
.spawn()?;
let exit_status = command.wait()?;
if !exit_status.success() {
return Err(BuildError::SubprocessError);
}
Ok(())
}
pub fn validate_plugin(path: impl AsRef<Path>) -> Result<(), BuildError> {
let path = path.as_ref().join("Cargo.toml");
let toml_data = std::fs::read_to_string(path)?;
let root: toml::Value =
toml::from_str(&toml_data).map_err(|e| BuildError::InvalidMetadata(e.to_string()))?;
let default_map = Map::new();
let default_table = Value::Table(Map::new());
let default_array = Vec::new();
let crate_type = root
.as_table()
.unwrap_or(&default_map)
.get("lib")
.unwrap_or(&default_table)
.get("crate-type")
.unwrap_or(&default_table)
.as_array()
.unwrap_or(&default_array);
if !crate_type.contains(&toml::Value::String("cdylib".to_string())) {
return Err(BuildError::MissingCdylib);
}
let release_profile = root
.as_table()
.unwrap_or(&default_map)
.get("profile")
.unwrap_or(&default_table)
.get("release")
.unwrap_or(&default_table);
let lto = release_profile
.get("lto")
.unwrap_or(&default_table)
.as_bool()
.unwrap_or_default();
if !lto {
eprintln!(
"{}: plugin should be compiled with `lto` enabled",
"warning".yellow()
);
}
let opt_level = release_profile
.get("opt-level")
.unwrap_or(&default_table)
.as_integer()
.unwrap_or_default();
if opt_level < 3 {
eprintln!(
"{}: plugin should be compiled with `opt-level` set to 3",
"warning".yellow()
);
}
let codegen_units = release_profile
.get("codegen-units")
.unwrap_or(&default_table)
.as_integer()
.unwrap_or_default();
if codegen_units > 1 {
eprintln!(
"{}: plugin should be compiled with `codegen-units` set to 1",
"warning".yellow()
);
}
let panic_setting = release_profile
.get("panic")
.unwrap_or(&default_table)
.as_str()
.unwrap_or_default();
if panic_setting != "abort" {
eprintln!(
"{}: plugin should be compiled with `panic` set to \"abort\"",
"warning".yellow()
);
}
let strip = release_profile
.get("strip")
.unwrap_or(&default_table)
.as_str()
.unwrap_or_default();
if strip != "debuginfo" {
eprintln!(
"{}: plugin should be compiled with `strip` set to \"debuginfo\"",
"warning".yellow()
);
}
Ok(())
}
pub fn interactive_build_plugin(
path: impl AsRef<Path>,
output: impl AsRef<Path>,
additional_args: &[String],
) -> Result<(), BuildError> {
let mut install_missing = false;
let installed_targets = installed_targets()?;
let wasi_installed = installed_targets.get("wasm32-wasi");
if !wasi_installed.unwrap_or(&false) {
println!("The required wasm32-wasi target is not installed.");
print!("Install it? (y/N) ");
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let input = input.trim().to_ascii_lowercase();
if &input == "y" || &input == "yes" {
install_missing = true;
} else {
return Err(BuildError::MissingTarget);
}
}
build_plugin(path, output, additional_args, install_missing)
}
pub fn build_plugin(
path: impl AsRef<Path>,
output: impl AsRef<Path>,
additional_args: &[String],
install_missing: bool,
) -> Result<(), BuildError> {
validate_plugin(path.as_ref())?;
let adapter_bytes = include_bytes!("../adapter/wasi_snapshot_preview1.reactor.wasm");
let path = path.as_ref();
let output = output.as_ref();
let output_dir = output.parent().ok_or(BuildError::MissingParent)?;
let installed_targets = installed_targets()?;
let wasi_installed = installed_targets.get("wasm32-wasi");
if !wasi_installed.unwrap_or(&false) {
if install_missing {
install_wasm32_wasi_target()?;
} else {
return Err(BuildError::MissingTarget);
}
}
let mut args = vec!["build", "--target=wasm32-wasi", "--release"];
let mut additional_args = additional_args.iter().map(|arg| arg.as_str()).collect();
args.append(&mut additional_args);
let mut command = Command::new("cargo")
.current_dir(path)
.args(&args)
.spawn()?;
let exit_status = command.wait()?;
if exit_status.success() {
let wasm_filename = wasm_filename(path)?;
let wasm_path = path.join("target/wasm32-wasi/release").join(wasm_filename);
let wasm_bytes = std::fs::read(&wasm_path)
.map_err(|err| BuildError::NotFound(wasm_path.to_string_lossy().to_string(), err))?;
let adapted_bytes = adapt_wasm_output(wasm_bytes, adapter_bytes.to_vec())?;
std::fs::create_dir_all(output_dir)?;
std::fs::write(output, adapted_bytes)?;
} else {
return Err(BuildError::SubprocessError);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plugin_name() -> Result<(), Box<dyn std::error::Error>> {
let bulwark_plugin_name = plugin_name(std::env::current_dir()?)?;
assert_eq!(bulwark_plugin_name, "bulwark-build");
Ok(())
}
#[test]
fn test_wasm_filename() -> Result<(), Box<dyn std::error::Error>> {
let wasm_filename = wasm_filename(std::env::current_dir()?)?;
assert_eq!(wasm_filename, "bulwark_build.wasm");
Ok(())
}
}