use crate::CompilerConfig;
use crate::config::OptimizationLevel;
use crate::parser::Parser;
use crate::resolver::{Resolver, find_stdlib};
use crate::stdlib_embed;
use sha2::{Digest, Sha256};
use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};
pub fn get_cache_dir() -> Option<PathBuf> {
if let Ok(xdg_cache) = std::env::var("XDG_CACHE_HOME") {
let path = PathBuf::from(xdg_cache);
if path.is_absolute() {
return Some(path.join("seq"));
}
}
if let Ok(home) = std::env::var("HOME") {
return Some(PathBuf::from(home).join(".cache").join("seq"));
}
None
}
pub fn compute_cache_key(
source_path: &Path,
source_files: &[PathBuf],
embedded_modules: &[String],
) -> Result<String, String> {
let mut hasher = Sha256::new();
let main_content =
fs::read(source_path).map_err(|e| format!("Failed to read source file: {}", e))?;
hasher.update(&main_content);
let mut sorted_files: Vec<_> = source_files.iter().collect();
sorted_files.sort();
for file in sorted_files {
if file != source_path {
let content = fs::read(file)
.map_err(|e| format!("Failed to read included file '{}': {}", file.display(), e))?;
hasher.update(&content);
}
}
let mut sorted_modules: Vec<_> = embedded_modules.iter().collect();
sorted_modules.sort();
for module_name in sorted_modules {
if let Some(content) = stdlib_embed::get_stdlib(module_name) {
hasher.update(content.as_bytes());
}
}
let hash = hasher.finalize();
Ok(hex::encode(hash))
}
fn strip_shebang(source: &str) -> std::borrow::Cow<'_, str> {
if source.starts_with("#!") {
if let Some(newline_pos) = source.find('\n') {
let mut result = String::with_capacity(source.len());
result.push('#');
result.push_str(&" ".repeat(newline_pos - 1));
result.push_str(&source[newline_pos..]);
std::borrow::Cow::Owned(result)
} else {
std::borrow::Cow::Borrowed("#")
}
} else {
std::borrow::Cow::Borrowed(source)
}
}
fn prepare_script(source_path: &Path) -> Result<PathBuf, String> {
let source_path = source_path.canonicalize().map_err(|e| {
format!(
"Failed to find source file '{}': {}",
source_path.display(),
e
)
})?;
let cache_dir =
get_cache_dir().ok_or_else(|| "Could not determine cache directory".to_string())?;
let source_raw = fs::read_to_string(&source_path)
.map_err(|e| format!("Failed to read source file: {}", e))?;
let source = strip_shebang(&source_raw);
let mut parser = Parser::new(&source);
let program = parser.parse()?;
let (source_files, embedded_modules) = if !program.includes.is_empty() {
let stdlib_path = find_stdlib();
let mut resolver = Resolver::new(stdlib_path);
let result = resolver.resolve(&source_path, program)?;
(result.source_files, result.embedded_modules)
} else {
(vec![source_path.clone()], Vec::new())
};
let cache_key = compute_cache_key(&source_path, &source_files, &embedded_modules)?;
let cached_binary = cache_dir.join(&cache_key);
if cached_binary.exists() {
return Ok(cached_binary);
}
fs::create_dir_all(&cache_dir)
.map_err(|e| format!("Failed to create cache directory: {}", e))?;
let pid = std::process::id();
let temp_binary = cache_dir.join(format!("{}.{}.tmp", cache_key, pid));
let temp_source = cache_dir.join(format!("{}.{}.seq", cache_key, pid));
fs::write(&temp_source, source.as_ref())
.map_err(|e| format!("Failed to write temp source: {}", e))?;
let config = CompilerConfig::new().with_optimization_level(OptimizationLevel::O0);
let compile_result =
crate::compile_file_with_config(&temp_source, &temp_binary, false, &config);
fs::remove_file(&temp_source).ok();
if let Err(e) = compile_result {
fs::remove_file(&temp_binary).ok();
return Err(e);
}
if fs::rename(&temp_binary, &cached_binary).is_err() {
if cached_binary.exists() {
fs::remove_file(&temp_binary).ok();
} else {
fs::remove_file(&temp_binary).ok();
return Err("Failed to cache compiled binary".to_string());
}
}
Ok(cached_binary)
}
#[cfg(unix)]
pub fn run_script(
source_path: &Path,
args: &[OsString],
) -> Result<std::convert::Infallible, String> {
use std::os::unix::process::CommandExt;
let cached_binary = prepare_script(source_path)?;
let err = std::process::Command::new(&cached_binary).args(args).exec();
Err(format!("Failed to execute script: {}", err))
}
#[cfg(not(unix))]
pub fn run_script(
source_path: &Path,
args: &[OsString],
) -> Result<std::convert::Infallible, String> {
let cached_binary = prepare_script(source_path)?;
let status = std::process::Command::new(&cached_binary)
.args(args)
.status()
.map_err(|e| format!("Failed to execute script: {}", e))?;
std::process::exit(status.code().unwrap_or(1));
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
#[serial]
fn test_get_cache_dir_with_xdg() {
let orig_xdg = std::env::var("XDG_CACHE_HOME").ok();
let orig_home = std::env::var("HOME").ok();
unsafe {
std::env::set_var("XDG_CACHE_HOME", "/tmp/test-xdg-cache");
}
let cache_dir = get_cache_dir();
assert!(cache_dir.is_some());
assert_eq!(cache_dir.unwrap(), PathBuf::from("/tmp/test-xdg-cache/seq"));
unsafe {
match orig_xdg {
Some(v) => std::env::set_var("XDG_CACHE_HOME", v),
None => std::env::remove_var("XDG_CACHE_HOME"),
}
match orig_home {
Some(v) => std::env::set_var("HOME", v),
None => std::env::remove_var("HOME"),
}
}
}
#[test]
#[serial]
fn test_get_cache_dir_fallback_to_home() {
let orig_xdg = std::env::var("XDG_CACHE_HOME").ok();
let orig_home = std::env::var("HOME").ok();
unsafe {
std::env::remove_var("XDG_CACHE_HOME");
std::env::set_var("HOME", "/tmp/test-home");
}
let cache_dir = get_cache_dir();
assert!(cache_dir.is_some());
assert_eq!(
cache_dir.unwrap(),
PathBuf::from("/tmp/test-home/.cache/seq")
);
unsafe {
match orig_xdg {
Some(v) => std::env::set_var("XDG_CACHE_HOME", v),
None => std::env::remove_var("XDG_CACHE_HOME"),
}
match orig_home {
Some(v) => std::env::set_var("HOME", v),
None => std::env::remove_var("HOME"),
}
}
}
#[test]
fn test_compute_cache_key_deterministic() {
use tempfile::tempdir;
let temp = tempdir().unwrap();
let source = temp.path().join("test.seq");
fs::write(&source, ": main ( -- Int ) 42 ;").unwrap();
let key1 = compute_cache_key(&source, std::slice::from_ref(&source), &[]).unwrap();
let key2 = compute_cache_key(&source, std::slice::from_ref(&source), &[]).unwrap();
assert_eq!(key1, key2);
assert_eq!(key1.len(), 64); }
#[test]
fn test_compute_cache_key_changes_with_content() {
use tempfile::tempdir;
let temp = tempdir().unwrap();
let source = temp.path().join("test.seq");
fs::write(&source, ": main ( -- Int ) 42 ;").unwrap();
let key1 = compute_cache_key(&source, std::slice::from_ref(&source), &[]).unwrap();
fs::write(&source, ": main ( -- Int ) 43 ;").unwrap();
let key2 = compute_cache_key(&source, std::slice::from_ref(&source), &[]).unwrap();
assert_ne!(key1, key2);
}
#[test]
fn test_compute_cache_key_includes_embedded_modules() {
use tempfile::tempdir;
let temp = tempdir().unwrap();
let source = temp.path().join("test.seq");
fs::write(&source, ": main ( -- Int ) 42 ;").unwrap();
let key1 = compute_cache_key(&source, std::slice::from_ref(&source), &[]).unwrap();
let key2 = compute_cache_key(
&source,
std::slice::from_ref(&source),
&["imath".to_string()],
)
.unwrap();
assert_ne!(key1, key2);
}
#[test]
fn test_strip_shebang_with_shebang() {
let source = "#!/usr/bin/env seqc\n: main ( -- Int ) 42 ;";
let stripped = strip_shebang(source);
assert!(stripped.starts_with('#'));
assert!(!stripped.starts_with("#!"));
assert!(stripped.contains(": main ( -- Int ) 42 ;"));
assert_eq!(stripped.matches('\n').count(), source.matches('\n').count());
}
#[test]
fn test_strip_shebang_without_shebang() {
let source = ": main ( -- Int ) 42 ;";
let stripped = strip_shebang(source);
assert_eq!(stripped.as_ref(), source);
}
#[test]
fn test_strip_shebang_with_comment() {
let source = "# This is a comment\n: main ( -- Int ) 42 ;";
let stripped = strip_shebang(source);
assert_eq!(stripped.as_ref(), source);
}
#[test]
fn test_strip_shebang_only_shebang() {
let source = "#!/usr/bin/env seqc";
let stripped = strip_shebang(source);
assert_eq!(stripped.as_ref(), "#");
}
}