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 InvalidProject(String),
13}
14
15impl fmt::Display for BuildError {
16 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17 match self {
18 BuildError::MissingConfig => write!(f, "No [contracts] configured in .sdkt.toml"),
19 BuildError::PathNotFound(path) => write!(f, "Contract path does not exist: {}", path),
20 BuildError::CargoFailed { path, stderr } => {
21 write!(f, "Cargo build failed in {}:\n{}", path, stderr)
22 }
23 BuildError::ArtifactNotFound(path) => {
24 write!(f, "Expected WASM artifact not found at: {}", path)
25 }
26 BuildError::InvalidProject(msg) => {
27 write!(f, "Invalid project dependency graph: {}", msg)
28 }
29 }
30 }
31}
32
33impl std::error::Error for BuildError {}
34
35#[derive(Debug, PartialEq)]
37pub struct BuildResult {
38 pub alias: String,
39 pub path: String,
40 pub wasm_artifact: PathBuf,
41}
42
43pub fn build_workspace(config: &DevKitConfig) -> Result<Vec<BuildResult>, BuildError> {
53 if config.contracts.is_empty() {
54 return Err(BuildError::MissingConfig);
55 }
56
57 let ordered = crate::project::resolve_deploy_order(config)
60 .map_err(|e| BuildError::InvalidProject(e.to_string()))?;
61
62 let mut results = Vec::new();
63
64 for alias in &ordered {
65 let contract_cfg = config
66 .contracts
67 .get(alias)
68 .expect("alias from resolved order");
69 let path = Path::new(&contract_cfg.path);
70
71 if !path.exists() || !path.is_dir() {
72 return Err(BuildError::PathNotFound(contract_cfg.path.clone()));
73 }
74
75 let output = Command::new("cargo")
77 .arg("build")
78 .arg("--target")
79 .arg("wasm32-unknown-unknown")
80 .arg("--release")
81 .current_dir(path)
82 .output()
83 .map_err(|e| BuildError::CargoFailed {
84 path: contract_cfg.path.clone(),
85 stderr: e.to_string(),
86 })?;
87
88 if !output.status.success() {
89 return Err(BuildError::CargoFailed {
90 path: contract_cfg.path.clone(),
91 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
92 });
93 }
94
95 let target_dir = path
101 .join("target")
102 .join("wasm32-unknown-unknown")
103 .join("release");
104
105 let mut found_wasm = None;
106 if target_dir.exists() {
107 if let Ok(entries) = std::fs::read_dir(&target_dir) {
108 for entry in entries.flatten() {
109 let p = entry.path();
110 if p.extension().is_some_and(|ext| ext == "wasm") {
111 found_wasm = Some(p);
112 break;
113 }
114 }
115 }
116 }
117
118 let wasm_artifact = found_wasm
119 .ok_or_else(|| BuildError::ArtifactNotFound(target_dir.display().to_string()))?;
120
121 results.push(BuildResult {
122 alias: alias.clone(),
123 path: contract_cfg.path.clone(),
124 wasm_artifact,
125 });
126 }
127
128 if let Ok(lock) = crate::lock::generate_lock(Path::new("."), config) {
133 match crate::lock::write_lock(Path::new("."), &lock) {
134 Ok(path) => {
135 if let Ok(toml) = crate::lock::lock_to_toml(&lock) {
136 println!("✓ Wrote {}", path.display());
137 println!("{}", toml);
138 }
139 }
140 Err(e) => eprintln!("Warning: could not write sdkt.lock: {}", e),
141 }
142 }
143
144 Ok(results)
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::config::ContractConfig;
151 use std::collections::HashMap;
152
153 #[test]
154 fn test_build_empty_config() {
155 let config = DevKitConfig::default();
156 let res = build_workspace(&config);
157 assert!(matches!(res, Err(BuildError::MissingConfig)));
158 }
159
160 #[test]
161 fn test_build_missing_path() {
162 let mut config = DevKitConfig::default();
163 let mut contracts = HashMap::new();
164 contracts.insert(
165 "token".to_string(),
166 ContractConfig {
167 path: "does_not_exist_xyz".to_string(),
168 deploy_after: vec![],
169 depends_on: vec![],
170 },
171 );
172 config.contracts = contracts;
173
174 let res = build_workspace(&config);
175 match res {
176 Err(BuildError::PathNotFound(p)) => assert_eq!(p, "does_not_exist_xyz"),
177 _ => panic!("Expected PathNotFound"),
178 }
179 }
180}