1use crate::CompilerConfig;
17use crate::config::OptimizationLevel;
18use crate::parser::Parser;
19use crate::resolver::{Resolver, find_stdlib};
20use crate::stdlib_embed;
21use sha2::{Digest, Sha256};
22use std::ffi::OsString;
23use std::fs;
24use std::path::{Path, PathBuf};
25
26pub fn get_cache_dir() -> Option<PathBuf> {
28 if let Ok(xdg_cache) = std::env::var("XDG_CACHE_HOME") {
30 let path = PathBuf::from(xdg_cache);
31 if path.is_absolute() {
32 return Some(path.join("seq"));
33 }
34 }
35
36 if let Ok(home) = std::env::var("HOME") {
38 return Some(PathBuf::from(home).join(".cache").join("seq"));
39 }
40
41 None
42}
43
44pub fn compute_cache_key(
52 source_path: &Path,
53 source_files: &[PathBuf],
54 embedded_modules: &[String],
55) -> Result<String, String> {
56 let mut hasher = Sha256::new();
57
58 let main_content =
60 fs::read(source_path).map_err(|e| format!("Failed to read source file: {}", e))?;
61 hasher.update(&main_content);
62
63 let mut sorted_files: Vec<_> = source_files.iter().collect();
65 sorted_files.sort();
66 for file in sorted_files {
67 if file != source_path {
68 let content = fs::read(file)
70 .map_err(|e| format!("Failed to read included file '{}': {}", file.display(), e))?;
71 hasher.update(&content);
72 }
73 }
74
75 let mut sorted_modules: Vec<_> = embedded_modules.iter().collect();
77 sorted_modules.sort();
78 for module_name in sorted_modules {
79 if let Some(content) = stdlib_embed::get_stdlib(module_name) {
80 hasher.update(content.as_bytes());
81 }
82 }
83
84 let hash = hasher.finalize();
85 Ok(hex::encode(hash))
86}
87
88fn strip_shebang(source: &str) -> std::borrow::Cow<'_, str> {
93 if source.starts_with("#!") {
94 if let Some(newline_pos) = source.find('\n') {
96 let mut result = String::with_capacity(source.len());
97 result.push('#');
98 result.push_str(&" ".repeat(newline_pos - 1));
99 result.push_str(&source[newline_pos..]);
100 std::borrow::Cow::Owned(result)
101 } else {
102 std::borrow::Cow::Borrowed("#")
104 }
105 } else {
106 std::borrow::Cow::Borrowed(source)
107 }
108}
109
110fn prepare_script(source_path: &Path) -> Result<PathBuf, String> {
119 let source_path = source_path.canonicalize().map_err(|e| {
121 format!(
122 "Failed to find source file '{}': {}",
123 source_path.display(),
124 e
125 )
126 })?;
127
128 let cache_dir =
130 get_cache_dir().ok_or_else(|| "Could not determine cache directory".to_string())?;
131
132 let source_raw = fs::read_to_string(&source_path)
134 .map_err(|e| format!("Failed to read source file: {}", e))?;
135 let source = strip_shebang(&source_raw);
136
137 let mut parser = Parser::new(&source);
138 let program = parser.parse()?;
139
140 let (source_files, embedded_modules) = if !program.includes.is_empty() {
142 let stdlib_path = find_stdlib();
143 let mut resolver = Resolver::new(stdlib_path);
144 let result = resolver.resolve(&source_path, program)?;
145 (result.source_files, result.embedded_modules)
146 } else {
147 (vec![source_path.clone()], Vec::new())
148 };
149
150 let cache_key = compute_cache_key(&source_path, &source_files, &embedded_modules)?;
152 let cached_binary = cache_dir.join(&cache_key);
153
154 if cached_binary.exists() {
156 return Ok(cached_binary);
157 }
158
159 fs::create_dir_all(&cache_dir)
161 .map_err(|e| format!("Failed to create cache directory: {}", e))?;
162
163 let pid = std::process::id();
165 let temp_binary = cache_dir.join(format!("{}.{}.tmp", cache_key, pid));
166 let temp_source = cache_dir.join(format!("{}.{}.seq", cache_key, pid));
167
168 fs::write(&temp_source, source.as_ref())
170 .map_err(|e| format!("Failed to write temp source: {}", e))?;
171
172 let config = CompilerConfig::new().with_optimization_level(OptimizationLevel::O0);
174
175 let compile_result =
176 crate::compile_file_with_config(&temp_source, &temp_binary, false, &config);
177
178 fs::remove_file(&temp_source).ok();
180
181 if let Err(e) = compile_result {
183 fs::remove_file(&temp_binary).ok();
185 return Err(e);
186 }
187
188 if fs::rename(&temp_binary, &cached_binary).is_err() {
191 if cached_binary.exists() {
193 fs::remove_file(&temp_binary).ok();
195 } else {
196 fs::remove_file(&temp_binary).ok();
198 return Err("Failed to cache compiled binary".to_string());
199 }
200 }
201
202 Ok(cached_binary)
203}
204
205#[cfg(unix)]
210pub fn run_script(
211 source_path: &Path,
212 args: &[OsString],
213) -> Result<std::convert::Infallible, String> {
214 use std::os::unix::process::CommandExt;
215
216 let cached_binary = prepare_script(source_path)?;
217
218 let err = std::process::Command::new(&cached_binary).args(args).exec();
220
221 Err(format!("Failed to execute script: {}", err))
223}
224
225#[cfg(not(unix))]
227pub fn run_script(
228 source_path: &Path,
229 args: &[OsString],
230) -> Result<std::convert::Infallible, String> {
231 let cached_binary = prepare_script(source_path)?;
232
233 let status = std::process::Command::new(&cached_binary)
235 .args(args)
236 .status()
237 .map_err(|e| format!("Failed to execute script: {}", e))?;
238
239 std::process::exit(status.code().unwrap_or(1));
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use serial_test::serial;
246
247 #[test]
248 #[serial]
249 fn test_get_cache_dir_with_xdg() {
250 let orig_xdg = std::env::var("XDG_CACHE_HOME").ok();
252 let orig_home = std::env::var("HOME").ok();
253
254 unsafe {
257 std::env::set_var("XDG_CACHE_HOME", "/tmp/test-xdg-cache");
259 }
260 let cache_dir = get_cache_dir();
261 assert!(cache_dir.is_some());
262 assert_eq!(cache_dir.unwrap(), PathBuf::from("/tmp/test-xdg-cache/seq"));
263
264 unsafe {
267 match orig_xdg {
268 Some(v) => std::env::set_var("XDG_CACHE_HOME", v),
269 None => std::env::remove_var("XDG_CACHE_HOME"),
270 }
271 match orig_home {
272 Some(v) => std::env::set_var("HOME", v),
273 None => std::env::remove_var("HOME"),
274 }
275 }
276 }
277
278 #[test]
279 #[serial]
280 fn test_get_cache_dir_fallback_to_home() {
281 let orig_xdg = std::env::var("XDG_CACHE_HOME").ok();
283 let orig_home = std::env::var("HOME").ok();
284
285 unsafe {
288 std::env::remove_var("XDG_CACHE_HOME");
290 std::env::set_var("HOME", "/tmp/test-home");
291 }
292 let cache_dir = get_cache_dir();
293 assert!(cache_dir.is_some());
294 assert_eq!(
295 cache_dir.unwrap(),
296 PathBuf::from("/tmp/test-home/.cache/seq")
297 );
298
299 unsafe {
302 match orig_xdg {
303 Some(v) => std::env::set_var("XDG_CACHE_HOME", v),
304 None => std::env::remove_var("XDG_CACHE_HOME"),
305 }
306 match orig_home {
307 Some(v) => std::env::set_var("HOME", v),
308 None => std::env::remove_var("HOME"),
309 }
310 }
311 }
312
313 #[test]
314 fn test_compute_cache_key_deterministic() {
315 use tempfile::tempdir;
316
317 let temp = tempdir().unwrap();
318 let source = temp.path().join("test.seq");
319 fs::write(&source, ": main ( -- Int ) 42 ;").unwrap();
320
321 let key1 = compute_cache_key(&source, std::slice::from_ref(&source), &[]).unwrap();
322 let key2 = compute_cache_key(&source, std::slice::from_ref(&source), &[]).unwrap();
323
324 assert_eq!(key1, key2);
325 assert_eq!(key1.len(), 64); }
327
328 #[test]
329 fn test_compute_cache_key_changes_with_content() {
330 use tempfile::tempdir;
331
332 let temp = tempdir().unwrap();
333 let source = temp.path().join("test.seq");
334
335 fs::write(&source, ": main ( -- Int ) 42 ;").unwrap();
336 let key1 = compute_cache_key(&source, std::slice::from_ref(&source), &[]).unwrap();
337
338 fs::write(&source, ": main ( -- Int ) 43 ;").unwrap();
339 let key2 = compute_cache_key(&source, std::slice::from_ref(&source), &[]).unwrap();
340
341 assert_ne!(key1, key2);
342 }
343
344 #[test]
345 fn test_compute_cache_key_includes_embedded_modules() {
346 use tempfile::tempdir;
347
348 let temp = tempdir().unwrap();
349 let source = temp.path().join("test.seq");
350 fs::write(&source, ": main ( -- Int ) 42 ;").unwrap();
351
352 let key1 = compute_cache_key(&source, std::slice::from_ref(&source), &[]).unwrap();
353 let key2 = compute_cache_key(
354 &source,
355 std::slice::from_ref(&source),
356 &["imath".to_string()],
357 )
358 .unwrap();
359
360 assert_ne!(key1, key2);
361 }
362
363 #[test]
364 fn test_strip_shebang_with_shebang() {
365 let source = "#!/usr/bin/env seqc\n: main ( -- Int ) 42 ;";
366 let stripped = strip_shebang(source);
367 assert!(stripped.starts_with('#'));
369 assert!(!stripped.starts_with("#!"));
370 assert!(stripped.contains(": main ( -- Int ) 42 ;"));
372 assert_eq!(stripped.matches('\n').count(), source.matches('\n').count());
374 }
375
376 #[test]
377 fn test_strip_shebang_without_shebang() {
378 let source = ": main ( -- Int ) 42 ;";
379 let stripped = strip_shebang(source);
380 assert_eq!(stripped.as_ref(), source);
382 }
383
384 #[test]
385 fn test_strip_shebang_with_comment() {
386 let source = "# This is a comment\n: main ( -- Int ) 42 ;";
387 let stripped = strip_shebang(source);
388 assert_eq!(stripped.as_ref(), source);
390 }
391
392 #[test]
393 fn test_strip_shebang_only_shebang() {
394 let source = "#!/usr/bin/env seqc";
395 let stripped = strip_shebang(source);
396 assert_eq!(stripped.as_ref(), "#");
398 }
399}