1use async_trait::async_trait;
6use portalis_core::{Agent, AgentCapability, AgentId, ArtifactMetadata, Error, Result};
7use serde::{Deserialize, Serialize};
8use std::fs;
9use std::path::PathBuf;
10use std::process::Command;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct BuildInput {
15 pub rust_code: String,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct BuildOutput {
21 pub wasm_bytes: Vec<u8>,
22 pub compilation_log: String,
23 pub metadata: ArtifactMetadata,
24}
25
26pub struct BuildAgent {
28 id: AgentId,
29 workspace_dir: PathBuf,
30}
31
32impl BuildAgent {
33 pub fn new() -> Self {
34 Self {
35 id: AgentId::new(),
36 workspace_dir: std::env::temp_dir().join(format!("portalis-build-{}", uuid::Uuid::new_v4())),
37 }
38 }
39
40 pub fn with_workspace(workspace_dir: PathBuf) -> Self {
41 Self {
42 id: AgentId::new(),
43 workspace_dir,
44 }
45 }
46
47 fn setup_project(&self, rust_code: &str) -> Result<PathBuf> {
49 fs::create_dir_all(&self.workspace_dir)
50 .map_err(|e| Error::Compilation(format!("Failed to create workspace: {}", e)))?;
51
52 let cargo_toml = r#"[package]
54name = "portalis-generated"
55version = "0.1.0"
56edition = "2021"
57
58[lib]
59crate-type = ["cdylib"]
60
61[dependencies]
62"#;
63
64 fs::write(self.workspace_dir.join("Cargo.toml"), cargo_toml)
65 .map_err(|e| Error::Compilation(format!("Failed to write Cargo.toml: {}", e)))?;
66
67 let src_dir = self.workspace_dir.join("src");
69 fs::create_dir_all(&src_dir)
70 .map_err(|e| Error::Compilation(format!("Failed to create src dir: {}", e)))?;
71
72 fs::write(src_dir.join("lib.rs"), rust_code)
73 .map_err(|e| Error::Compilation(format!("Failed to write lib.rs: {}", e)))?;
74
75 Ok(self.workspace_dir.clone())
76 }
77
78 fn compile_to_wasm(&self) -> Result<(Vec<u8>, String)> {
80 let output = Command::new("cargo")
82 .args(&["build", "--release", "--target", "wasm32-unknown-unknown"])
83 .current_dir(&self.workspace_dir)
84 .output()
85 .map_err(|e| Error::Compilation(format!("Failed to run cargo: {}", e)))?;
86
87 let log = format!(
88 "STDOUT:\n{}\n\nSTDERR:\n{}",
89 String::from_utf8_lossy(&output.stdout),
90 String::from_utf8_lossy(&output.stderr)
91 );
92
93 if !output.status.success() {
94 return Err(Error::Compilation(format!(
95 "Compilation failed:\n{}",
96 log
97 )));
98 }
99
100 let wasm_path = self.workspace_dir
102 .join("target/wasm32-unknown-unknown/release/portalis_generated.wasm");
103
104 let wasm_bytes = fs::read(&wasm_path)
105 .map_err(|e| Error::Compilation(format!("Failed to read WASM: {}", e)))?;
106
107 Ok((wasm_bytes, log))
108 }
109
110 fn cleanup(&self) -> Result<()> {
112 if self.workspace_dir.exists() {
113 fs::remove_dir_all(&self.workspace_dir)
114 .map_err(|e| Error::Compilation(format!("Failed to cleanup: {}", e)))?;
115 }
116 Ok(())
117 }
118}
119
120impl Default for BuildAgent {
121 fn default() -> Self {
122 Self::new()
123 }
124}
125
126impl Drop for BuildAgent {
127 fn drop(&mut self) {
128 let _ = self.cleanup();
129 }
130}
131
132#[async_trait]
133impl Agent for BuildAgent {
134 type Input = BuildInput;
135 type Output = BuildOutput;
136
137 async fn execute(&self, input: Self::Input) -> Result<Self::Output> {
138 tracing::info!("Building Rust code to WASM");
139
140 self.setup_project(&input.rust_code)?;
142
143 let (wasm_bytes, log) = self.compile_to_wasm()?;
145
146 if !wasm_bytes.starts_with(&[0x00, 0x61, 0x73, 0x6D]) {
148 return Err(Error::Compilation("Invalid WASM binary".into()));
149 }
150
151 let metadata = ArtifactMetadata::new(self.name())
152 .with_tag("size", wasm_bytes.len().to_string())
153 .with_tag("workspace", self.workspace_dir.display().to_string());
154
155 Ok(BuildOutput {
156 wasm_bytes,
157 compilation_log: log,
158 metadata,
159 })
160 }
161
162 fn id(&self) -> AgentId {
163 self.id
164 }
165
166 fn name(&self) -> &str {
167 "BuildAgent"
168 }
169
170 fn capabilities(&self) -> Vec<AgentCapability> {
171 vec![AgentCapability::Compilation]
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[tokio::test]
180 #[ignore] async fn test_compile_simple_rust() {
182 let agent = BuildAgent::new();
183
184 let rust_code = r#"
185#[no_mangle]
186pub extern "C" fn add(a: i32, b: i32) -> i32 {
187 a + b
188}
189"#;
190
191 let input = BuildInput {
192 rust_code: rust_code.to_string(),
193 };
194
195 let output = agent.execute(input).await.unwrap();
196
197 assert!(!output.wasm_bytes.is_empty());
198 assert!(output.wasm_bytes.starts_with(&[0x00, 0x61, 0x73, 0x6D]));
199 }
200}