theater_cli/commands/
build.rs1use anyhow::{anyhow, Result};
2use clap::Parser;
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::process::Command;
6use tracing::{debug, error, info};
7
8use crate::{error::CliError, output::formatters::BuildResult, CommandContext};
9use theater::config::actor_manifest::ManifestConfig;
10
11#[derive(Debug, Parser)]
12pub struct BuildArgs {
13 #[arg(default_value = ".")]
15 pub project_dir: PathBuf,
16
17 #[arg(short, long, default_value = "true")]
19 pub release: bool,
20
21 #[arg(short, long, default_value = "false")]
23 pub clean: bool,
24
25 #[arg(long, default_value = "false")]
28 pub no_verify: bool,
29}
30
31pub async fn execute_async(args: &BuildArgs, ctx: &CommandContext) -> Result<(), CliError> {
33 let project_dir = if args.project_dir.is_absolute() {
34 args.project_dir.clone()
35 } else {
36 std::env::current_dir()
37 .map_err(|e| CliError::file_operation_failed("get current directory", ".", e))?
38 .join(&args.project_dir)
39 };
40
41 debug!("Building actor in directory: {}", project_dir.display());
42 debug!("Release mode: {}", args.release);
43 debug!("Clean build: {}", args.clean);
44
45 let cargo_toml_path = project_dir.join("Cargo.toml");
47 if !cargo_toml_path.exists() {
48 return Err(CliError::invalid_manifest(format!(
49 "Not a Rust project directory (Cargo.toml not found): {}",
50 project_dir.display()
51 )));
52 }
53
54 let package_name = get_package_name(&cargo_toml_path)
56 .map_err(|e| CliError::invalid_manifest(format!("Failed to parse Cargo.toml: {}", e)))?;
57
58 let manifest_path = project_dir.join("manifest.toml");
60 let manifest_exists = manifest_path.exists();
61
62 if args.clean {
64 debug!("Cleaning build artifacts...");
65 let mut clean_cmd = Command::new("cargo");
66 clean_cmd.arg("clean").current_dir(&project_dir);
67
68 if let Err(e) = run_command_with_output(&mut clean_cmd, ctx.is_verbose()) {
69 error!("Failed to clean cargo artifacts: {}", e);
70 }
72 }
73
74 debug!(
76 "Building WebAssembly module for actor in {}...",
77 project_dir.display()
78 );
79
80 let mut build_cmd = Command::new("cargo");
82 build_cmd.args(["build", "--target", "wasm32-unknown-unknown"]);
83
84 if args.release {
85 build_cmd.arg("--release");
86 }
87
88 build_cmd.current_dir(&project_dir);
89
90 let (status, stdout, stderr) = run_command_with_output(&mut build_cmd, ctx.is_verbose())
92 .map_err(|e| CliError::build_failed(format!("Failed to execute cargo build: {}", e)))?;
93
94 if !status.success() {
96 let error_details = if stderr.is_empty() { stdout } else { stderr };
97 return Err(CliError::build_failed(format!(
98 "Cargo build failed:\\n{}",
99 error_details
100 )));
101 }
102
103 let build_type = if args.release { "release" } else { "debug" };
105 let wasm_file_name = format!("{}.wasm", package_name.replace('-', "_"));
106 let wasm_path = project_dir
107 .join("target/wasm32-unknown-unknown")
108 .join(build_type)
109 .join(&wasm_file_name);
110
111 if !wasm_path.exists() {
113 return Err(CliError::build_failed(format!(
114 "Built WASM file not found at expected path: {}",
115 wasm_path.display()
116 )));
117 }
118
119 let artifact_path = wasm_path.clone();
128 if args.no_verify {
129 info!(
130 "--no-verify: skipping the self-contained verification gate for {}",
131 wasm_path.display()
132 );
133 } else {
134 super::compose::verify_self_contained(&wasm_path).map_err(|e| {
135 CliError::build_failed(format!(
136 "Self-contained verification failed for {}: {e}",
137 wasm_path.display()
138 ))
139 })?;
140 info!("Verified self-contained actor: {}", wasm_path.display());
141 }
142
143 if manifest_exists {
145 let manifest_content = fs::read_to_string(&manifest_path).map_err(|e| {
146 CliError::file_operation_failed(
147 "read manifest.toml",
148 manifest_path.display().to_string(),
149 e,
150 )
151 })?;
152
153 let mut manifest: ManifestConfig = toml::from_str(&manifest_content).map_err(|e| {
154 CliError::invalid_manifest(format!("Failed to parse manifest.toml: {}", e))
155 })?;
156
157 manifest.package = artifact_path.to_string_lossy().to_string();
160
161 let updated_manifest = toml::to_string(&manifest).map_err(|e| {
163 CliError::invalid_manifest(format!("Failed to serialize manifest.toml: {}", e))
164 })?;
165
166 fs::write(&manifest_path, updated_manifest).map_err(|e| {
167 CliError::file_operation_failed(
168 "write manifest.toml",
169 manifest_path.display().to_string(),
170 e,
171 )
172 })?;
173
174 info!(
175 "Updated manifest with component path: {}",
176 artifact_path.display()
177 );
178 }
179
180 let result = BuildResult {
182 success: true,
183 project_dir,
184 wasm_path: Some(artifact_path),
185 manifest_exists,
186 manifest_path: Some(manifest_path),
187 build_type: build_type.to_string(),
188 package_name,
189 stdout,
190 stderr,
191 };
192
193 ctx.output.output(&result, None)?;
194 Ok(())
195}
196
197fn run_command_with_output(
199 cmd: &mut Command,
200 verbose: bool,
201) -> Result<(std::process::ExitStatus, String, String)> {
202 debug!("Running command: {:?}", cmd);
203 cmd.env("RUST_BACKTRACE", "1");
204 cmd.env("RUST_COLOR", "always");
205 cmd.env("CARGO_TERM_COLOR", "always");
206
207 if verbose {
208 let status = cmd
211 .status()
212 .map_err(|e| anyhow!("Failed to execute command: {}", e))?;
213
214 Ok((status, String::new(), String::new()))
217 } else {
218 let output = cmd
221 .output()
222 .map_err(|e| anyhow!("Failed to execute command: {}", e))?;
223
224 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
225 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
226
227 Ok((output.status, stdout, stderr))
228 }
229}
230
231fn get_package_name(cargo_toml_path: &Path) -> Result<String> {
233 let cargo_toml = std::fs::read_to_string(cargo_toml_path)?;
234
235 for line in cargo_toml.lines() {
237 let line = line.trim();
238 if line.starts_with("name") {
239 let parts: Vec<&str> = line.split('=').collect();
240 if parts.len() >= 2 {
241 let name = parts[1].trim().trim_matches('"').trim_matches('\'');
242 return Ok(name.to_string());
243 }
244 }
245 }
246
247 Err(anyhow!("Could not find package name in Cargo.toml"))
248}