Skip to main content

turbomcp_cli/
build.rs

1//! Build command implementation for MCP servers.
2//!
3//! Supports building for native and WASM targets, with platform-specific
4//! optimizations for Cloudflare Workers and other edge runtimes.
5
6use crate::cli::{BuildArgs, WasmPlatform};
7use crate::error::{CliError, CliResult};
8use std::path::Path;
9use std::process::Command;
10
11/// Execute the build command.
12pub 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    // Check if Cargo.toml exists
22    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    // Determine target based on platform or explicit target
31    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    // Build the cargo command
44    let mut cmd = Command::new("cargo");
45    cmd.arg("build");
46    cmd.current_dir(&project_path);
47
48    // Add target if specified
49    if let Some(ref t) = target {
50        cmd.arg("--target").arg(t);
51    }
52
53    // Release mode
54    if args.release {
55        cmd.arg("--release");
56    }
57
58    // Features
59    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    // Execute cargo build
68    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    // Determine output path
79    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    // For WASM targets, run wasm-opt if requested
89    if args.optimize && target.as_ref().is_some_and(|t| t.contains("wasm")) {
90        optimize_wasm(&output_dir, args)?;
91    }
92
93    // Copy to output directory if specified
94    if let Some(ref output) = args.output {
95        copy_artifacts(&output_dir, output, &target)?;
96    }
97
98    // Print output location
99    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
108/// Determine the Rust target based on platform or explicit target argument.
109fn determine_target(args: &BuildArgs) -> CliResult<Option<String>> {
110    // Explicit target takes precedence
111    if let Some(ref target) = args.target {
112        return Ok(Some(target.clone()));
113    }
114
115    // Platform-specific targets
116    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    // No target specified - build for native
126    Ok(None)
127}
128
129/// Optimize WASM binary using wasm-opt.
130fn optimize_wasm(output_dir: &Path, args: &BuildArgs) -> CliResult<()> {
131    // Check if wasm-opt is available
132    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    // Find all .wasm files in the output directory
143    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            // Replace original with optimized
165            std::fs::rename(&optimized_path, &wasm_path)
166                .map_err(|e| CliError::Other(format!("Failed to replace WASM file: {}", e)))?;
167
168            // Get file size for reporting
169            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
182/// Copy build artifacts to the specified output directory.
183fn copy_artifacts(source_dir: &Path, output_dir: &Path, target: &Option<String>) -> CliResult<()> {
184    // Create output directory
185    std::fs::create_dir_all(output_dir)
186        .map_err(|e| CliError::Other(format!("Failed to create output directory: {}", e)))?;
187
188    // Determine which files to copy based on target
189    let is_wasm = target.as_ref().is_some_and(|t| t.contains("wasm"));
190
191    if is_wasm {
192        // Copy .wasm files
193        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        // Copy binary files (no extension on Unix, .exe on Windows)
208        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}