1use crate::config::DevKitConfig;
2use std::fmt;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5
6#[derive(Debug)]
7pub enum BuildError {
8 MissingConfig,
9 PathNotFound(String),
10 CargoFailed { path: String, stderr: String },
11 ArtifactNotFound(String),
12}
13
14impl fmt::Display for BuildError {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 match self {
17 BuildError::MissingConfig => write!(f, "No [contracts] configured in .sdkt.toml"),
18 BuildError::PathNotFound(path) => write!(f, "Contract path does not exist: {}", path),
19 BuildError::CargoFailed { path, stderr } => {
20 write!(f, "Cargo build failed in {}:\n{}", path, stderr)
21 }
22 BuildError::ArtifactNotFound(path) => {
23 write!(f, "Expected WASM artifact not found at: {}", path)
24 }
25 }
26 }
27}
28
29impl std::error::Error for BuildError {}
30
31#[derive(Debug, PartialEq)]
33pub struct BuildResult {
34 pub alias: String,
35 pub path: String,
36 pub wasm_artifact: PathBuf,
37}
38
39pub fn build_workspace(config: &DevKitConfig) -> Result<Vec<BuildResult>, BuildError> {
44 if config.contracts.is_empty() {
45 return Err(BuildError::MissingConfig);
46 }
47
48 let mut results = Vec::new();
49
50 for (alias, contract_cfg) in &config.contracts {
51 let path = Path::new(&contract_cfg.path);
52
53 if !path.exists() || !path.is_dir() {
54 return Err(BuildError::PathNotFound(contract_cfg.path.clone()));
55 }
56
57 let output = Command::new("cargo")
59 .arg("build")
60 .arg("--target")
61 .arg("wasm32-unknown-unknown")
62 .arg("--release")
63 .current_dir(path)
64 .output()
65 .map_err(|e| BuildError::CargoFailed {
66 path: contract_cfg.path.clone(),
67 stderr: e.to_string(),
68 })?;
69
70 if !output.status.success() {
71 return Err(BuildError::CargoFailed {
72 path: contract_cfg.path.clone(),
73 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
74 });
75 }
76
77 let target_dir = path
83 .join("target")
84 .join("wasm32-unknown-unknown")
85 .join("release");
86
87 let mut found_wasm = None;
88 if target_dir.exists() {
89 if let Ok(entries) = std::fs::read_dir(&target_dir) {
90 for entry in entries.flatten() {
91 let p = entry.path();
92 if p.extension().is_some_and(|ext| ext == "wasm") {
93 found_wasm = Some(p);
94 break;
95 }
96 }
97 }
98 }
99
100 let wasm_artifact = found_wasm
101 .ok_or_else(|| BuildError::ArtifactNotFound(target_dir.display().to_string()))?;
102
103 results.push(BuildResult {
104 alias: alias.clone(),
105 path: contract_cfg.path.clone(),
106 wasm_artifact,
107 });
108 }
109
110 Ok(results)
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::config::ContractConfig;
117 use std::collections::HashMap;
118
119 #[test]
120 fn test_build_empty_config() {
121 let config = DevKitConfig::default();
122 let res = build_workspace(&config);
123 assert!(matches!(res, Err(BuildError::MissingConfig)));
124 }
125
126 #[test]
127 fn test_build_missing_path() {
128 let mut config = DevKitConfig::default();
129 let mut contracts = HashMap::new();
130 contracts.insert(
131 "token".to_string(),
132 ContractConfig {
133 path: "does_not_exist_xyz".to_string(),
134 deploy_after: vec![],
135 },
136 );
137 config.contracts = contracts;
138
139 let res = build_workspace(&config);
140 match res {
141 Err(BuildError::PathNotFound(p)) => assert_eq!(p, "does_not_exist_xyz"),
142 _ => panic!("Expected PathNotFound"),
143 }
144 }
145}