use std::{
env::{self, consts::OS},
path::{Path, PathBuf},
};
#[allow(clippy::useless_attribute)]
#[allow(unused_imports)]
use crate::{
errors::{HuakError, HuakResult},
package::python::PythonPackage,
utils::{
path::search_parents_for_filepath,
shell::{get_shell_name, get_shell_path, get_shell_source_command},
},
};
const DEFAULT_SEARCH_STEPS: usize = 5;
pub(crate) const DEFAULT_VENV_NAME: &str = ".venv";
pub(crate) const DEFAULT_PYTHON_ALIAS: &str = "python";
pub(crate) const BIN_NAME: &str = "bin";
pub(crate) const WINDOWS_BIN_NAME: &str = "Scripts";
pub(crate) const HUAK_VENV_ENV_VAR: &str = "HUAK_VENV_ACTIVE";
#[derive(Clone)]
pub struct Venv {
pub path: PathBuf,
}
impl Default for Venv {
fn default() -> Venv {
let cwd = match env::current_dir() {
Err(_) => Path::new(".").to_path_buf(),
Ok(p) => p,
};
Venv {
path: cwd.join(DEFAULT_VENV_NAME),
}
}
}
impl Venv {
pub fn new(path: PathBuf) -> Venv {
Venv { path }
}
pub fn from_path(from: &Path) -> HuakResult<Venv> {
let names = vec![".venv", "venv"];
for name in &names {
if let Ok(Some(path)) =
search_parents_for_filepath(from, name, DEFAULT_SEARCH_STEPS)
{
return Ok(Venv::new(path));
};
}
Err(HuakError::VenvNotFound)
}
pub fn name(&self) -> HuakResult<&str> {
let name = crate::utils::path::parse_filename(self.path.as_path())?;
Ok(name)
}
pub fn activate(&self) -> HuakResult<()> {
if env::var(HUAK_VENV_ENV_VAR).is_ok() {
return Err(HuakError::VenvActive);
}
let script = self.get_activation_script()?;
if !script.exists() {
return Err(HuakError::VenvNotFound);
}
let source_command = get_shell_source_command()?;
let activation_command =
format!("{} {}", source_command, script.display());
env::set_var(HUAK_VENV_ENV_VAR, "1");
self.spawn_pseudo_terminal(&activation_command)?;
Ok(())
}
#[cfg(unix)]
fn spawn_pseudo_terminal(
&self,
activation_command: &str,
) -> HuakResult<()> {
let shell_path = get_shell_path()?;
let mut new_shell = expectrl::spawn(shell_path)?;
let mut stdin = expectrl::stream::stdin::Stdin::open()?;
new_shell.send_line(activation_command)?;
if let Some((cols, rows)) = terminal_size::terminal_size() {
new_shell
.set_window_size(cols.0, rows.0)
.map_err(|e| HuakError::InternalError(e.to_string()))?;
}
new_shell.interact(&mut stdin, std::io::stdout()).spawn()?;
stdin.close()?;
Ok(())
}
#[cfg(windows)]
fn spawn_pseudo_terminal(
&self,
activation_command: &str,
) -> HuakResult<()> {
let shell_path = get_shell_path()?;
let mut sh = expectrl::spawn(shell_path)?;
let stdin = expectrl::stream::stdin::Stdin::open()?;
sh.send_line(&activation_command)?;
sh.interact(stdin, std::io::stdout()).spawn()?;
let stdin = expectrl::stream::stdin::Stdin::open()?;
stdin.close()?;
Ok(())
}
fn get_activation_script(&self) -> HuakResult<PathBuf> {
let shell_name = get_shell_name()?;
let suffix = match shell_name.as_str() {
"fish" => ".fish",
"csh" | "tcsh" => ".csh",
"powershell" | "pwsh" => ".ps1",
"cmd" => ".bat",
"nu" => ".nu",
_ => "",
};
let path = self
.bin_path()
.join(Path::new(&("activate".to_owned() + suffix)));
Ok(path)
}
pub fn create(&self) -> HuakResult<()> {
if self.path.exists() {
return Ok(());
}
let from = match self.path.parent() {
Some(p) => p,
_ => {
return Err(HuakError::ConfigurationError(
"Invalid venv path, no parent directory.".into(),
))
}
};
let name = self.name()?;
let args = ["-m", "venv", name];
println!("Creating venv {}", self.path.display());
let py = match crate::env::system::find_python_binary_path(None) {
Ok(it) => it,
Err(e) => {
match e {
HuakError::PythonNotFound => {
DEFAULT_PYTHON_ALIAS.to_string()
}
_ => return Err(e),
}
}
};
crate::utils::command::run_command(&py, &args, from)?;
Ok(())
}
pub fn python_binary(&self) -> HuakResult<String> {
let path = crate::env::system::find_python_binary_path(Some(
self.path.to_path_buf(),
))?;
Ok(path)
}
pub fn bin_path(&self) -> PathBuf {
match OS {
"windows" => self.path.join(WINDOWS_BIN_NAME),
_ => self.path.join(BIN_NAME),
}
}
pub fn module_path(&self, module: &str) -> HuakResult<PathBuf> {
let bin_path = self.bin_path();
let mut path = bin_path.join(module);
if OS != "windows" {
return Ok(path);
}
match path.set_extension("exe") {
true => Ok(path),
false => Err(HuakError::InternalError(format!(
"failed to create path for {module}"
))),
}
}
pub fn exec_module(
&self,
module: &str,
args: &[&str],
from: &Path,
) -> HuakResult<()> {
self.create()?;
let module_path = self.module_path(module)?;
let package = match PythonPackage::from(module) {
Ok(it) => it,
Err(_) => {
return Err(HuakError::PyPackageInitError(module.to_string()))
}
};
if !module_path.exists() {
self.install_package(&package)?;
}
let module_path = crate::utils::path::to_string(module_path.as_path())?;
crate::utils::command::run_command(module_path, args, from)?;
Ok(())
}
pub fn exec_command(&self, command: &str) -> HuakResult<()> {
self.create()?;
let source_command = get_shell_source_command()?;
let script = self.get_activation_script()?;
let activation_command =
format!("{} {}", source_command, script.display());
let shell_path = get_shell_path()?;
let cwd = env::current_dir()?;
crate::utils::command::run_command(
&shell_path,
&["-c", &format!("{} && {}", activation_command, command)],
cwd.as_path(),
)?;
Ok(())
}
pub fn install_package(&self, package: &PythonPackage) -> HuakResult<()> {
let cwd = env::current_dir()?;
let module_str = &package.string();
let args = ["install", module_str];
let module = "pip";
self.exec_module(module, &args, cwd.as_path())?;
Ok(())
}
pub fn uninstall_package(&self, name: &str) -> HuakResult<()> {
let cwd = env::current_dir()?;
let module = "pip";
let args = ["uninstall", name, "-y"];
self.exec_module(module, &args, cwd.as_path())?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use tempfile::tempdir;
use super::*;
#[test]
fn default() {
let venv = Venv::default();
assert!(venv.path.ends_with(DEFAULT_VENV_NAME));
}
#[test]
fn from() {
let directory = tempdir().unwrap().into_path();
let first_venv = Venv::new(directory.join(".venv"));
first_venv.create().unwrap();
let second_venv = Venv::from_path(&directory).unwrap();
assert!(second_venv.path.exists());
assert!(second_venv.module_path("pip").unwrap().exists());
assert_eq!(first_venv.path, second_venv.path);
}
}