Skip to main content

sdkt_core/
build.rs

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/// Result of building a single contract.
32#[derive(Debug, PartialEq)]
33pub struct BuildResult {
34    pub alias: String,
35    pub path: String,
36    pub wasm_artifact: PathBuf,
37}
38
39/// Builds all contracts defined in the DevKitConfig.
40///
41/// For each contract, it navigates to the configured `path` and runs:
42/// `cargo build --target wasm32-unknown-unknown --release`
43pub 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        // Execute cargo build
58        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        // We assume a standard Soroban project structure where Cargo.toml has a package name.
78        // For sdkt build, we will attempt to extract the expected artifact name from Cargo.toml,
79        // or just glob the target/wasm32-unknown-unknown/release/*.wasm dir.
80        // For stability without adding `cargo-metadata` dependency, we will look for any .wasm file
81        // generated in the release directory.
82        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}