use std::collections::HashMap;
use std::process::{Command, Stdio};
use std::sync::OnceLock;
use crate::error::CargoScriptError;
static SUPPORTS_STABLE: OnceLock<bool> = OnceLock::new();
pub fn stable_cargo_script_supported() -> bool {
*SUPPORTS_STABLE.get_or_init(|| {
let out = Command::new("cargo")
.args(["script", "--version"])
.stderr(Stdio::null())
.output();
match out {
Ok(o) if o.status.success() => {
let stdout = String::from_utf8_lossy(&o.stdout);
stdout
.lines()
.next()
.map(is_builtin_cargo_version_line)
.unwrap_or(false)
}
_ => false,
}
})
}
pub fn is_builtin_cargo_version_line(line: &str) -> bool {
let trimmed = line.trim();
let mut parts = trimmed.splitn(2, char::is_whitespace);
let head = parts.next().unwrap_or("");
let rest = parts.next().unwrap_or("").trim_start();
head == "cargo" && rest.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false)
}
pub fn cargo_available() -> bool {
Command::new("cargo")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
pub fn nightly_available() -> bool {
let out = Command::new("rustup")
.args(["toolchain", "list"])
.output();
match out {
Ok(o) => String::from_utf8_lossy(&o.stdout).contains("nightly"),
Err(_) => false,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CargoScriptInvocation {
Stable,
Nightly,
}
pub fn build_cargo_script_argv(
invocation: CargoScriptInvocation,
path: &str,
extra_args: &[String],
) -> (&'static str, Vec<String>) {
let mut args: Vec<String> = match invocation {
CargoScriptInvocation::Stable => vec!["script".into(), path.to_string()],
CargoScriptInvocation::Nightly => {
vec!["+nightly".into(), "-Zscript".into(), path.to_string()]
}
};
if !extra_args.is_empty() {
args.push("--".into());
args.extend(extra_args.iter().cloned());
}
("cargo", args)
}
pub fn execute_cargo_script(
script_name: &str,
path: &str,
extra_args: &[String],
env_vars: &HashMap<String, String>,
) -> Result<(), CargoScriptError> {
if !cargo_available() {
return Err(CargoScriptError::CargoScriptNotAvailable {
suggestion: " Install Rust toolchain from https://rustup.rs/".to_string(),
});
}
let invocation = if stable_cargo_script_supported() {
CargoScriptInvocation::Stable
} else if nightly_available() {
CargoScriptInvocation::Nightly
} else {
return Err(CargoScriptError::CargoScriptNotAvailable {
suggestion:
" cargo script (RFC 3502) requires either a stable cargo with the \
feature stabilized, or `rustup toolchain install nightly`."
.to_string(),
});
};
let (program, full_args) = build_cargo_script_argv(invocation, path, extra_args);
let base_args: Vec<String> = match invocation {
CargoScriptInvocation::Stable => vec!["script".into(), path.to_string()],
CargoScriptInvocation::Nightly => {
vec!["+nightly".into(), "-Zscript".into(), path.to_string()]
}
};
let mut cmd = Command::new(program);
cmd.args(&full_args);
cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
for (k, v) in env_vars {
cmd.env(k, v);
}
let mut child = cmd
.spawn()
.map_err(|e| CargoScriptError::ExecutionError {
script: script_name.to_string(),
command: format!("{} {}", program, base_args.join(" ")),
source: e,
})?;
let status = child.wait().map_err(|e| CargoScriptError::ExecutionError {
script: script_name.to_string(),
command: format!("{} {}", program, base_args.join(" ")),
source: e,
})?;
if !status.success() {
return Err(CargoScriptError::ExecutionError {
script: script_name.to_string(),
command: format!("{} {}", program, base_args.join(" ")),
source: std::io::Error::new(
std::io::ErrorKind::Other,
format!("cargo script exited with status: {}", status),
),
});
}
Ok(())
}
pub fn looks_like_cargo_script(path: &str) -> bool {
path.ends_with(".rs") && std::path::Path::new(path).exists()
}