1use std::path::{Path, PathBuf};
4use std::process::Command;
5
6use crate::error::{Error, Result};
7
8#[derive(Debug, Clone)]
10pub struct CompilerOptions {
11 pub target: String,
13
14 pub opt_level: OptimizationLevel,
16
17 pub debug_level: DebugLevel,
19
20 pub features: Vec<String>,
22
23 pub no_default_features: bool,
25
26 pub extra_args: Vec<String>,
28
29 pub toolchain: String,
31
32 pub profile: BuildProfile,
34
35 pub target_cpu: Option<String>,
37
38 pub rustflags: Option<String>,
40}
41
42impl Default for CompilerOptions {
43 fn default() -> Self {
44 Self {
45 target: "wasm32-wasi".to_string(),
46 opt_level: OptimizationLevel::Default,
47 debug_level: DebugLevel::None,
48 features: Vec::new(),
49 no_default_features: false,
50 extra_args: Vec::new(),
51 toolchain: "stable".to_string(),
52 profile: BuildProfile::Release,
53 target_cpu: None,
54 rustflags: None,
55 }
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum OptimizationLevel {
62 None,
64
65 Basic,
67
68 Default,
70
71 Size,
73
74 Speed,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum DebugLevel {
81 None,
83
84 Basic,
86
87 Full,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum BuildProfile {
94 Debug,
96
97 Release,
99}
100
101pub trait Compiler {
103 fn compile(
105 &self,
106 project_path: &Path,
107 output_path: &Path,
108 options: &CompilerOptions,
109 ) -> Result<PathBuf>;
110
111 fn check_available(&self) -> bool;
113
114 fn version(&self) -> Result<String>;
116}
117
118pub struct CargoCompiler;
120
121impl CargoCompiler {
122 pub fn new() -> Self {
124 Self
125 }
126}
127
128impl Compiler for CargoCompiler {
129 fn compile(
130 &self,
131 project_path: &Path,
132 output_path: &Path,
133 options: &CompilerOptions,
134 ) -> Result<PathBuf> {
135 if !self.check_available() {
137 return Err(Error::Compilation { message: "Cargo is not available".to_string() });
138 }
139
140 std::fs::create_dir_all(output_path)
142 .map_err(|e| Error::Filesystem {
143 operation: "create_dir_all".to_string(),
144 path: output_path.to_path_buf(),
145 reason: format!("Failed to create output directory: {}", e)
146 })?;
147
148 let mut cmd = Command::new("cargo");
150
151 if options.toolchain != "stable" {
153 cmd.arg(format!("+{}", options.toolchain));
154 }
155
156 cmd.current_dir(project_path)
158 .arg("build")
159 .arg("--target").arg(&options.target);
160
161 match options.profile {
163 BuildProfile::Debug => {
164 },
166 BuildProfile::Release => {
167 cmd.arg("--release");
168 },
169 }
170
171 match options.opt_level {
173 OptimizationLevel::None => {
174 cmd.env("RUSTFLAGS", "-C opt-level=0");
175 },
176 OptimizationLevel::Basic => {
177 cmd.env("RUSTFLAGS", "-C opt-level=1");
178 },
179 OptimizationLevel::Default => {
180 },
182 OptimizationLevel::Size => {
183 cmd.env("RUSTFLAGS", "-C opt-level=s");
184 },
185 OptimizationLevel::Speed => {
186 cmd.env("RUSTFLAGS", "-C opt-level=3");
187 },
188 }
189
190 match options.debug_level {
192 DebugLevel::None => {
193 if options.profile == BuildProfile::Debug {
195 cmd.env("RUSTFLAGS", "-C debuginfo=0");
196 }
197 },
198 DebugLevel::Basic => {
199 cmd.env("RUSTFLAGS", "-C debuginfo=1");
200 },
201 DebugLevel::Full => {
202 cmd.env("RUSTFLAGS", "-C debuginfo=2");
203 },
204 }
205
206 if !options.features.is_empty() {
208 cmd.arg("--features").arg(options.features.join(","));
209 }
210
211 if options.no_default_features {
213 cmd.arg("--no-default-features");
214 }
215
216 for arg in &options.extra_args {
218 cmd.arg(arg);
219 }
220
221 let output = cmd.output()
223 .map_err(|e| Error::Compilation { message: format!("Failed to execute cargo: {}", e) })?;
224
225 if !output.status.success() {
227 let stderr = String::from_utf8_lossy(&output.stderr);
228 return Err(Error::Compilation { message: format!("Build failed: {}", stderr) });
229 }
230
231 let profile_dir = match options.profile {
233 BuildProfile::Debug => "debug",
234 BuildProfile::Release => "release",
235 };
236
237 let cargo_toml_path = project_path.join("Cargo.toml");
239 let cargo_toml = std::fs::read_to_string(&cargo_toml_path)
240 .map_err(|e| Error::Filesystem {
241 operation: "read".to_string(),
242 path: cargo_toml_path.clone(),
243 reason: format!("Failed to read Cargo.toml: {}", e)
244 })?;
245
246 let package_name = cargo_toml
248 .lines()
249 .find_map(|line| {
250 if line.trim().starts_with("name") {
251 line.split('=')
252 .nth(1)
253 .map(|s| s.trim().trim_matches('"').to_string())
254 } else {
255 None
256 }
257 })
258 .ok_or_else(|| Error::Compilation { message: "Failed to determine package name".to_string() })?;
259
260 let wasm_file = format!("{}.wasm", package_name);
262 let wasm_path = project_path
263 .join("target")
264 .join(&options.target)
265 .join(profile_dir)
266 .join(wasm_file);
267
268 let output_wasm_path = output_path.join(format!("{}.wasm", package_name));
270 std::fs::copy(&wasm_path, &output_wasm_path)
271 .map_err(|e| Error::Filesystem {
272 operation: "copy".to_string(),
273 path: wasm_path.clone(),
274 reason: format!("Failed to copy WASM file: {}", e)
275 })?;
276
277 Ok(output_wasm_path)
278 }
279
280 fn check_available(&self) -> bool {
281 Command::new("cargo")
282 .arg("--version")
283 .output()
284 .is_ok()
285 }
286
287 fn version(&self) -> Result<String> {
288 let output = Command::new("cargo")
289 .arg("--version")
290 .output()
291 .map_err(|e| Error::Compilation { message: format!("Failed to get cargo version: {}", e) })?;
292
293 if output.status.success() {
294 let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
295 Ok(version)
296 } else {
297 let stderr = String::from_utf8_lossy(&output.stderr);
298 Err(Error::Compilation { message: format!("Failed to get cargo version: {}", stderr) })
299 }
300 }
301}
302
303pub mod cargo;
304pub mod wasi;