virtual_rust/
cargo_runner.rs1use std::fs;
34use std::hash::{Hash, Hasher};
35use std::path::{Path, PathBuf};
36use std::process::Command;
37
38pub struct EmbeddedManifest {
42 pub toml_content: String,
44}
45
46pub fn parse_embedded_manifest(source: &str) -> Option<EmbeddedManifest> {
51 let mut toml_lines = Vec::new();
52 let mut found_section = false;
53
54 for line in source.lines() {
55 let trimmed = line.trim();
56
57 if let Some(rest) = trimmed.strip_prefix("//!") {
58 let content = rest.strip_prefix(' ').unwrap_or(rest);
60 if content.starts_with('[') {
61 found_section = true;
62 }
63 toml_lines.push(content.to_string());
64 } else if trimmed.is_empty() {
65 if found_section {
67 toml_lines.push(String::new());
68 }
69 } else {
70 break;
72 }
73 }
74
75 if !found_section {
76 return None;
77 }
78
79 Some(EmbeddedManifest {
80 toml_content: toml_lines.join("\n"),
81 })
82}
83
84pub fn has_dependencies(source: &str) -> bool {
86 parse_embedded_manifest(source).is_some()
87}
88
89fn strip_manifest_comments(source: &str) -> String {
96 let mut result_lines: Vec<&str> = Vec::new();
97 let mut in_header = true;
98
99 for line in source.lines() {
100 if in_header {
101 let trimmed = line.trim();
102 if trimmed.starts_with("//!") || trimmed.is_empty() {
103 continue; }
105 in_header = false;
106 }
107 result_lines.push(line);
108 }
109
110 result_lines.join("\n")
111}
112
113fn project_cache_dir(source_path: Option<&Path>) -> Result<PathBuf, String> {
120 let base = std::env::temp_dir().join("virtual-rust-cache");
121
122 let project_name = match source_path {
123 Some(path) => {
124 let mut hasher = std::collections::hash_map::DefaultHasher::new();
125 path.canonicalize()
126 .unwrap_or_else(|_| path.to_path_buf())
127 .hash(&mut hasher);
128 format!("project_{:x}", hasher.finish())
129 }
130 None => "project_anonymous".to_string(),
131 };
132
133 let dir = base.join(project_name);
134 fs::create_dir_all(&dir).map_err(|e| format!("Failed to create cache directory: {e}"))?;
135 Ok(dir)
136}
137
138fn generate_cargo_toml(manifest: &EmbeddedManifest, source_path: Option<&Path>) -> String {
140 let name = source_path
141 .and_then(|p| p.file_stem())
142 .and_then(|s| s.to_str())
143 .unwrap_or("virtual-rust-script")
144 .to_lowercase()
145 .replace(|c: char| !c.is_alphanumeric() && c != '-' && c != '_', "-");
146
147 let has_package = manifest.toml_content.contains("[package]");
148
149 let mut toml = String::new();
150
151 if !has_package {
152 toml.push_str(&format!(
153 "[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n"
154 ));
155 }
156
157 toml.push_str(&manifest.toml_content);
158 toml.push('\n');
159 toml
160}
161
162pub fn run_with_cargo(source: &str, source_path: Option<&Path>) -> Result<(), String> {
174 let manifest =
175 parse_embedded_manifest(source).ok_or("No embedded dependency manifest found")?;
176
177 let project_dir = project_cache_dir(source_path)?;
179 let src_dir = project_dir.join("src");
180 fs::create_dir_all(&src_dir).map_err(|e| format!("Failed to create src directory: {e}"))?;
181
182 let cargo_toml = generate_cargo_toml(&manifest, source_path);
184 fs::write(project_dir.join("Cargo.toml"), &cargo_toml)
185 .map_err(|e| format!("Failed to write Cargo.toml: {e}"))?;
186
187 let clean_source = strip_manifest_comments(source);
189 fs::write(src_dir.join("main.rs"), &clean_source)
190 .map_err(|e| format!("Failed to write main.rs: {e}"))?;
191
192 eprintln!(
194 "\x1b[1;32m Compiling\x1b[0m {} with cargo (dependencies detected)",
195 source_path
196 .and_then(|p| p.file_name())
197 .and_then(|s| s.to_str())
198 .unwrap_or("script")
199 );
200
201 let status = Command::new("cargo")
203 .args(["run", "--quiet"])
204 .current_dir(&project_dir)
205 .status()
206 .map_err(|e| format!("Failed to invoke cargo: {e}. Is cargo installed?"))?;
207
208 if !status.success() {
209 return Err(format!(
210 "Compilation failed (exit code: {})",
211 status.code().unwrap_or(-1)
212 ));
213 }
214
215 Ok(())
216}
217
218pub fn is_cargo_project(path: &Path) -> bool {
223 path.is_dir() && path.join("Cargo.toml").exists()
224}
225
226pub fn run_cargo_project(project_dir: &Path, extra_args: &[String]) -> Result<(), String> {
233 let cargo_toml = project_dir.join("Cargo.toml");
234 if !cargo_toml.exists() {
235 return Err(format!(
236 "No Cargo.toml found in '{}'",
237 project_dir.display()
238 ));
239 }
240
241 let display_name = read_project_name(&cargo_toml)
243 .unwrap_or_else(|| project_dir.file_name()
244 .and_then(|s| s.to_str())
245 .unwrap_or("project")
246 .to_string());
247
248 eprintln!(
249 "\x1b[1;32m Compiling\x1b[0m {} (cargo project)",
250 display_name
251 );
252
253 let mut cmd = Command::new("cargo");
254 cmd.arg("run").arg("--quiet").current_dir(project_dir);
255
256 if !extra_args.is_empty() {
258 cmd.arg("--");
259 cmd.args(extra_args);
260 }
261
262 let status = cmd
263 .status()
264 .map_err(|e| format!("Failed to invoke cargo: {e}. Is cargo installed?"))?;
265
266 if !status.success() {
267 return Err(format!(
268 "cargo run failed (exit code: {})",
269 status.code().unwrap_or(-1)
270 ));
271 }
272
273 Ok(())
274}
275
276fn read_project_name(cargo_toml: &Path) -> Option<String> {
278 let content = fs::read_to_string(cargo_toml).ok()?;
279 for line in content.lines() {
280 let trimmed = line.trim();
281 if let Some(rest) = trimmed.strip_prefix("name") {
282 let rest = rest.trim();
283 if let Some(rest) = rest.strip_prefix('=') {
284 let rest = rest.trim().trim_matches('"');
285 return Some(rest.to_string());
286 }
287 }
288 }
289 None
290}
291
292#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn parse_manifest_with_dependencies() {
300 let source = r#"//! [dependencies]
301//! rand = "0.8"
302//! serde = { version = "1.0", features = ["derive"] }
303
304use rand::Rng;
305
306fn main() {
307 println!("hello");
308}
309"#;
310 let manifest = parse_embedded_manifest(source).unwrap();
311 assert!(manifest.toml_content.contains("[dependencies]"));
312 assert!(manifest.toml_content.contains("rand = \"0.8\""));
313 assert!(manifest.toml_content.contains("serde"));
314 }
315
316 #[test]
317 fn parse_manifest_none_without_deps() {
318 let source = r#"fn main() {
319 println!("hello");
320}
321"#;
322 assert!(parse_embedded_manifest(source).is_none());
323 }
324
325 #[test]
326 fn strip_manifest_preserves_code() {
327 let source = r#"//! [dependencies]
328//! rand = "0.8"
329
330use rand::Rng;
331
332fn main() {}
333"#;
334 let cleaned = strip_manifest_comments(source);
335 assert!(!cleaned.contains("//!"));
336 assert!(cleaned.contains("use rand::Rng;"));
337 assert!(cleaned.contains("fn main()"));
338 }
339
340 #[test]
341 fn has_dependencies_detection() {
342 assert!(has_dependencies(
343 "//! [dependencies]\n//! x = \"1\"\nfn main() {}"
344 ));
345 assert!(!has_dependencies("fn main() { println!(\"hi\"); }"));
346 }
347
348 #[test]
349 fn generate_toml_includes_package() {
350 let manifest = EmbeddedManifest {
351 toml_content: "[dependencies]\nrand = \"0.8\"".to_string(),
352 };
353 let toml = generate_cargo_toml(&manifest, None);
354 assert!(toml.contains("[package]"));
355 assert!(toml.contains("edition = \"2021\""));
356 assert!(toml.contains("[dependencies]"));
357 assert!(toml.contains("rand = \"0.8\""));
358 }
359
360 #[test]
361 fn generate_toml_respects_existing_package() {
362 let manifest = EmbeddedManifest {
363 toml_content: "[package]\nname = \"my-script\"\nedition = \"2021\"\n\n[dependencies]\nrand = \"0.8\"".to_string(),
364 };
365 let toml = generate_cargo_toml(&manifest, None);
366 assert_eq!(toml.matches("[package]").count(), 1);
368 assert!(toml.contains("my-script"));
369 }
370
371 #[test]
372 fn is_cargo_project_detection() {
373 let project_root = Path::new(env!("CARGO_MANIFEST_DIR"));
375 assert!(is_cargo_project(project_root));
376
377 assert!(!is_cargo_project(Path::new("/nonexistent/fake/path")));
379
380 let cargo_toml = project_root.join("Cargo.toml");
382 assert!(!is_cargo_project(&cargo_toml));
383 }
384
385 #[test]
386 fn read_project_name_works() {
387 let project_root = Path::new(env!("CARGO_MANIFEST_DIR"));
388 let name = read_project_name(&project_root.join("Cargo.toml"));
389 assert_eq!(name, Some("virtual-rust".to_string()));
390 }
391}