use std::path::{Path, PathBuf};
use std::process::Command;
use crate::error::{Error, Result};
#[derive(Debug, Clone)]
pub struct CompilerOptions {
pub target: String,
pub opt_level: OptimizationLevel,
pub debug_level: DebugLevel,
pub features: Vec<String>,
pub no_default_features: bool,
pub extra_args: Vec<String>,
pub toolchain: String,
pub profile: BuildProfile,
pub target_cpu: Option<String>,
pub rustflags: Option<String>,
}
impl Default for CompilerOptions {
fn default() -> Self {
Self {
target: "wasm32-wasi".to_string(),
opt_level: OptimizationLevel::Default,
debug_level: DebugLevel::None,
features: Vec::new(),
no_default_features: false,
extra_args: Vec::new(),
toolchain: "stable".to_string(),
profile: BuildProfile::Release,
target_cpu: None,
rustflags: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OptimizationLevel {
None,
Basic,
Default,
Size,
Speed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DebugLevel {
None,
Basic,
Full,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuildProfile {
Debug,
Release,
}
pub trait Compiler {
fn compile(
&self,
project_path: &Path,
output_path: &Path,
options: &CompilerOptions,
) -> Result<PathBuf>;
fn check_available(&self) -> bool;
fn version(&self) -> Result<String>;
}
pub struct CargoCompiler;
impl CargoCompiler {
pub fn new() -> Self {
Self
}
}
impl Compiler for CargoCompiler {
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");
if options.toolchain != "stable" {
cmd.arg(format!("+{}", options.toolchain));
}
cmd.current_dir(project_path)
.arg("build")
.arg("--target").arg(&options.target);
match options.profile {
BuildProfile::Debug => {
},
BuildProfile::Release => {
cmd.arg("--release");
},
}
match options.opt_level {
OptimizationLevel::None => {
cmd.env("RUSTFLAGS", "-C opt-level=0");
},
OptimizationLevel::Basic => {
cmd.env("RUSTFLAGS", "-C opt-level=1");
},
OptimizationLevel::Default => {
},
OptimizationLevel::Size => {
cmd.env("RUSTFLAGS", "-C opt-level=s");
},
OptimizationLevel::Speed => {
cmd.env("RUSTFLAGS", "-C opt-level=3");
},
}
match options.debug_level {
DebugLevel::None => {
if options.profile == BuildProfile::Debug {
cmd.env("RUSTFLAGS", "-C debuginfo=0");
}
},
DebugLevel::Basic => {
cmd.env("RUSTFLAGS", "-C debuginfo=1");
},
DebugLevel::Full => {
cmd.env("RUSTFLAGS", "-C debuginfo=2");
},
}
if !options.features.is_empty() {
cmd.arg("--features").arg(options.features.join(","));
}
if options.no_default_features {
cmd.arg("--no-default-features");
}
for arg in &options.extra_args {
cmd.arg(arg);
}
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 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 package_name = 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() })?;
let wasm_file = format!("{}.wasm", package_name);
let wasm_path = project_path
.join("target")
.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)
})?;
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) })
}
}
}
pub mod cargo;
pub mod wasi;