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
26pub async fn execute_async(args: &BuildArgs, ctx: &CommandContext) -> Result<(), CliError> {
28 let project_dir = if args.project_dir.is_absolute() {
29 args.project_dir.clone()
30 } else {
31 std::env::current_dir()
32 .map_err(|e| CliError::file_operation_failed("get current directory", ".", e))?
33 .join(&args.project_dir)
34 };
35
36 debug!("Building actor in directory: {}", project_dir.display());
37 debug!("Release mode: {}", args.release);
38 debug!("Clean build: {}", args.clean);
39
40 let cargo_toml_path = project_dir.join("Cargo.toml");
42 if !cargo_toml_path.exists() {
43 return Err(CliError::invalid_manifest(format!(
44 "Not a Rust project directory (Cargo.toml not found): {}",
45 project_dir.display()
46 )));
47 }
48
49 let package_name = get_package_name(&cargo_toml_path)
51 .map_err(|e| CliError::invalid_manifest(format!("Failed to parse Cargo.toml: {}", e)))?;
52
53 let manifest_path = project_dir.join("manifest.toml");
55 let manifest_exists = manifest_path.exists();
56
57 if !is_cargo_component_installed() {
59 return Err(CliError::build_failed(
60 "cargo-component is not installed. Please install it with 'cargo install cargo-component'."
61 ));
62 }
63
64 if args.clean {
66 debug!("Cleaning build artifacts...");
67 let mut clean_cmd = Command::new("cargo");
68 clean_cmd.arg("clean").current_dir(&project_dir);
69
70 if let Err(e) = run_command_with_output(&mut clean_cmd, ctx.verbose) {
71 error!("Failed to clean cargo artifacts: {}", e);
72 }
74 }
75
76 debug!(
78 "Building WebAssembly component for actor in {}...",
79 project_dir.display()
80 );
81
82 let mut build_cmd = Command::new("cargo");
84 build_cmd.args(["component", "build", "--target", "wasm32-unknown-unknown"]);
85
86 if args.release {
87 build_cmd.arg("--release");
88 }
89
90 build_cmd.current_dir(&project_dir);
91
92 let (status, stdout, stderr) =
94 run_command_with_output(&mut build_cmd, ctx.verbose).map_err(|e| {
95 CliError::build_failed(format!("Failed to execute cargo component build: {}", e))
96 })?;
97
98 if !status.success() {
100 let error_details = if stderr.is_empty() { stdout } else { stderr };
101 return Err(CliError::build_failed(format!(
102 "Cargo component build failed:\\n{}",
103 error_details
104 )));
105 }
106
107 let build_type = if args.release { "release" } else { "debug" };
109 let wasm_file_name = format!("{}.wasm", package_name.replace('-', "_"));
110 let wasm_path = project_dir
111 .join("target/wasm32-unknown-unknown")
112 .join(build_type)
113 .join(&wasm_file_name);
114
115 if !wasm_path.exists() {
117 return Err(CliError::build_failed(format!(
118 "Built WASM file not found at expected path: {}",
119 wasm_path.display()
120 )));
121 }
122
123 if manifest_exists {
125 let manifest_content = fs::read_to_string(&manifest_path).map_err(|e| {
126 CliError::file_operation_failed(
127 "read manifest.toml",
128 manifest_path.display().to_string(),
129 e,
130 )
131 })?;
132
133 let mut manifest: ManifestConfig = toml::from_str(&manifest_content).map_err(|e| {
134 CliError::invalid_manifest(format!("Failed to parse manifest.toml: {}", e))
135 })?;
136
137 manifest.component = wasm_path.to_string_lossy().to_string();
139
140 let updated_manifest = toml::to_string(&manifest).map_err(|e| {
142 CliError::invalid_manifest(format!("Failed to serialize manifest.toml: {}", e))
143 })?;
144
145 fs::write(&manifest_path, updated_manifest).map_err(|e| {
146 CliError::file_operation_failed(
147 "write manifest.toml",
148 manifest_path.display().to_string(),
149 e,
150 )
151 })?;
152
153 info!(
154 "Updated manifest with component path: {}",
155 wasm_path.display()
156 );
157 }
158
159 let result = BuildResult {
161 success: true,
162 project_dir,
163 wasm_path: Some(wasm_path),
164 manifest_exists,
165 manifest_path: Some(manifest_path),
166 build_type: build_type.to_string(),
167 package_name,
168 stdout,
169 stderr,
170 };
171
172 ctx.output.output(&result, None)?;
173 Ok(())
174}
175
176fn run_command_with_output(
178 cmd: &mut Command,
179 verbose: bool,
180) -> Result<(std::process::ExitStatus, String, String)> {
181 debug!("Running command: {:?}", cmd);
182 cmd.env("RUST_BACKTRACE", "1");
183 cmd.env("RUST_COLOR", "always");
184 cmd.env("CARGO_TERM_COLOR", "always");
185
186 if verbose {
187 let status = cmd
190 .status()
191 .map_err(|e| anyhow!("Failed to execute command: {}", e))?;
192
193 Ok((status, String::new(), String::new()))
196 } else {
197 let output = cmd
200 .output()
201 .map_err(|e| anyhow!("Failed to execute command: {}", e))?;
202
203 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
204 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
205
206 Ok((output.status, stdout, stderr))
207 }
208}
209
210fn is_cargo_component_installed() -> bool {
212 let output = Command::new("cargo").args(["--list"]).output();
213
214 match output {
215 Ok(output) => {
216 let stdout = String::from_utf8_lossy(&output.stdout);
217 stdout.contains("component")
218 }
219 Err(_) => false,
220 }
221}
222
223fn get_package_name(cargo_toml_path: &Path) -> Result<String> {
225 let cargo_toml = std::fs::read_to_string(cargo_toml_path)?;
226
227 for line in cargo_toml.lines() {
229 let line = line.trim();
230 if line.starts_with("name") {
231 let parts: Vec<&str> = line.split('=').collect();
232 if parts.len() >= 2 {
233 let name = parts[1].trim().trim_matches('"').trim_matches('\'');
234 return Ok(name.to_string());
235 }
236 }
237 }
238
239 Err(anyhow!("Could not find package name in Cargo.toml"))
240}