use std::collections::hash_map::DefaultHasher;
use std::fs;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use std::sync::LazyLock;
use std::time::Duration;
use evalbox_sandbox::{Executor, Mount, Plan};
use tempfile::TempDir;
use crate::error::{Error, Result};
use crate::output::Output;
use crate::probe::Probe;
#[cfg(any(feature = "python", feature = "go"))]
use crate::probe_cache::ProbeCache;
use super::{GoProbe, wrap_go_code};
static PROBE_CACHE: LazyLock<ProbeCache> = LazyLock::new(ProbeCache::new);
#[derive(Debug, Clone)]
pub struct GoBuilder {
code: String,
timeout: Duration,
memory: u64,
max_pids: u32,
max_output: u64,
network: bool,
mounts: Vec<Mount>,
stdin: Option<Vec<u8>>,
env: Vec<(String, String)>,
auto_wrap: bool,
auto_import: bool,
go_mod: Option<String>,
cgo_enabled: bool,
no_cache: bool,
files: Vec<(String, Vec<u8>)>,
}
impl GoBuilder {
pub fn new(code: &str) -> Self {
Self {
code: code.to_string(),
timeout: Duration::from_secs(30),
memory: 256 * 1024 * 1024,
max_pids: 64,
max_output: 16 * 1024 * 1024,
network: false,
mounts: Vec::new(),
stdin: None,
env: Vec::new(),
auto_wrap: true,
auto_import: true,
go_mod: None,
cgo_enabled: false,
no_cache: false,
files: Vec::new(),
}
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn memory(mut self, bytes: u64) -> Self {
self.memory = bytes;
self
}
pub fn max_pids(mut self, max: u32) -> Self {
self.max_pids = max;
self
}
pub fn max_output(mut self, bytes: u64) -> Self {
self.max_output = bytes;
self
}
pub fn network(mut self, enabled: bool) -> Self {
self.network = enabled;
self
}
pub fn with(mut self, path: impl Into<PathBuf>) -> Self {
let path = path.into();
self.mounts.push(Mount::ro(&path));
self
}
pub fn with_bind(mut self, host: impl Into<PathBuf>, sandbox: impl Into<PathBuf>) -> Self {
self.mounts.push(Mount::bind(host.into(), sandbox.into()));
self
}
pub fn with_rw(mut self, path: impl Into<PathBuf>) -> Self {
let path = path.into();
self.mounts.push(Mount::rw(&path));
self
}
pub fn with_rw_bind(mut self, host: impl Into<PathBuf>, sandbox: impl Into<PathBuf>) -> Self {
self.mounts
.push(Mount::bind(host.into(), sandbox.into()).writable());
self
}
pub fn stdin(mut self, data: impl Into<Vec<u8>>) -> Self {
self.stdin = Some(data.into());
self
}
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.push((key.into(), value.into()));
self
}
pub fn auto_wrap(mut self, enabled: bool) -> Self {
self.auto_wrap = enabled;
self
}
pub fn auto_import(mut self, enabled: bool) -> Self {
self.auto_import = enabled;
self
}
pub fn go_mod(mut self, content: impl Into<String>) -> Self {
self.go_mod = Some(content.into());
self
}
pub fn cgo(mut self, enabled: bool) -> Self {
self.cgo_enabled = enabled;
self
}
pub fn no_cache(mut self, skip: bool) -> Self {
self.no_cache = skip;
self
}
pub fn file(mut self, name: impl Into<String>, content: impl Into<Vec<u8>>) -> Self {
self.files.push((name.into(), content.into()));
self
}
pub fn exec(self) -> Result<Output> {
let probe = if self.cgo_enabled {
GoProbe::with_cgo()
} else {
GoProbe::new()
};
let go_binary = probe.detect().ok_or_else(|| Error::RuntimeNotFound {
runtime: "go".to_string(),
searched: "$GOROOT, which go, /usr/local/go/bin/go".to_string(),
})?;
let runtime_info = PROBE_CACHE.get_or_probe(&probe, &go_binary)?;
let transformed_code = wrap_go_code(&self.code, self.auto_wrap, self.auto_import);
let cache_key =
compute_cache_key(&transformed_code, self.go_mod.as_deref(), self.cgo_enabled);
let cache_dir = get_go_cache_dir()?.join(&cache_key);
let cached_binary = cache_dir.join("main");
let binary_content = if !self.no_cache && cached_binary.exists() {
fs::read(&cached_binary)?
} else {
let binary = compile_in_sandbox(
&go_binary,
&transformed_code,
self.go_mod.as_deref(),
&self.files,
self.cgo_enabled,
&runtime_info,
)?;
fs::create_dir_all(&cache_dir)?;
fs::write(&cached_binary, &binary)?;
binary
};
let mut plan = Plan::new(["/work/main".to_string()])
.cwd("/work")
.executable("main", binary_content)
.timeout(self.timeout)
.memory(self.memory)
.max_pids(self.max_pids)
.max_output(self.max_output)
.network(self.network)
.mounts(self.mounts);
for (key, value) in self.env {
plan = plan.env(key, value);
}
if let Some(stdin) = self.stdin {
plan = plan.stdin(stdin);
}
let output = Executor::run(plan)?;
Ok(output.into())
}
}
fn compile_in_sandbox(
go_binary: &std::path::Path,
code: &str,
go_mod: Option<&str>,
files: &[(String, Vec<u8>)],
cgo_enabled: bool,
runtime_info: &crate::probe::RuntimeInfo,
) -> Result<Vec<u8>> {
let output_dir = TempDir::new()?;
let output_path = output_dir.path().join("main");
let mut cmd = vec![
go_binary.to_string_lossy().into_owned(),
"build".to_string(),
"-o".to_string(),
"/output/main".to_string(),
"-trimpath".to_string(),
"main.go".to_string(),
];
if !cgo_enabled {
cmd.insert(2, "-ldflags=-s -w".to_string());
}
let mut plan = Plan::new(cmd)
.cwd("/work")
.file("main.go", code.as_bytes().to_vec())
.timeout(Duration::from_secs(120)) .memory(1024 * 1024 * 1024) .env("CGO_ENABLED", if cgo_enabled { "1" } else { "0" });
if let Some(go_mod_content) = go_mod {
plan = plan.file("go.mod", go_mod_content.as_bytes().to_vec());
}
for (path, content) in files {
plan = plan.file(path, content.clone());
}
for (key, value) in &runtime_info.env {
plan = plan.env(key, value);
}
plan = plan.mounts(runtime_info.mounts.clone());
plan = plan.mount(Mount::bind(output_dir.path(), "/output").writable());
let output = Executor::run(plan)?;
if !output.success() {
return Err(Error::Compilation {
stderr: output.stderr_str(),
exit_code: Some(output.exit_code.unwrap_or(-1)),
});
}
if !output_path.exists() {
return Err(Error::Compilation {
stderr: "go build succeeded but no binary was produced".to_string(),
exit_code: None,
});
}
Ok(fs::read(&output_path)?)
}
fn get_go_cache_dir() -> Result<PathBuf> {
let cache_base = std::env::var("XDG_CACHE_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
std::env::var("HOME")
.map(|h| PathBuf::from(h).join(".cache"))
.unwrap_or_else(|_| PathBuf::from("/tmp"))
});
Ok(cache_base.join("evalbox").join("go"))
}
fn compute_cache_key(code: &str, go_mod: Option<&str>, cgo_enabled: bool) -> String {
let mut hasher = DefaultHasher::new();
code.hash(&mut hasher);
go_mod.hash(&mut hasher);
cgo_enabled.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_defaults() {
let builder = GoBuilder::new(r#"fmt.Println("hello")"#);
assert!(builder.auto_wrap);
assert!(builder.auto_import);
assert!(!builder.cgo_enabled);
assert!(!builder.network);
}
#[test]
fn test_builder_options() {
let builder = GoBuilder::new(r#"fmt.Println("hello")"#)
.timeout(Duration::from_secs(10))
.auto_wrap(false)
.cgo(true);
assert_eq!(builder.timeout, Duration::from_secs(10));
assert!(!builder.auto_wrap);
assert!(builder.cgo_enabled);
}
#[test]
fn test_cache_key_deterministic() {
let key1 = compute_cache_key("code", None, false);
let key2 = compute_cache_key("code", None, false);
assert_eq!(key1, key2);
}
#[test]
fn test_cache_key_different() {
let key1 = compute_cache_key("code1", None, false);
let key2 = compute_cache_key("code2", None, false);
assert_ne!(key1, key2);
}
}