use colored::*;
use std::collections::hash_map::DefaultHasher;
use std::fs;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::process::Command;
use super::output;
fn find_project_root() -> Option<PathBuf> {
let mut current = std::env::current_dir().ok()?;
loop {
if current.join("Cargo.toml").exists() {
return Some(current);
}
if !current.pop() {
return None;
}
}
}
pub fn run_file(file: &str, extra_deps: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
let file_path = Path::new(file);
if !file_path.exists() {
return Err(format!("File not found: {}", file).into());
}
if !file_path.extension().map_or(false, |ext| ext == "rs") {
return Err("File must be a .rs file".into());
}
output::header("Oxidite Runner v2.3.3");
output::debug(&format!("Executing file: {}", file_path.display()));
if let Some(project_root) = find_project_root() {
output::info("Running in project mode");
output::debug(&format!("Project root: {}", project_root.display()));
run_in_project(&project_root, file_path, extra_deps)
} else {
output::info("Running in standalone mode");
run_standalone(file_path, extra_deps)
}
}
fn run_in_project(
project_root: &Path,
file_path: &Path,
extra_deps: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
let src_bin = project_root.join("src").join("bin");
fs::create_dir_all(&src_bin)?;
let bin_name = file_path
.file_stem()
.and_then(|s| s.to_str())
.ok_or("Could not determine binary name")?;
let target_path = src_bin.join(format!("{}.rs", bin_name));
let should_copy = if target_path.exists() {
fs::canonicalize(file_path)? != fs::canonicalize(&target_path)?
} else {
true
};
if should_copy {
fs::copy(file_path, &target_path)?;
output::info(&format!(
"Copied {} -> {}",
file_path.display(),
target_path.display()
));
}
if let Some(deps) = extra_deps {
output::debug(&format!("Adding dependencies: {}", deps));
add_dependencies(project_root, deps)?;
}
output::step(&format!("Running {} in project context", bin_name.bold()));
let status = Command::new("cargo")
.arg("run")
.arg("--bin")
.arg(bin_name)
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.stdin(std::process::Stdio::inherit())
.current_dir(project_root)
.status()?;
if !status.success() {
return Err(format!("Script exited with status: {}", status).into());
}
output::success("Completed successfully");
Ok(())
}
fn run_standalone(
file_path: &Path,
extra_deps: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
let mut deps: Vec<(String, String)> = vec![
("oxidite".into(), "\"2.3.3\"".into()),
("tokio".into(), "{ version = \"1\", features = [\"full\"] }".into()),
];
if let Some(extra) = extra_deps {
for dep in extra.split(',') {
let dep = dep.trim();
if !dep.is_empty() {
if let Some((name, version)) = dep.split_once('@') {
deps.push((name.trim().into(), format!("\"{}\"", version.trim())));
} else {
deps.push((dep.into(), "\"*\"".into()));
}
}
}
}
let deps_hash = {
let mut hasher = DefaultHasher::new();
for (name, version) in &deps {
name.hash(&mut hasher);
version.hash(&mut hasher);
}
format!("{:016x}", hasher.finish())
};
let cache_dir = dirs_cache().join(&deps_hash);
let src_dir = cache_dir.join("src");
fs::create_dir_all(&src_dir)?;
let cargo_toml_content = format!(
r#"[package]
name = "oxidite-script"
version = "0.1.0"
edition = "2021"
[dependencies]
{}
"#,
deps.iter()
.map(|(name, version)| format!("{} = {}", name, version))
.collect::<Vec<_>>()
.join("\n")
);
let cargo_toml_path = cache_dir.join("Cargo.toml");
write_if_changed(&cargo_toml_path, &cargo_toml_content)?;
let script_content = fs::read_to_string(file_path)?;
let main_rs_path = src_dir.join("main.rs");
write_if_changed(&main_rs_path, &script_content)?;
output::info(&format!("Running {} (standalone)", file_path.display()));
output::debug(&format!("Cache: {}", cache_dir.display()));
let status = Command::new("cargo")
.arg("run")
.arg("--quiet")
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.stdin(std::process::Stdio::inherit())
.current_dir(&cache_dir)
.status()?;
if !status.success() {
return Err(format!("Script exited with status: {}", status).into());
}
output::success("Completed successfully");
Ok(())
}
fn dirs_cache() -> PathBuf {
if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
PathBuf::from(xdg).join("oxidite-run")
} else if let Ok(home) = std::env::var("HOME") {
PathBuf::from(home).join(".cache").join("oxidite-run")
} else {
std::env::temp_dir().join("oxidite-run-cache")
}
}
fn write_if_changed(path: &Path, content: &str) -> Result<bool, std::io::Error> {
if path.exists() {
if let Ok(existing) = fs::read_to_string(path) {
if existing == content {
return Ok(false); }
}
}
fs::write(path, content)?;
Ok(true) }
fn add_dependencies(project_root: &Path, deps: &str) -> Result<(), Box<dyn std::error::Error>> {
let cargo_toml_path = project_root.join("Cargo.toml");
let mut cargo_toml = fs::read_to_string(&cargo_toml_path)?;
let mut needs_update = false;
for dep in deps.split(',') {
let dep = dep.trim();
if dep.is_empty() {
continue;
}
if !cargo_toml.contains(&format!("{} = ", dep)) && !cargo_toml.contains(&format!("{}=", dep))
{
if cargo_toml.contains("[dependencies]") {
let lines: Vec<&str> = cargo_toml.lines().collect();
let mut new_lines: Vec<String> = Vec::new();
let mut in_deps = false;
let mut inserted = false;
let dep_line = format!("{} = \"*\"", dep);
for line in &lines {
if line.trim().starts_with('[') && in_deps && !inserted {
new_lines.push(dep_line.clone());
inserted = true;
in_deps = false;
}
if line.trim() == "[dependencies]" {
in_deps = true;
}
new_lines.push(line.to_string());
}
if !inserted {
new_lines.push(dep_line);
}
cargo_toml = new_lines.join("\n");
needs_update = true;
}
}
}
if needs_update {
fs::write(cargo_toml_path, cargo_toml)?;
output::info(&format!("Added dependencies: {}", deps));
}
Ok(())
}