Skip to main content

portalis_build/
lib.rs

1//! Build Agent - Rust to WASM Compilation
2//!
3//! Compiles generated Rust code to WebAssembly.
4
5use 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/// Input from Transpiler Agent
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct BuildInput {
15    pub rust_code: String,
16}
17
18/// Compiled WASM output
19#[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
26/// Build Agent implementation
27pub 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    /// Create a temporary Rust project for compilation
48    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        // Create Cargo.toml
53        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        // Create src directory and lib.rs
68        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    /// Compile Rust code to WASM
79    fn compile_to_wasm(&self) -> Result<(Vec<u8>, String)> {
80        // Build with cargo
81        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        // Read the compiled WASM file
101        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    /// Clean up temporary workspace
111    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        // Set up project
141        self.setup_project(&input.rust_code)?;
142
143        // Compile to WASM
144        let (wasm_bytes, log) = self.compile_to_wasm()?;
145
146        // Validate WASM magic number
147        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] // Requires Rust toolchain and wasm32 target
181    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}