use crate::commands;
use crate::dep_types::Version;
use crate::util;
use crossterm::Color;
use std::error::Error;
use std::{collections::HashMap, fmt, fs, io, path::Path, process};
#[derive(Clone, Copy, Debug)]
enum PyVers {
V3_7_4,
V3_6_9,
V3_5_6, V3_4_10,
}
impl From<Version> for PyVers {
fn from(v: Version) -> Self {
if v.major != 3 {
util::abort("Unsupported python version requested; only Python 3 is supported");
unreachable!()
}
match v.minor {
4 => Self::V3_4_10,
5 => Self::V3_5_6,
6 => Self::V3_6_9,
7 => Self::V3_7_4,
_ => {
util::abort("Unsupported python version requested; only Python >=3.4 is supported");
unreachable!()
}
}
}
}
impl ToString for PyVers {
fn to_string(&self) -> String {
match self {
Self::V3_7_4 => "3.7.4".into(),
Self::V3_6_9 => "3.6.9".into(),
Self::V3_5_6 => "3.5.6".into(),
Self::V3_4_10 => "3.4.10".into(),
}
}
}
impl PyVers {
fn to_vers(self) -> Version {
match self {
Self::V3_7_4 => Version::new(3, 7, 4),
Self::V3_6_9 => Version::new(3, 6, 9),
Self::V3_5_6 => Version::new(3, 5, 6),
Self::V3_4_10 => Version::new(3, 4, 10),
}
}
}
#[derive(Clone, Copy, Debug)]
enum Os {
Ubuntu,
Windows,
Mac,
}
fn download(py_install_path: &Path, version: &Version) {
#[cfg(target_os = "windows")]
let os = "windows";
#[cfg(target_os = "linux")]
let os = "ubuntu";
#[cfg(target_os = "macos")]
let os = "mac";
let vers_to_dl2: PyVers = (*version).into();
let vers_to_dl = vers_to_dl2.to_string();
let url = format!(
"https://github.com/David-OConnor/pybin/releases/\
download/{}/python-{}-{}.tar.xz",
vers_to_dl, vers_to_dl, os
);
let archive_path = py_install_path.join(&format!("python-{}-{}.tar.xz", vers_to_dl, os));
if !archive_path.exists() {
util::print_color(
&format!("Downloading Python {}...", vers_to_dl),
Color::Cyan,
);
let mut resp = reqwest::get(&url).expect("Problem downloading Python"); let mut out =
fs::File::create(&archive_path).expect("Failed to save downloaded package file");
io::copy(&mut resp, &mut out).expect("failed to copy content");
}
util::print_color(&format!("Installing Python {}...", vers_to_dl), Color::Cyan);
util::unpack_tar_xz(&archive_path, &py_install_path);
let extracted_path = py_install_path.join(&format!("python-{}", vers_to_dl));
fs::rename(
py_install_path.join(&format!("python-{}-{}", vers_to_dl, os)),
&extracted_path,
)
.expect("Problem renaming extracted Python folder");
}
#[derive(Debug)]
pub struct AliasError {
pub details: String,
}
impl Error for AliasError {
fn description(&self) -> &str {
&self.details
}
}
impl fmt::Display for AliasError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.details)
}
}
pub fn prompt_alias(aliases: &[(String, Version)]) -> (String, Version) {
util::print_color("Found multiple compatible Python aliases. Please enter the number associated with the one you'd like to use for this project:", Color::Magenta);
for (i, (alias, version)) in aliases.iter().enumerate() {
println!("{}: {} version: {}", i + 1, alias, version.to_string())
}
let mut mapping = HashMap::new();
for (i, alias) in aliases.iter().enumerate() {
mapping.insert(i + 1, alias);
}
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("Unable to read user input for version");
let input = input
.chars()
.next()
.expect("Problem reading input")
.to_string();
let (alias, version) = mapping
.get(
&input
.parse::<usize>()
.expect("Enter the number associated with the Python alias."),
)
.expect(
"Can't find the Python alias associated with that number. Is it in the list above?",
);
(alias.to_string(), *version)
}
pub fn find_py_aliases(version: &Version) -> Vec<(String, Version)> {
let possible_aliases = &[
"python3.10",
"python3.9",
"python3.8",
"python3.7",
"python3.6",
"python3.5",
"python3.4",
"python3.3",
"python3.2",
"python3.1",
"python3",
"python",
"python2",
];
let mut result = Vec::new();
for alias in possible_aliases {
if let Some(v) = commands::find_py_version(alias) {
if v.major == version.major && v.minor == version.minor {
result.push((alias.to_string(), v));
}
}
}
result
}
fn find_installed_versions() -> Vec<Version> {
#[cfg(target_os = "windows")]
let py_name = "python";
#[cfg(target_os = "linux")]
let py_name = "python3";
#[cfg(target_os = "macos")]
let py_name = "python3";
let python_installs_dir = dirs::home_dir()
.expect("Problem finding home directory")
.join(".python-installs");
if !&python_installs_dir.exists() && fs::create_dir(&python_installs_dir).is_err(){
util::abort("Problem creating ~/python-installs directory")
}
let mut result = vec![];
for entry in python_installs_dir
.read_dir()
.expect("Can't open python installs path")
{
if let Ok(entry) = entry {
if !entry.path().is_dir() {
continue;
}
if let Some(v) =
commands::find_py_version(entry.path().join("bin").join(py_name).to_str().unwrap())
{
result.push(v);
}
}
}
result
}
pub fn create_venv(cfg_v: &Version, pyypackages_dir: &Path) -> Version {
let python_installs_dir = dirs::home_dir()
.expect("Problem finding home directory")
.join(".python-installs");
#[cfg(target_os = "windows")]
let py_name = "python";
#[cfg(target_os = "linux")]
let py_name = "python3";
#[cfg(target_os = "macos")]
let py_name = "python3";
let mut alias = None;
let mut alias_path = None;
let mut py_ver = None;
let installed_versions = find_installed_versions();
for iv in installed_versions.iter() {
if iv.major == cfg_v.major && iv.minor == cfg_v.minor {
let folder_name = format!("python-{}", iv.to_string2());
alias_path = Some(
python_installs_dir
.join(folder_name)
.join("bin")
.join(py_name),
);
py_ver = Some(*iv);
break;
}
}
if py_ver.is_none() {
let aliases = find_py_aliases(cfg_v);
match aliases.len() {
0 => (),
1 => {
let r = aliases[0].clone();
alias = Some(r.0);
py_ver = Some(r.1);
}
_ => {
let r = prompt_alias(&aliases);
alias = Some(r.0);
py_ver = Some(r.1);
}
};
}
if py_ver.is_none() {
download(&python_installs_dir, cfg_v);
let py_ver2: PyVers = (*cfg_v).into();
py_ver = Some(py_ver2.to_vers());
let folder_name = format!("python-{}", py_ver2.to_string());
alias_path = Some(
python_installs_dir
.join(folder_name)
.join("bin")
.join(py_name),
);
}
let py_ver = py_ver.expect("missing Python version");
let vers_path = pyypackages_dir.join(format!("{}.{}", py_ver.major, py_ver.minor));
let lib_path = vers_path.join("lib");
if !lib_path.exists() {
fs::create_dir_all(&lib_path).expect("Problem creating __pypackages__ directory");
}
println!("Setting up Python environment...");
if let Some(alias) = alias {
if commands::create_venv(&alias, &lib_path, ".venv").is_err() {
util::abort("Problem creating virtual environment");
}
} else if let Some(alias_path) = alias_path {
if commands::create_venv2(&alias_path, &lib_path, ".venv").is_err() {
util::abort("Problem creating virtual environment");
}
}
let python_name;
let pip_name;
#[cfg(target_os = "windows")]
{
python_name = "python.exe";
pip_name = "pip.exe";
}
#[cfg(target_os = "linux")]
{
python_name = "python";
pip_name = "pip";
}
#[cfg(target_os = "macos")]
{
python_name = "python";
pip_name = "pip";
}
let bin_path = util::find_bin_path(&vers_path);
util::wait_for_dirs(&[bin_path.join(python_name), bin_path.join(pip_name)])
.expect("Timed out waiting for venv to be created.");
process::Command::new(bin_path.join("python"))
.args(&["-m", "pip", "install", "--quiet", "wheel"])
.status()
.expect("Problem installing `wheel`");
py_ver
}