use crate::cli::{BuildArgs, WasmPlatform};
use crate::error::{CliError, CliResult};
use std::path::Path;
use std::process::Command;
pub fn execute(args: &BuildArgs) -> CliResult<()> {
let project_path = args.path.canonicalize().map_err(|e| {
CliError::Other(format!(
"Failed to resolve project path '{}': {}",
args.path.display(),
e
))
})?;
let cargo_toml = project_path.join("Cargo.toml");
if !cargo_toml.exists() {
return Err(CliError::Other(format!(
"No Cargo.toml found at '{}'",
project_path.display()
)));
}
let target = determine_target(args)?;
println!("Building MCP server...");
if let Some(ref t) = target {
println!(" Target: {}", t);
}
if args.release {
println!(" Mode: release");
} else {
println!(" Mode: debug");
}
let mut cmd = Command::new("cargo");
cmd.arg("build");
cmd.current_dir(&project_path);
if let Some(ref t) = target {
cmd.arg("--target").arg(t);
}
if args.release {
cmd.arg("--release");
}
if args.no_default_features {
cmd.arg("--no-default-features");
}
for feature in &args.features {
cmd.arg("--features").arg(feature);
}
let status = cmd
.status()
.map_err(|e| CliError::Other(format!("Failed to execute cargo build: {}", e)))?;
if !status.success() {
return Err(CliError::Other("Cargo build failed".to_string()));
}
println!("Build successful!");
let profile = if args.release { "release" } else { "debug" };
let target_dir = project_path.join("target");
let output_dir = if let Some(ref t) = target {
target_dir.join(t).join(profile)
} else {
target_dir.join(profile)
};
if args.optimize && target.as_ref().is_some_and(|t| t.contains("wasm")) {
optimize_wasm(&output_dir, args)?;
}
if let Some(ref output) = args.output {
copy_artifacts(&output_dir, output, &target)?;
}
if let Some(ref output) = args.output {
println!("Artifacts copied to: {}", output.display());
} else {
println!("Artifacts at: {}", output_dir.display());
}
Ok(())
}
fn determine_target(args: &BuildArgs) -> CliResult<Option<String>> {
if let Some(ref target) = args.target {
return Ok(Some(target.clone()));
}
if let Some(ref platform) = args.platform {
let target = match platform {
WasmPlatform::CloudflareWorkers | WasmPlatform::DenoWorkers | WasmPlatform::Wasm32 => {
"wasm32-unknown-unknown"
}
};
return Ok(Some(target.to_string()));
}
Ok(None)
}
fn optimize_wasm(output_dir: &Path, args: &BuildArgs) -> CliResult<()> {
let wasm_opt_check = Command::new("wasm-opt").arg("--version").output();
if wasm_opt_check.is_err() {
println!("Warning: wasm-opt not found, skipping optimization");
println!(" Install with: cargo install wasm-opt");
return Ok(());
}
println!("Optimizing WASM binary...");
let wasm_files: Vec<_> = std::fs::read_dir(output_dir)
.map_err(|e| CliError::Other(format!("Failed to read output directory: {}", e)))?
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "wasm"))
.collect();
for entry in wasm_files {
let wasm_path = entry.path();
let optimized_path = wasm_path.with_extension("optimized.wasm");
let opt_level = if args.release { "-O3" } else { "-O1" };
let status = Command::new("wasm-opt")
.arg(opt_level)
.arg("-o")
.arg(&optimized_path)
.arg(&wasm_path)
.status()
.map_err(|e| CliError::Other(format!("Failed to run wasm-opt: {}", e)))?;
if status.success() {
std::fs::rename(&optimized_path, &wasm_path)
.map_err(|e| CliError::Other(format!("Failed to replace WASM file: {}", e)))?;
let metadata = std::fs::metadata(&wasm_path)
.map_err(|e| CliError::Other(format!("Failed to get file metadata: {}", e)))?;
let size_kb = metadata.len() / 1024;
println!(" Optimized: {} ({}KB)", wasm_path.display(), size_kb);
} else {
println!("Warning: wasm-opt failed for {}", wasm_path.display());
}
}
Ok(())
}
fn copy_artifacts(source_dir: &Path, output_dir: &Path, target: &Option<String>) -> CliResult<()> {
std::fs::create_dir_all(output_dir)
.map_err(|e| CliError::Other(format!("Failed to create output directory: {}", e)))?;
let is_wasm = target.as_ref().is_some_and(|t| t.contains("wasm"));
if is_wasm {
for entry in std::fs::read_dir(source_dir)
.map_err(|e| CliError::Other(format!("Failed to read source directory: {}", e)))?
{
let entry =
entry.map_err(|e| CliError::Other(format!("Failed to read entry: {}", e)))?;
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "wasm") {
let dest = output_dir.join(path.file_name().unwrap());
std::fs::copy(&path, &dest)
.map_err(|e| CliError::Other(format!("Failed to copy file: {}", e)))?;
}
}
} else {
for entry in std::fs::read_dir(source_dir)
.map_err(|e| CliError::Other(format!("Failed to read source directory: {}", e)))?
{
let entry =
entry.map_err(|e| CliError::Other(format!("Failed to read entry: {}", e)))?;
let path = entry.path();
if path.is_file() {
let is_binary = if cfg!(windows) {
path.extension().is_some_and(|ext| ext == "exe")
} else {
path.extension().is_none()
&& std::fs::metadata(&path)
.map(|m| m.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
};
if is_binary {
let dest = output_dir.join(path.file_name().unwrap());
std::fs::copy(&path, &dest)
.map_err(|e| CliError::Other(format!("Failed to copy file: {}", e)))?;
}
}
}
}
Ok(())
}
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[cfg(not(unix))]
trait PermissionsExt {
fn mode(&self) -> u32 {
0
}
}
#[cfg(not(unix))]
impl PermissionsExt for std::fs::Permissions {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_determine_target_explicit() {
let args = BuildArgs {
path: ".".into(),
platform: None,
target: Some("x86_64-unknown-linux-gnu".to_string()),
release: false,
optimize: false,
features: vec![],
no_default_features: false,
output: None,
};
let target = determine_target(&args).unwrap();
assert_eq!(target, Some("x86_64-unknown-linux-gnu".to_string()));
}
#[test]
fn test_determine_target_platform() {
let args = BuildArgs {
path: ".".into(),
platform: Some(WasmPlatform::CloudflareWorkers),
target: None,
release: false,
optimize: false,
features: vec![],
no_default_features: false,
output: None,
};
let target = determine_target(&args).unwrap();
assert_eq!(target, Some("wasm32-unknown-unknown".to_string()));
}
#[test]
fn test_determine_target_none() {
let args = BuildArgs {
path: ".".into(),
platform: None,
target: None,
release: false,
optimize: false,
features: vec![],
no_default_features: false,
output: None,
};
let target = determine_target(&args).unwrap();
assert_eq!(target, None);
}
}