use std::path::{Path, PathBuf};
use std::process::Command;
use std::collections::HashMap;
use crate::error::{Error, Result};
use super::{Compiler, CompilerOptions, BuildProfile};
pub struct EnhancedCargoCompiler {
env_vars: HashMap<String, String>,
cargo_flags: Vec<String>,
target_dir: Option<PathBuf>,
toolchain_overrides: HashMap<String, String>,
cache_dir: Option<PathBuf>,
}
impl Default for EnhancedCargoCompiler {
fn default() -> Self {
Self {
env_vars: HashMap::new(),
cargo_flags: Vec::new(),
target_dir: None,
toolchain_overrides: HashMap::new(),
cache_dir: None,
}
}
}
impl EnhancedCargoCompiler {
pub fn new() -> Self {
Self::default()
}
pub fn with_env_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env_vars.insert(key.into(), value.into());
self
}
pub fn with_cargo_flag(mut self, flag: impl Into<String>) -> Self {
self.cargo_flags.push(flag.into());
self
}
pub fn with_target_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.target_dir = Some(dir.into());
self
}
pub fn with_toolchain_override(
mut self,
target: impl Into<String>,
toolchain: impl Into<String>,
) -> Self {
self.toolchain_overrides.insert(target.into(), toolchain.into());
self
}
pub fn with_cache_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.cache_dir = Some(dir.into());
self
}
fn generate_rustflags(&self, options: &CompilerOptions) -> String {
let mut flags = Vec::<String>::new();
let opt_flag = match options.opt_level {
super::OptimizationLevel::None => "-C opt-level=0".to_string(),
super::OptimizationLevel::Basic => "-C opt-level=1".to_string(),
super::OptimizationLevel::Default => "-C opt-level=2".to_string(),
super::OptimizationLevel::Size => "-C opt-level=s".to_string(),
super::OptimizationLevel::Speed => "-C opt-level=3".to_string(),
};
flags.push(opt_flag);
let debug_flag = match options.debug_level {
super::DebugLevel::None => "-C debuginfo=0".to_string(),
super::DebugLevel::Basic => "-C debuginfo=1".to_string(),
super::DebugLevel::Full => "-C debuginfo=2".to_string(),
};
flags.push(debug_flag);
if let Some(ref cpu) = options.target_cpu {
flags.push(format!("-C target-cpu={}", cpu));
}
if let Some(ref rustflags) = options.rustflags {
flags.push(rustflags.clone());
}
flags.join(" ")
}
}
impl Compiler for EnhancedCargoCompiler {
fn compile(
&self,
project_path: &Path,
output_path: &Path,
options: &CompilerOptions,
) -> Result<PathBuf> {
if !self.check_available() {
return Err(Error::Compilation { message: "Cargo is not available".to_string() });
}
std::fs::create_dir_all(output_path)
.map_err(|e| Error::Filesystem {
operation: "create_dir_all".to_string(),
path: output_path.to_path_buf(),
reason: format!("Failed to create output directory: {}", e)
})?;
let mut cmd = Command::new("cargo");
let toolchain = self.toolchain_overrides
.get(&options.target)
.unwrap_or(&options.toolchain);
if toolchain != "stable" {
cmd.arg(format!("+{}", toolchain));
}
cmd.current_dir(project_path)
.arg("build")
.arg("--target").arg(&options.target);
match options.profile {
BuildProfile::Debug => {
},
BuildProfile::Release => {
cmd.arg("--release");
},
}
cmd.env("RUSTFLAGS", self.generate_rustflags(options));
if let Some(ref target_dir) = self.target_dir {
cmd.arg("--target-dir").arg(target_dir);
}
if !options.features.is_empty() {
cmd.arg("--features").arg(options.features.join(","));
}
if options.no_default_features {
cmd.arg("--no-default-features");
}
for flag in &self.cargo_flags {
cmd.arg(flag);
}
for arg in &options.extra_args {
cmd.arg(arg);
}
for (key, value) in &self.env_vars {
cmd.env(key, value);
}
let output = cmd.output()
.map_err(|e| Error::Compilation { message: format!("Failed to execute cargo: {}", e) })?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::Compilation { message: format!("Build failed: {}", stderr) });
}
let profile_dir = match options.profile {
BuildProfile::Debug => "debug",
BuildProfile::Release => "release",
};
let package_name = self.get_package_name(project_path)?;
let default_target_dir = project_path.join("target");
let target_dir = self.target_dir
.as_ref()
.unwrap_or(&default_target_dir);
let wasm_file = format!("{}.wasm", package_name);
let wasm_path = target_dir
.join(&options.target)
.join(profile_dir)
.join(wasm_file);
let output_wasm_path = output_path.join(format!("{}.wasm", package_name));
std::fs::copy(&wasm_path, &output_wasm_path)
.map_err(|e| Error::Filesystem {
operation: "copy".to_string(),
path: wasm_path.clone(),
reason: format!("Failed to copy WASM file: {}", e)
})?;
let dts_path = wasm_path.with_extension("d.ts");
if dts_path.exists() {
let output_dts_path = output_wasm_path.with_extension("d.ts");
std::fs::copy(&dts_path, &output_dts_path)
.map_err(|e| Error::Filesystem {
operation: "copy".to_string(),
path: dts_path.clone(),
reason: format!("Failed to copy .d.ts file: {}", e)
})?;
}
Ok(output_wasm_path)
}
fn check_available(&self) -> bool {
Command::new("cargo")
.arg("--version")
.output()
.is_ok()
}
fn version(&self) -> Result<String> {
let output = Command::new("cargo")
.arg("--version")
.output()
.map_err(|e| Error::Compilation { message: format!("Failed to get cargo version: {}", e) })?;
if output.status.success() {
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(version)
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(Error::Compilation { message: format!("Failed to get cargo version: {}", stderr) })
}
}
}
impl EnhancedCargoCompiler {
fn get_package_name(&self, project_path: &Path) -> Result<String> {
let cargo_toml_path = project_path.join("Cargo.toml");
let cargo_toml = std::fs::read_to_string(&cargo_toml_path)
.map_err(|e| Error::Filesystem {
operation: "read".to_string(),
path: cargo_toml_path.clone(),
reason: format!("Failed to read Cargo.toml: {}", e)
})?;
cargo_toml
.lines()
.find_map(|line| {
if line.trim().starts_with("name") {
line.split('=')
.nth(1)
.map(|s| s.trim().trim_matches('"').to_string())
} else {
None
}
})
.ok_or_else(|| Error::Compilation { message: "Failed to determine package name".to_string() })
}
}
pub struct CachingCargoCompiler {
inner: EnhancedCargoCompiler,
cache_dir: PathBuf,
}
impl CachingCargoCompiler {
pub fn new(cache_dir: impl Into<PathBuf>) -> Self {
let cache_dir = cache_dir.into();
std::fs::create_dir_all(&cache_dir).ok();
Self {
inner: EnhancedCargoCompiler::default().with_cache_dir(&cache_dir),
cache_dir,
}
}
fn cache_key(&self, project_path: &Path, options: &CompilerOptions) -> Result<String> {
let cargo_toml_path = project_path.join("Cargo.toml");
let cargo_toml = std::fs::read_to_string(&cargo_toml_path)
.map_err(|e| Error::Filesystem {
operation: "read".to_string(),
path: cargo_toml_path.clone(),
reason: format!("Failed to read Cargo.toml: {}", e)
})?;
let cargo_lock = std::fs::read_to_string(project_path.join("Cargo.lock"))
.unwrap_or_default();
let inputs = format!(
"{}:{}:{}:{}:{}:{}",
cargo_toml,
cargo_lock,
options.target,
format!("{:?}", options.opt_level),
format!("{:?}", options.profile),
options.features.join(",")
);
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
inputs.hash(&mut hasher);
let hash = hasher.finish();
Ok(format!("{:016x}", hash))
}
}
impl Compiler for CachingCargoCompiler {
fn compile(
&self,
project_path: &Path,
output_path: &Path,
options: &CompilerOptions,
) -> Result<PathBuf> {
let key = self.cache_key(project_path, options)?;
let package_name = self.inner.get_package_name(project_path)?;
let cached_path = self.cache_dir.join(format!("{}-{}.wasm", package_name, key));
if cached_path.exists() {
let output_wasm_path = output_path.join(format!("{}.wasm", package_name));
std::fs::copy(&cached_path, &output_wasm_path)
.map_err(|e| Error::Filesystem {
operation: "copy".to_string(),
path: cached_path.clone(),
reason: format!("Failed to copy cached WASM file: {}", e)
})?;
return Ok(output_wasm_path);
}
let result = self.inner.compile(project_path, output_path, options)?;
std::fs::copy(&result, &cached_path)
.map_err(|e| Error::Filesystem {
operation: "copy".to_string(),
path: result.clone(),
reason: format!("Failed to cache WASM file: {}", e)
})?;
Ok(result)
}
fn check_available(&self) -> bool {
self.inner.check_available()
}
fn version(&self) -> Result<String> {
self.inner.version()
}
}