use std::path::{Path, PathBuf};
use std::process::Command;
use rust_i18n::t;
use crate::core::VirtualenvService;
use crate::error::{Result, ScoopError};
use crate::output::Output;
use crate::{paths, validate};
pub fn execute(_output: &Output, env_name: &str, command: &[String]) -> Result<()> {
validate::validate_env_name(env_name)?;
let service = VirtualenvService::auto()?;
if !service.exists(env_name)? {
return Err(ScoopError::VirtualenvNotFound {
name: env_name.to_string(),
});
}
if command.is_empty() {
return Err(ScoopError::InvalidArgument {
message: t!("run.empty_command").to_string(),
});
}
let venv_path = service.get_path(env_name)?;
let bin_dir = paths::virtualenv_bin(env_name)?;
let mut cmd = build_command(&venv_path, &bin_dir, env_name, command);
let status = match cmd.status() {
Ok(s) => s,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(ScoopError::ExecutableNotFound {
exe: command[0].clone(),
env: env_name.to_string(),
});
}
Err(e) => return Err(ScoopError::Io(e)),
};
let code = status
.code()
.or_else(|| signal_exit_code(&status))
.unwrap_or(1);
std::process::exit(code);
}
fn build_command(venv_path: &Path, bin_dir: &Path, env_name: &str, command: &[String]) -> Command {
let program = resolve_program(bin_dir, &command[0]);
let mut cmd = Command::new(program);
cmd.args(&command[1..])
.env("VIRTUAL_ENV", venv_path)
.env("SCOOP_ACTIVE", env_name)
.env_remove("PYTHONHOME");
let mut path_parts: Vec<PathBuf> = vec![bin_dir.to_path_buf()];
if let Some(existing) = std::env::var_os("PATH") {
path_parts.extend(std::env::split_paths(&existing));
}
if let Ok(joined) = std::env::join_paths(path_parts) {
cmd.env("PATH", joined);
}
cmd
}
fn resolve_program(bin_dir: &Path, program: &str) -> PathBuf {
if program.contains('/') || program.contains('\\') {
return PathBuf::from(program);
}
paths::find_executable_in(bin_dir, program).unwrap_or_else(|| PathBuf::from(program))
}
#[cfg(unix)]
fn signal_exit_code(status: &std::process::ExitStatus) -> Option<i32> {
use std::os::unix::process::ExitStatusExt;
status.signal().map(|s| 128 + s)
}
#[cfg(not(unix))]
fn signal_exit_code(_status: &std::process::ExitStatus) -> Option<i32> {
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::with_temp_scoop_home;
use serial_test::serial;
use std::path::PathBuf;
fn fake_command(name: &str, args: &[&str]) -> Vec<String> {
std::iter::once(name.to_string())
.chain(args.iter().map(|s| s.to_string()))
.collect()
}
#[test]
fn build_command_sets_env_vars() {
let venv = PathBuf::from("/envs/myenv");
let bin = PathBuf::from("/envs/myenv/bin");
let cmd = build_command(&venv, &bin, "myenv", &fake_command("python", &["x.py"]));
let envs: Vec<_> = cmd.get_envs().collect();
let lookup = |key: &str| -> Option<Option<&std::ffi::OsStr>> {
envs.iter()
.find(|(k, _)| k.to_str() == Some(key))
.map(|(_, v)| *v)
};
assert_eq!(
lookup("VIRTUAL_ENV")
.and_then(|v| v)
.and_then(|v| v.to_str()),
Some("/envs/myenv")
);
assert_eq!(
lookup("SCOOP_ACTIVE")
.and_then(|v| v)
.and_then(|v| v.to_str()),
Some("myenv")
);
assert_eq!(lookup("PYTHONHOME"), Some(None));
let path_val = lookup("PATH")
.and_then(|v| v)
.and_then(|v| v.to_str().map(str::to_string))
.expect("PATH set");
assert!(path_val.starts_with("/envs/myenv/bin"));
}
#[test]
fn build_command_passes_through_args() {
let venv = PathBuf::from("/envs/myenv");
let bin = PathBuf::from("/envs/myenv/bin");
let cmd = build_command(
&venv,
&bin,
"myenv",
&fake_command("pytest", &["-vv", "--tb=short"]),
);
let args: Vec<_> = cmd
.get_args()
.map(|a| a.to_string_lossy().to_string())
.collect();
assert_eq!(args, vec!["-vv", "--tb=short"]);
}
#[test]
fn resolve_program_uses_bin_dir_for_bare_name() {
let dir = tempfile::tempdir().unwrap();
let exe = dir.path().join("python");
std::fs::write(&exe, b"").unwrap();
assert_eq!(resolve_program(dir.path(), "python"), exe);
}
#[test]
fn resolve_program_passes_through_path_unchanged() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(
resolve_program(dir.path(), "/usr/bin/python3"),
PathBuf::from("/usr/bin/python3")
);
}
#[test]
fn resolve_program_falls_back_to_bare_name_when_missing() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(resolve_program(dir.path(), "ls"), PathBuf::from("ls"));
}
#[test]
#[serial]
fn execute_returns_invalid_env_name() {
with_temp_scoop_home(|temp_dir| {
std::fs::create_dir_all(temp_dir.path().join("virtualenvs")).unwrap();
let output = Output::new(0, true, true, false);
let err = execute(&output, "../bad", &["python".to_string()]).unwrap_err();
assert!(matches!(err, ScoopError::InvalidEnvName { .. }));
});
}
#[test]
#[serial]
fn execute_returns_not_found_for_missing_env() {
with_temp_scoop_home(|temp_dir| {
std::fs::create_dir_all(temp_dir.path().join("virtualenvs")).unwrap();
let output = Output::new(0, true, true, false);
let err = execute(&output, "ghost", &["python".to_string()]).unwrap_err();
assert!(matches!(err, ScoopError::VirtualenvNotFound { .. }));
});
}
#[test]
#[serial]
fn execute_rejects_empty_command() {
with_temp_scoop_home(|temp_dir| {
let bin = temp_dir
.path()
.join("virtualenvs")
.join("myenv")
.join("bin");
std::fs::create_dir_all(&bin).unwrap();
let output = Output::new(0, true, true, false);
let err = execute(&output, "myenv", &[]).unwrap_err();
assert!(matches!(err, ScoopError::InvalidArgument { .. }));
});
}
}