Skip to main content

sentri_utils/
solc.rs

1//! Solidity compiler (solc) management and execution.
2//!
3//! This module handles finding, downloading, and running the solc compiler
4//! to generate JSON AST output for Solidity smart contracts.
5
6use anyhow::{anyhow, bail, Context, Result};
7use serde::{Deserialize, Serialize};
8use std::path::{Path, PathBuf};
9use std::process::Command;
10use tracing::info;
11
12/// Minimum supported solc version
13pub const MIN_SOLC_VERSION: &str = "0.6.0";
14
15/// Represents the solc compiler manager
16#[derive(Debug, Clone)]
17pub struct SolcManager {
18    solc_path: PathBuf,
19    version: String,
20}
21
22/// Solc combined JSON output
23#[derive(Debug, Clone, Deserialize, Serialize)]
24pub struct SolcOutput {
25    /// Contract data keyed by fully qualified name
26    pub contracts: std::collections::HashMap<String, serde_json::Value>,
27    /// Source files with AST
28    pub sources: std::collections::HashMap<String, SourceData>,
29}
30
31/// Source file data with AST
32#[derive(Debug, Clone, Deserialize, Serialize)]
33pub struct SourceData {
34    #[serde(rename = "AST")]
35    /// Raw AST JSON value from solc output
36    pub ast: serde_json::Value,
37}
38
39impl SolcManager {
40    /// Find or download a compatible solc binary
41    ///
42    /// Search order:
43    /// 1. SOLC_PATH environment variable
44    /// 2. solc on system PATH
45    /// 3. ~/.sentri/solc/solc (cached download)
46    /// 4. Download latest stable from binaries.soliditylang.org
47    pub fn new() -> Result<Self> {
48        // Try SOLC_PATH environment variable
49        if let Ok(path) = std::env::var("SOLC_PATH") {
50            let path = PathBuf::from(path);
51            if path.exists() {
52                let version = Self::get_version(&path)?;
53                info!(
54                    "Using solc from SOLC_PATH: {} (v{})",
55                    path.display(),
56                    version
57                );
58                return Ok(Self {
59                    solc_path: path,
60                    version,
61                });
62            }
63        }
64
65        // Try solc on system PATH
66        if let Ok(output) = Command::new("solc").arg("--version").output() {
67            if output.status.success() {
68                let version_str = String::from_utf8_lossy(&output.stdout);
69                let version = Self::parse_version(&version_str)?;
70                info!("Using system solc (v{})", version);
71                return Ok(Self {
72                    solc_path: PathBuf::from("solc"),
73                    version,
74                });
75            }
76        }
77
78        // Try cached download
79        let cache_path = Self::cache_path();
80        if cache_path.exists() {
81            let version = Self::get_version(&cache_path)?;
82            info!(
83                "Using cached solc from {} (v{})",
84                cache_path.display(),
85                version
86            );
87            return Ok(Self {
88                solc_path: cache_path,
89                version,
90            });
91        }
92
93        // Download latest stable
94        info!("solc not found locally, downloading...");
95        Self::download_solc()
96    }
97
98    /// Get the full AST JSON output for a Solidity file
99    pub fn get_ast_json(&self, source_path: &Path) -> Result<SolcOutput> {
100        let output = Command::new(&self.solc_path)
101            .args([
102                "--combined-json",
103                "ast,abi,bin,bin-runtime,srcmap,srcmap-runtime",
104                "--allow-paths",
105                ".",
106            ])
107            .arg(source_path)
108            .output()
109            .with_context(|| format!("Failed to run solc on: {}", source_path.display()))?;
110
111        if !output.status.success() {
112            let stderr = String::from_utf8_lossy(&output.stderr);
113            // Only fail on fatal errors, not warnings
114            if Self::has_fatal_errors(&stderr) {
115                bail!("solc failed on {}: {}", source_path.display(), stderr);
116            }
117        }
118
119        let stdout = String::from_utf8_lossy(&output.stdout);
120        serde_json::from_str(&stdout).with_context(|| {
121            format!(
122                "Failed to parse solc JSON output for: {}",
123                source_path.display()
124            )
125        })
126    }
127
128    /// Get AST for source code string (writes to temp file)
129    pub fn get_ast_for_source(&self, source: &str, _filename: &str) -> Result<SolcOutput> {
130        let tmp = tempfile::Builder::new().suffix(".sol").tempfile()?;
131        std::fs::write(tmp.path(), source)?;
132        self.get_ast_json(tmp.path())
133    }
134
135    fn cache_path() -> PathBuf {
136        let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
137        PathBuf::from(home)
138            .join(".sentri")
139            .join("solc")
140            .join("solc")
141    }
142
143    fn get_version(path: &Path) -> Result<String> {
144        let output = Command::new(path).arg("--version").output()?;
145        let stdout = String::from_utf8_lossy(&output.stdout);
146        Self::parse_version(&stdout)
147    }
148
149    fn parse_version(output: &str) -> Result<String> {
150        // Extract version from "solc, the solidity compiler programm
151        // Version: 0.8.21+commit.d9974bed"
152        output
153            .lines()
154            .find(|l| l.contains("Version:"))
155            .and_then(|l| l.split_whitespace().nth(1))
156            .map(|v| v.split('+').next().unwrap_or(v).to_string())
157            .ok_or_else(|| anyhow!("Could not parse solc version"))
158    }
159
160    fn has_fatal_errors(stderr: &str) -> bool {
161        stderr
162            .lines()
163            .any(|l| l.to_lowercase().contains("error:") && !l.contains("Warning:"))
164    }
165
166    fn download_solc() -> Result<Self> {
167        let cache_path = Self::cache_path();
168        std::fs::create_dir_all(cache_path.parent().unwrap())?;
169
170        let platform = if cfg!(target_os = "macos") {
171            "macosx-amd64"
172        } else if cfg!(target_os = "windows") {
173            "windows-amd64"
174        } else {
175            "linux-amd64"
176        };
177
178        let version = "0.8.21";
179        let url =
180            format!("https://binaries.soliditylang.org/{platform}/solc-{platform}-v{version}");
181
182        info!("Downloading solc v{} for {}...", version, platform);
183
184        let response = ureq::get(&url)
185            .call()
186            .map_err(|e| anyhow!("Failed to download solc from {}: {}", url, e))?;
187
188        let mut file = std::fs::File::create(&cache_path)?;
189        std::io::copy(&mut response.into_reader(), &mut file)?;
190
191        // Make executable on Unix
192        #[cfg(unix)]
193        {
194            use std::os::unix::fs::PermissionsExt;
195            let mut perms = std::fs::metadata(&cache_path)?.permissions();
196            perms.set_mode(0o755);
197            std::fs::set_permissions(&cache_path, perms)?;
198        }
199
200        info!("Downloaded solc to {}", cache_path.display());
201
202        Ok(Self {
203            solc_path: cache_path,
204            version: version.to_string(),
205        })
206    }
207
208    /// Get the version of the managed solc binary
209    pub fn version(&self) -> &str {
210        &self.version
211    }
212
213    /// Get the path to the solc binary
214    pub fn path(&self) -> &Path {
215        &self.solc_path
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_parse_version() {
225        let output = "solc, the solidity compiler programm\nVersion: 0.8.21+commit.d9974bed";
226        let version = SolcManager::parse_version(output).unwrap();
227        assert_eq!(version, "0.8.21");
228    }
229
230    #[test]
231    fn test_has_fatal_errors() {
232        assert!(SolcManager::has_fatal_errors("Error: Something went wrong"));
233        assert!(!SolcManager::has_fatal_errors("Warning: Be careful"));
234        assert!(SolcManager::has_fatal_errors(
235            "Warning: Be careful\nError: Failed"
236        ));
237    }
238}