use std::path::{Path, PathBuf};
use std::process::Command;
use std::collections::HashMap;
use crate::error::{Error, Result};
use super::{Compiler, CompilerOptions};
#[derive(Debug, Clone)]
pub struct WasiConfig {
pub version: String,
pub features: Vec<String>,
pub mapped_dirs: HashMap<PathBuf, String>,
pub env_vars: HashMap<String, String>,
pub args: Vec<String>,
pub preopens: Vec<String>,
}
impl Default for WasiConfig {
fn default() -> Self {
Self {
version: "snapshot1".to_string(),
features: vec![],
mapped_dirs: HashMap::new(),
env_vars: HashMap::new(),
args: vec![],
preopens: vec![],
}
}
}
pub struct WasiCompiler<C: Compiler> {
inner: C,
wasi_config: WasiConfig,
}
impl<C: Compiler> WasiCompiler<C> {
pub fn new(inner: C) -> Self {
Self {
inner,
wasi_config: WasiConfig::default(),
}
}
pub fn with_wasi_config(mut self, config: WasiConfig) -> Self {
self.wasi_config = config;
self
}
pub fn map_directory(mut self, host_path: impl Into<PathBuf>, guest_path: impl Into<String>) -> Self {
self.wasi_config.mapped_dirs.insert(host_path.into(), guest_path.into());
self
}
pub fn with_env_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.wasi_config.env_vars.insert(key.into(), value.into());
self
}
pub fn with_arg(mut self, arg: impl Into<String>) -> Self {
self.wasi_config.args.push(arg.into());
self
}
pub fn with_preopen(mut self, dir: impl Into<String>) -> Self {
self.wasi_config.preopens.push(dir.into());
self
}
fn generate_wasi_wrapper(&self, wasm_path: &Path, _options: &CompilerOptions) -> Result<PathBuf> {
let parent = wasm_path.parent().ok_or_else(|| {
Error::Filesystem {
operation: "get_parent".to_string(),
path: wasm_path.to_path_buf(),
reason: "Invalid WASM path, no parent directory".to_string()
}
})?;
let wrapper_dir = parent.join("wasi_wrapper");
std::fs::create_dir_all(&wrapper_dir)
.map_err(|e| Error::Filesystem {
operation: "create_dir_all".to_string(),
path: wrapper_dir.clone(),
reason: format!("Failed to create wrapper directory: {}", e)
})?;
let wrapper_path = wrapper_dir.join("wrapper.js");
let env_vars = serde_json::to_string(&self.wasi_config.env_vars)
.map_err(|e| Error::Generic { message: format!("Failed to serialize environment variables: {}", e) })?;
let mapped_dirs: HashMap<String, String> = self.wasi_config.mapped_dirs.iter()
.map(|(host, guest)| (guest.clone(), host.to_string_lossy().to_string()))
.collect();
let mapped_dirs = serde_json::to_string(&mapped_dirs)
.map_err(|e| Error::Generic { message: format!("Failed to serialize mapped directories: {}", e) })?;
let args = serde_json::to_string(&self.wasi_config.args)
.map_err(|e| Error::Generic { message: format!("Failed to serialize arguments: {}", e) })?;
let preopens = serde_json::to_string(&self.wasi_config.preopens)
.map_err(|e| Error::Generic { message: format!("Failed to serialize preopens: {}", e) })?;
let wrapper_content = format!(
r#"// WASI wrapper for {wasm_name}
const fs = require('fs');
const path = require('path');
const {{ WASI }} = require('wasi');
// WASI configuration
const wasi = new WASI({{
version: '{wasi_version}',
args: {args},
env: {env_vars},
preopens: {preopens},
mappedDirectories: {mapped_dirs},
}});
// Load the WASM module
async function run() {{
try {{
const wasmPath = path.resolve(__dirname, '../{wasm_name}');
const wasmBuffer = fs.readFileSync(wasmPath);
const wasmModule = await WebAssembly.compile(wasmBuffer);
// Set up imports
const importObject = {{
wasi_snapshot_preview1: wasi.wasiImport,
}};
// Instantiate the module
const instance = await WebAssembly.instantiate(wasmModule, importObject);
// Start the module
wasi.start(instance);
}} catch (error) {{
console.error('Error running WASI module:', error);
process.exit(1);
}}
}}
run();
"#,
wasm_name = wasm_path.file_name().unwrap().to_string_lossy(),
wasi_version = self.wasi_config.version,
args = args,
env_vars = env_vars,
preopens = preopens,
mapped_dirs = mapped_dirs,
);
std::fs::write(&wrapper_path, wrapper_content)
.map_err(|e| Error::Filesystem {
operation: "write".to_string(),
path: wrapper_path.clone(),
reason: format!("Failed to write wrapper file: {}", e)
})?;
Ok(wrapper_path)
}
fn generate_wasi_config_file(&self, wasm_path: &Path) -> Result<PathBuf> {
let parent = wasm_path.parent().ok_or_else(|| {
Error::Filesystem {
operation: "get_parent".to_string(),
path: wasm_path.to_path_buf(),
reason: "Invalid WASM path, no parent directory".to_string()
}
})?;
let config_path = parent.join(format!("{}.wasi.json",
wasm_path.file_stem().unwrap().to_string_lossy()));
let mapped_dirs: HashMap<String, String> = self.wasi_config.mapped_dirs.iter()
.map(|(host, guest)| (guest.clone(), host.to_string_lossy().to_string()))
.collect();
let mut config = std::collections::HashMap::new();
config.insert("version", self.wasi_config.version.clone());
config.insert("features", format!("{:?}", self.wasi_config.features));
config.insert("mappedDirs", format!("{:?}", mapped_dirs));
config.insert("env", format!("{:?}", self.wasi_config.env_vars));
config.insert("args", format!("{:?}", self.wasi_config.args));
config.insert("preopens", format!("{:?}", self.wasi_config.preopens));
let mut config_str = String::from("{\n");
for (key, value) in &config {
config_str.push_str(&format!(" \"{}\": {},\n", key, value));
}
config_str.push_str("}");
std::fs::write(&config_path, config_str)
.map_err(|e| Error::Filesystem {
operation: "write".to_string(),
path: config_path.clone(),
reason: format!("Failed to write WASI config file: {}", e)
})?;
Ok(config_path)
}
}
impl<C: Compiler> Compiler for WasiCompiler<C> {
fn compile(
&self,
project_path: &Path,
output_path: &Path,
options: &CompilerOptions,
) -> Result<PathBuf> {
let wasm_path = self.inner.compile(project_path, output_path, options)?;
let _wrapper_path = self.generate_wasi_wrapper(&wasm_path, options)?;
let _config_path = self.generate_wasi_config_file(&wasm_path)?;
Ok(wasm_path)
}
fn check_available(&self) -> bool {
if !self.inner.check_available() {
return false;
}
let output = Command::new("rustc")
.args(["--print", "target-list"])
.output();
match output {
Ok(output) if output.status.success() => {
let targets = String::from_utf8_lossy(&output.stdout);
targets.contains("wasm32-wasi")
}
_ => false,
}
}
fn version(&self) -> Result<String> {
let inner_version = self.inner.version()?;
Ok(format!("{} with WASI {}", inner_version, self.wasi_config.version))
}
}
pub fn ensure_wasi_target() -> Result<()> {
let output = Command::new("rustc")
.args(["--print", "target-list"])
.output()
.map_err(|e| Error::Compilation { message: format!("Failed to execute rustc: {}", e) })?;
if output.status.success() {
let targets = String::from_utf8_lossy(&output.stdout);
if targets.contains("wasm32-wasi") {
return Ok(());
}
}
let output = Command::new("rustup")
.args(["target", "add", "wasm32-wasi"])
.output()
.map_err(|e| Error::Compilation { message: format!("Failed to execute rustup: {}", e) })?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::Compilation { message: format!(
"Failed to install wasm32-wasi target: {}", stderr
) });
}
Ok(())
}
pub fn check_wasi_compatibility(project_path: &Path) -> Result<bool> {
let cargo_toml_path = project_path.join("Cargo.toml");
if !cargo_toml_path.exists() {
return Err(Error::Filesystem {
operation: "find".to_string(),
path: cargo_toml_path.clone(),
reason: "Cargo.toml not found".to_string()
});
}
std::fs::read_to_string(&cargo_toml_path)
.map_err(|e| Error::Filesystem {
operation: "read".to_string(),
path: cargo_toml_path.clone(),
reason: e.to_string()
})?;
let incompatible_deps = [
"std::fs::File", "std::net::TcpStream", "std::process::Command",
"tokio::fs::File", "tokio::net::TcpStream", "tokio::process::Command",
"async_std::fs::File", "async_std::net::TcpStream", "async_std::process::Command",
];
let src_dir = project_path.join("src");
if !src_dir.exists() || !src_dir.is_dir() {
return Err(Error::Filesystem {
operation: "find".to_string(),
path: src_dir.clone(),
reason: "src directory not found".to_string()
});
}
let mut compatible = true;
let mut check_file = |file_path: &Path| -> std::io::Result<()> {
if !file_path.extension().map_or(false, |ext| ext == "rs") {
return Ok(());
}
let content = std::fs::read_to_string(file_path)?;
for dep in &incompatible_deps {
if content.contains(dep) {
compatible = false;
println!("Warning: Potentially incompatible code in {}: {}",
file_path.display(), dep);
}
}
Ok(())
};
fn visit_dirs(dir: &Path, cb: &mut dyn FnMut(&Path) -> std::io::Result<()>) -> std::io::Result<()> {
if dir.is_dir() {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
visit_dirs(&path, cb)?;
} else {
cb(&path)?;
}
}
}
Ok(())
}
visit_dirs(&src_dir, &mut check_file).map_err(|e| {
Error::Filesystem {
operation: "check".to_string(),
path: src_dir.clone(),
reason: format!("Failed to check source files: {}", e)
}
})?;
Ok(compatible)
}