1use crate::cli::{BuildArgs, WasmPlatform};
7use crate::error::{CliError, CliResult};
8use std::path::Path;
9use std::process::Command;
10
11pub fn execute(args: &BuildArgs) -> CliResult<()> {
13 let project_path = args.path.canonicalize().map_err(|e| {
14 CliError::Other(format!(
15 "Failed to resolve project path '{}': {}",
16 args.path.display(),
17 e
18 ))
19 })?;
20
21 let cargo_toml = project_path.join("Cargo.toml");
23 if !cargo_toml.exists() {
24 return Err(CliError::Other(format!(
25 "No Cargo.toml found at '{}'",
26 project_path.display()
27 )));
28 }
29
30 let target = determine_target(args)?;
32
33 println!("Building MCP server...");
34 if let Some(ref t) = target {
35 println!(" Target: {}", t);
36 }
37 if args.release {
38 println!(" Mode: release");
39 } else {
40 println!(" Mode: debug");
41 }
42
43 let mut cmd = Command::new("cargo");
45 cmd.arg("build");
46 cmd.current_dir(&project_path);
47
48 if let Some(ref t) = target {
50 cmd.arg("--target").arg(t);
51 }
52
53 if args.release {
55 cmd.arg("--release");
56 }
57
58 if args.no_default_features {
60 cmd.arg("--no-default-features");
61 }
62
63 for feature in &args.features {
64 cmd.arg("--features").arg(feature);
65 }
66
67 let status = cmd
69 .status()
70 .map_err(|e| CliError::Other(format!("Failed to execute cargo build: {}", e)))?;
71
72 if !status.success() {
73 return Err(CliError::Other("Cargo build failed".to_string()));
74 }
75
76 println!("Build successful!");
77
78 let profile = if args.release { "release" } else { "debug" };
80 let target_dir = project_path.join("target");
81
82 let output_dir = if let Some(ref t) = target {
83 target_dir.join(t).join(profile)
84 } else {
85 target_dir.join(profile)
86 };
87
88 if args.optimize && target.as_ref().is_some_and(|t| t.contains("wasm")) {
90 optimize_wasm(&output_dir, args)?;
91 }
92
93 if let Some(ref output) = args.output {
95 copy_artifacts(&output_dir, output, &target)?;
96 }
97
98 if let Some(ref output) = args.output {
100 println!("Artifacts copied to: {}", output.display());
101 } else {
102 println!("Artifacts at: {}", output_dir.display());
103 }
104
105 Ok(())
106}
107
108fn determine_target(args: &BuildArgs) -> CliResult<Option<String>> {
110 if let Some(ref target) = args.target {
112 return Ok(Some(target.clone()));
113 }
114
115 if let Some(ref platform) = args.platform {
117 let target = match platform {
118 WasmPlatform::CloudflareWorkers | WasmPlatform::DenoWorkers | WasmPlatform::Wasm32 => {
119 "wasm32-unknown-unknown"
120 }
121 };
122 return Ok(Some(target.to_string()));
123 }
124
125 Ok(None)
127}
128
129fn optimize_wasm(output_dir: &Path, args: &BuildArgs) -> CliResult<()> {
131 let wasm_opt_check = Command::new("wasm-opt").arg("--version").output();
133
134 if wasm_opt_check.is_err() {
135 println!("Warning: wasm-opt not found, skipping optimization");
136 println!(" Install with: cargo install wasm-opt");
137 return Ok(());
138 }
139
140 println!("Optimizing WASM binary...");
141
142 let wasm_files: Vec<_> = std::fs::read_dir(output_dir)
144 .map_err(|e| CliError::Other(format!("Failed to read output directory: {}", e)))?
145 .filter_map(|entry| entry.ok())
146 .filter(|entry| entry.path().extension().is_some_and(|ext| ext == "wasm"))
147 .collect();
148
149 for entry in wasm_files {
150 let wasm_path = entry.path();
151 let optimized_path = wasm_path.with_extension("optimized.wasm");
152
153 let opt_level = if args.release { "-O3" } else { "-O1" };
154
155 let status = Command::new("wasm-opt")
156 .arg(opt_level)
157 .arg("-o")
158 .arg(&optimized_path)
159 .arg(&wasm_path)
160 .status()
161 .map_err(|e| CliError::Other(format!("Failed to run wasm-opt: {}", e)))?;
162
163 if status.success() {
164 std::fs::rename(&optimized_path, &wasm_path)
166 .map_err(|e| CliError::Other(format!("Failed to replace WASM file: {}", e)))?;
167
168 let metadata = std::fs::metadata(&wasm_path)
170 .map_err(|e| CliError::Other(format!("Failed to get file metadata: {}", e)))?;
171 let size_kb = metadata.len() / 1024;
172
173 println!(" Optimized: {} ({}KB)", wasm_path.display(), size_kb);
174 } else {
175 println!("Warning: wasm-opt failed for {}", wasm_path.display());
176 }
177 }
178
179 Ok(())
180}
181
182fn copy_artifacts(source_dir: &Path, output_dir: &Path, target: &Option<String>) -> CliResult<()> {
184 std::fs::create_dir_all(output_dir)
186 .map_err(|e| CliError::Other(format!("Failed to create output directory: {}", e)))?;
187
188 let is_wasm = target.as_ref().is_some_and(|t| t.contains("wasm"));
190
191 if is_wasm {
192 for entry in std::fs::read_dir(source_dir)
194 .map_err(|e| CliError::Other(format!("Failed to read source directory: {}", e)))?
195 {
196 let entry =
197 entry.map_err(|e| CliError::Other(format!("Failed to read entry: {}", e)))?;
198 let path = entry.path();
199
200 if path.extension().is_some_and(|ext| ext == "wasm") {
201 let dest = output_dir.join(path.file_name().unwrap());
202 std::fs::copy(&path, &dest)
203 .map_err(|e| CliError::Other(format!("Failed to copy file: {}", e)))?;
204 }
205 }
206 } else {
207 for entry in std::fs::read_dir(source_dir)
209 .map_err(|e| CliError::Other(format!("Failed to read source directory: {}", e)))?
210 {
211 let entry =
212 entry.map_err(|e| CliError::Other(format!("Failed to read entry: {}", e)))?;
213 let path = entry.path();
214
215 if path.is_file() {
216 let is_binary = if cfg!(windows) {
217 path.extension().is_some_and(|ext| ext == "exe")
218 } else {
219 path.extension().is_none()
220 && std::fs::metadata(&path)
221 .map(|m| m.permissions().mode() & 0o111 != 0)
222 .unwrap_or(false)
223 };
224
225 if is_binary {
226 let dest = output_dir.join(path.file_name().unwrap());
227 std::fs::copy(&path, &dest)
228 .map_err(|e| CliError::Other(format!("Failed to copy file: {}", e)))?;
229 }
230 }
231 }
232 }
233
234 Ok(())
235}
236
237#[cfg(unix)]
238use std::os::unix::fs::PermissionsExt;
239
240#[cfg(not(unix))]
241trait PermissionsExt {
242 fn mode(&self) -> u32 {
243 0
244 }
245}
246
247#[cfg(not(unix))]
248impl PermissionsExt for std::fs::Permissions {}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn test_determine_target_explicit() {
256 let args = BuildArgs {
257 path: ".".into(),
258 platform: None,
259 target: Some("x86_64-unknown-linux-gnu".to_string()),
260 release: false,
261 optimize: false,
262 features: vec![],
263 no_default_features: false,
264 output: None,
265 };
266
267 let target = determine_target(&args).unwrap();
268 assert_eq!(target, Some("x86_64-unknown-linux-gnu".to_string()));
269 }
270
271 #[test]
272 fn test_determine_target_platform() {
273 let args = BuildArgs {
274 path: ".".into(),
275 platform: Some(WasmPlatform::CloudflareWorkers),
276 target: None,
277 release: false,
278 optimize: false,
279 features: vec![],
280 no_default_features: false,
281 output: None,
282 };
283
284 let target = determine_target(&args).unwrap();
285 assert_eq!(target, Some("wasm32-unknown-unknown".to_string()));
286 }
287
288 #[test]
289 fn test_determine_target_none() {
290 let args = BuildArgs {
291 path: ".".into(),
292 platform: None,
293 target: None,
294 release: false,
295 optimize: false,
296 features: vec![],
297 no_default_features: false,
298 output: None,
299 };
300
301 let target = determine_target(&args).unwrap();
302 assert_eq!(target, None);
303 }
304}