use crate::compiler::builder::{BuildConfig, BuildResult, OptimizationLevel, WasmBuilder};
use crate::error::{CompilationError, CompilationResult};
use crate::plugin::{Plugin, PluginCapabilities, PluginInfo, PluginType};
use crate::utils::{CommandExecutor, PathResolver};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Clone)]
pub struct CPlugin {
info: PluginInfo,
}
impl CPlugin {
pub fn new() -> Self {
let info = PluginInfo {
name: "c".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
description: "C WebAssembly compiler using Emscripten".to_string(),
author: "Wasmrun Team".to_string(),
extensions: vec!["c".to_string(), "h".to_string()],
entry_files: vec!["main.c".to_string(), "Makefile".to_string()],
plugin_type: PluginType::Builtin,
source: None,
dependencies: vec![],
capabilities: PluginCapabilities {
compile_wasm: true,
compile_webapp: true,
live_reload: true,
optimization: true,
custom_targets: vec!["wasm".to_string(), "web".to_string()],
supported_languages: Some(vec!["c".to_string(), "cpp".to_string()]),
},
};
Self { info }
}
fn find_entry_file(&self, project_path: &str) -> CompilationResult<PathBuf> {
let common_entry_files = ["main.c", "src/main.c", "app.c", "index.c"];
for entry_name in common_entry_files.iter() {
let entry_path = Path::new(project_path).join(entry_name);
if entry_path.exists() {
return Ok(entry_path);
}
}
if let Ok(entries) = fs::read_dir(project_path) {
for entry in entries.flatten() {
if let Some(extension) = entry.path().extension() {
if extension == "c" {
return Ok(entry.path());
}
}
}
}
Err(CompilationError::MissingEntryFile {
language: self.language_name().to_string(),
candidates: vec![
"main.c".to_string(),
"src/main.c".to_string(),
"app.c".to_string(),
"index.c".to_string(),
],
})
}
fn has_makefile(&self, project_path: &str) -> bool {
let makefile_variants = ["Makefile", "makefile", "GNUmakefile"];
for variant in makefile_variants {
let makefile_path = PathResolver::join_paths(project_path, variant);
if Path::new(&makefile_path).exists() {
return true;
}
}
false
}
fn build_with_makefile(&self, config: &BuildConfig) -> CompilationResult<BuildResult> {
if !CommandExecutor::is_tool_installed("make") {
return Err(CompilationError::BuildToolNotFound {
tool: "make".to_string(),
language: self.language_name().to_string(),
});
}
let build_output = CommandExecutor::execute_command(
"make",
&["wasm"],
&config.project_path,
config.verbose,
)?;
if !build_output.status.success() {
let build_output = CommandExecutor::execute_command(
"make",
&[],
&config.project_path,
config.verbose,
)?;
if !build_output.status.success() {
return Err(CompilationError::BuildFailed {
language: self.language_name().to_string(),
reason: format!(
"Make build failed: {}",
String::from_utf8_lossy(&build_output.stderr)
),
});
}
}
let wasm_files = PathResolver::find_files_with_extension(&config.project_path, "wasm")
.map_err(|e| CompilationError::BuildFailed {
language: self.language_name().to_string(),
reason: format!("Failed to find WASM files after make build: {e}"),
})?;
if wasm_files.is_empty() {
return Err(CompilationError::BuildFailed {
language: self.language_name().to_string(),
reason: "No WASM file found after make build".to_string(),
});
}
let output_path = CommandExecutor::copy_to_output(&wasm_files[0], &config.output_dir, "C")?;
let js_files =
PathResolver::find_files_with_extension(&config.project_path, "js").unwrap_or_default();
let js_output_path = if !js_files.is_empty() {
Some(CommandExecutor::copy_to_output(
&js_files[0],
&config.output_dir,
"C",
)?)
} else {
None
};
let has_js_bindings = js_output_path.is_some();
Ok(BuildResult {
wasm_path: output_path,
js_path: js_output_path,
additional_files: vec![],
is_wasm_bindgen: has_js_bindings,
})
}
fn build_with_emscripten(&self, config: &BuildConfig) -> CompilationResult<BuildResult> {
let entry_path = self.find_entry_file(&config.project_path)?;
PathResolver::ensure_output_directory(&config.output_dir).map_err(|_| {
CompilationError::OutputDirectoryCreationFailed {
path: config.output_dir.clone(),
}
})?;
let output_name = entry_path
.file_stem()
.unwrap()
.to_string_lossy()
.to_string();
let wasm_output_file = Path::new(&config.output_dir).join(format!("{output_name}.wasm"));
let js_output_file = Path::new(&config.output_dir).join(format!("{output_name}.js"));
println!("🔨 Building with Emscripten...");
let c_files = self.collect_c_files(&config.project_path)?;
let mut args = vec![
"-o",
js_output_file.to_str().unwrap(),
"-s",
"WASM=1",
"-s",
"EXPORTED_RUNTIME_METHODS=['cwrap']",
];
match config.optimization_level {
OptimizationLevel::Debug => {
args.extend(&["-g", "-O0"]);
}
OptimizationLevel::Release => {
args.extend(&["-O3"]);
}
OptimizationLevel::Size => {
args.extend(&["-Os", "-s", "ELIMINATE_DUPLICATE_FUNCTIONS=1"]);
}
}
for c_file in &c_files {
args.push(c_file);
}
let build_output =
CommandExecutor::execute_command("emcc", &args, &config.project_path, config.verbose)?;
if !build_output.status.success() {
return Err(CompilationError::BuildFailed {
language: self.language_name().to_string(),
reason: format!(
"Emscripten build failed: {}",
String::from_utf8_lossy(&build_output.stderr)
),
});
}
if !wasm_output_file.exists() || !js_output_file.exists() {
return Err(CompilationError::BuildFailed {
language: self.language_name().to_string(),
reason: "Emscripten build completed but output files were not created".to_string(),
});
}
Ok(BuildResult {
wasm_path: wasm_output_file.to_string_lossy().to_string(),
js_path: Some(js_output_file.to_string_lossy().to_string()),
additional_files: vec![],
is_wasm_bindgen: true,
})
}
fn collect_c_files(&self, project_path: &str) -> CompilationResult<Vec<String>> {
let mut c_files = Vec::new();
let entries = fs::read_dir(project_path).map_err(|e| CompilationError::BuildFailed {
language: self.language_name().to_string(),
reason: format!("Failed to read project directory: {e}"),
})?;
for entry in entries.flatten() {
let path = entry.path();
if let Some(extension) = path.extension() {
if extension == "c" {
if let Some(path_str) = path.to_str() {
c_files.push(path_str.to_string());
}
}
}
}
if c_files.is_empty() {
return Err(CompilationError::BuildFailed {
language: self.language_name().to_string(),
reason: "No .c files found in project directory".to_string(),
});
}
Ok(c_files)
}
}
impl Plugin for CPlugin {
fn info(&self) -> &PluginInfo {
&self.info
}
fn can_handle_project(&self, project_path: &str) -> bool {
if self.has_makefile(project_path) {
return true;
}
if let Ok(entries) = fs::read_dir(project_path) {
for entry in entries.flatten() {
if let Some(extension) = entry.path().extension() {
let ext = extension.to_string_lossy().to_lowercase();
if ext == "c" {
return true;
}
}
}
}
false
}
fn get_builder(&self) -> Box<dyn WasmBuilder> {
Box::new(CPlugin::new())
}
}
impl WasmBuilder for CPlugin {
fn supported_extensions(&self) -> &[&str] {
&["c", "h", "cpp", "hpp", "cc", "cxx"]
}
fn entry_file_candidates(&self) -> &[&str] {
&[
"main.c",
"src/main.c",
"app.c",
"index.c",
"Makefile",
"CMakeLists.txt",
]
}
fn language_name(&self) -> &str {
"C"
}
fn check_dependencies(&self) -> Vec<String> {
let mut missing = Vec::new();
if !CommandExecutor::is_tool_installed("emcc") {
missing.push(
"emcc (Emscripten compiler - install from https://emscripten.org)".to_string(),
);
}
if self.has_makefile(&BuildConfig::default().project_path)
&& !CommandExecutor::is_tool_installed("make")
{
missing.push("make (build system)".to_string());
}
missing
}
fn validate_project(&self, project_path: &str) -> CompilationResult<()> {
PathResolver::validate_directory_exists(project_path).map_err(|e| {
CompilationError::InvalidProjectStructure {
language: self.language_name().to_string(),
reason: format!("Project directory validation failed: {e}"),
}
})?;
if !self.has_makefile(project_path) {
let _ = self.find_entry_file(project_path)?;
}
Ok(())
}
fn build(&self, config: &BuildConfig) -> CompilationResult<BuildResult> {
if !CommandExecutor::is_tool_installed("emcc") {
return Err(CompilationError::BuildToolNotFound {
tool: "emcc".to_string(),
language: self.language_name().to_string(),
});
}
if self.has_makefile(&config.project_path) {
self.build_with_makefile(config)
} else {
self.build_with_emscripten(config)
}
}
fn can_handle_project(&self, project_path: &str) -> bool {
if let Ok(entries) = std::fs::read_dir(project_path) {
for entry in entries.flatten() {
if let Some(extension) = entry.path().extension() {
let ext = extension.to_string_lossy().to_lowercase();
if self.supported_extensions().contains(&ext.as_str()) {
return true;
}
}
}
}
for entry_file in self.entry_file_candidates() {
let file_path = std::path::Path::new(project_path).join(entry_file);
if file_path.exists() {
return true;
}
}
false
}
fn clean(&self, project_path: &str) -> crate::error::Result<()> {
let artifacts = ["*.o", "*.wasm", "*.js", "build"];
for artifact in artifacts {
let path = std::path::Path::new(project_path).join(artifact);
if path.exists() {
if path.is_dir() {
let _ = std::fs::remove_dir_all(path);
} else {
let _ = std::fs::remove_file(path);
}
}
}
Ok(())
}
fn clone_box(&self) -> Box<dyn WasmBuilder> {
Box::new(self.clone())
}
}
impl Default for CPlugin {
fn default() -> Self {
Self::new()
}
}