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>, extra_args: &[String]) -> 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 mut cmd = Command::new("cargo");
203 cmd.args(["run", "--quiet"]).current_dir(&project_dir);
204
205 if !extra_args.is_empty() {
207 cmd.args(extra_args);
208 }
209
210 let status = cmd
211 .status()
212 .map_err(|e| format!("Failed to invoke cargo: {e}. Is cargo installed?"))?;
213
214 if !status.success() {
215 return Err(format!(
216 "Compilation failed (exit code: {})",
217 status.code().unwrap_or(-1)
218 ));
219 }
220
221 Ok(())
222}
223
224pub fn is_cargo_project(path: &Path) -> bool {
229 path.is_dir() && path.join("Cargo.toml").exists()
230}
231
232pub fn run_cargo_project(project_dir: &Path, extra_args: &[String]) -> Result<(), String> {
239 let cargo_toml = project_dir.join("Cargo.toml");
240 if !cargo_toml.exists() {
241 return Err(format!(
242 "No Cargo.toml found in '{}'",
243 project_dir.display()
244 ));
245 }
246
247 let display_name = read_project_name(&cargo_toml)
249 .unwrap_or_else(|| project_dir.file_name()
250 .and_then(|s| s.to_str())
251 .unwrap_or("project")
252 .to_string());
253
254 eprintln!(
255 "\x1b[1;32m Compiling\x1b[0m {} (cargo project)",
256 display_name
257 );
258
259 let mut cmd = Command::new("cargo");
260 cmd.arg("run").arg("--quiet").current_dir(project_dir);
261
262 if !extra_args.is_empty() {
264 cmd.args(extra_args);
265 }
266
267 let status = cmd
268 .status()
269 .map_err(|e| format!("Failed to invoke cargo: {e}. Is cargo installed?"))?;
270
271 if !status.success() {
272 return Err(format!(
273 "cargo run failed (exit code: {})",
274 status.code().unwrap_or(-1)
275 ));
276 }
277
278 Ok(())
279}
280
281fn read_project_name(cargo_toml: &Path) -> Option<String> {
283 let content = fs::read_to_string(cargo_toml).ok()?;
284 for line in content.lines() {
285 let trimmed = line.trim();
286 if let Some(rest) = trimmed.strip_prefix("name") {
287 let rest = rest.trim();
288 if let Some(rest) = rest.strip_prefix('=') {
289 let rest = rest.trim().trim_matches('"');
290 return Some(rest.to_string());
291 }
292 }
293 }
294 None
295}
296
297#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn parse_manifest_with_dependencies() {
305 let source = r#"//! [dependencies]
306//! rand = "0.8"
307//! serde = { version = "1.0", features = ["derive"] }
308
309use rand::Rng;
310
311fn main() {
312 println!("hello");
313}
314"#;
315 let manifest = parse_embedded_manifest(source).unwrap();
316 assert!(manifest.toml_content.contains("[dependencies]"));
317 assert!(manifest.toml_content.contains("rand = \"0.8\""));
318 assert!(manifest.toml_content.contains("serde"));
319 }
320
321 #[test]
322 fn parse_manifest_none_without_deps() {
323 let source = r#"fn main() {
324 println!("hello");
325}
326"#;
327 assert!(parse_embedded_manifest(source).is_none());
328 }
329
330 #[test]
331 fn strip_manifest_preserves_code() {
332 let source = r#"//! [dependencies]
333//! rand = "0.8"
334
335use rand::Rng;
336
337fn main() {}
338"#;
339 let cleaned = strip_manifest_comments(source);
340 assert!(!cleaned.contains("//!"));
341 assert!(cleaned.contains("use rand::Rng;"));
342 assert!(cleaned.contains("fn main()"));
343 }
344
345 #[test]
346 fn has_dependencies_detection() {
347 assert!(has_dependencies(
348 "//! [dependencies]\n//! x = \"1\"\nfn main() {}"
349 ));
350 assert!(!has_dependencies("fn main() { println!(\"hi\"); }"));
351 }
352
353 #[test]
354 fn generate_toml_includes_package() {
355 let manifest = EmbeddedManifest {
356 toml_content: "[dependencies]\nrand = \"0.8\"".to_string(),
357 };
358 let toml = generate_cargo_toml(&manifest, None);
359 assert!(toml.contains("[package]"));
360 assert!(toml.contains("edition = \"2021\""));
361 assert!(toml.contains("[dependencies]"));
362 assert!(toml.contains("rand = \"0.8\""));
363 }
364
365 #[test]
366 fn generate_toml_respects_existing_package() {
367 let manifest = EmbeddedManifest {
368 toml_content: "[package]\nname = \"my-script\"\nedition = \"2021\"\n\n[dependencies]\nrand = \"0.8\"".to_string(),
369 };
370 let toml = generate_cargo_toml(&manifest, None);
371 assert_eq!(toml.matches("[package]").count(), 1);
373 assert!(toml.contains("my-script"));
374 }
375
376 #[test]
377 fn is_cargo_project_detection() {
378 let project_root = Path::new(env!("CARGO_MANIFEST_DIR"));
380 assert!(is_cargo_project(project_root));
381
382 assert!(!is_cargo_project(Path::new("/nonexistent/fake/path")));
384
385 let cargo_toml = project_root.join("Cargo.toml");
387 assert!(!is_cargo_project(&cargo_toml));
388 }
389
390 #[test]
391 fn read_project_name_works() {
392 let project_root = Path::new(env!("CARGO_MANIFEST_DIR"));
393 let name = read_project_name(&project_root.join("Cargo.toml"));
394 assert_eq!(name, Some("virtual-rust".to_string()));
395 }
396}