use crate::util::print_color;
use crate::{commands, dep_types::Version, util};
use flate2::read::GzDecoder;
use regex::Regex;
use ring::digest;
use std::{fs, io, io::BufRead, path::Path, process::Command};
use tar::Archive;
use termcolor::Color;
#[derive(Copy, Clone, Debug)]
pub enum PackageType {
Wheel,
Source,
}
fn sha256_digest<R: io::Read>(mut reader: R) -> Result<digest::Digest, std::io::Error> {
let mut context = digest::Context::new(&digest::SHA256);
let mut buffer = [0; 1024];
loop {
let count = reader.read(&mut buffer)?;
if count == 0 {
break;
}
context.update(&buffer[..count]);
}
Ok(context.finish())
}
fn replace_distutils(setup_path: &Path) {
let setup_text = if let Ok(t) = fs::read_to_string(setup_path) {
t
} else {
util::abort(&format!(
"Can't find setup.py in this source distribution\
path: {:?}. This could mean there are no suitable wheels for this package,\
and there's a problem with its setup.py.",
setup_path
));
unreachable!()
};
let re = Regex::new(r"distutils.core").unwrap();
let new_text = re.replace_all(&setup_text, "setuptools");
if new_text != setup_text {
fs::write(setup_path, new_text.to_string())
.expect("Problem replacing `distutils.core` with `setuptools` in `setup.py`");
}
}
fn remove_scripts(scripts: &[String], scripts_path: &Path) {
for entry in
fs::read_dir(scripts_path).expect("Problem reading dist directory when removing scripts")
{
let entry = entry.unwrap();
if !entry.file_type().unwrap().is_file() {
continue;
}
let data = fs::read_to_string(entry.path()).unwrap();
for script in scripts {
if data.contains(&format!("from {}", script)) {
fs::remove_file(entry.path()).expect("Problem removing console script");
util::print_color(&format!("Removed console script {}:", script), Color::Green);
}
}
}
}
pub fn make_script(path: &Path, name: &str, module: &str, func: &str) {
let contents = format!(
r"import re
import sys
from {} import {}
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
sys.exit({}())",
module, func, func
);
fs::write(path, contents)
.unwrap_or_else(|_| util::abort(&format!("Problem creating script file for {}", name)));
}
pub fn setup_scripts(name: &str, version: &Version, lib_path: &Path, entry_pt_path: &Path) {
let mut scripts = vec![];
let mut dist_info_path = lib_path.join(format!("{}-{}.dist-info", name, version.to_string()));
if !dist_info_path.exists() && (version.patch == Some(0) || version.patch == None) {
dist_info_path = lib_path.join(format!("{}-{}.dist-info", name, version.to_string_med()));
if !dist_info_path.exists() && (version.minor == Some(0) || version.minor == None) {
dist_info_path =
lib_path.join(format!("{}-{}.dist-info", name, version.to_string_short()));
}
}
if let Ok(ep_file) = fs::File::open(&dist_info_path.join("entry_points.txt")) {
let mut in_scripts_section = false;
for line in io::BufReader::new(ep_file).lines().flatten() {
if line.contains("[console_scripts]") {
in_scripts_section = true;
continue;
}
if line.starts_with('[') {
break;
}
if in_scripts_section && !line.is_empty() {
scripts.push(line.clone().replace(" ", ""));
}
}
}
if !entry_pt_path.exists() && fs::create_dir(&entry_pt_path).is_err() {
util::abort("Problem creating script path")
}
for new_script in scripts {
let re = Regex::new(r"^(.*?)\s*=\s*(.*?):(.*)$").unwrap();
if let Some(caps) = re.captures(&new_script) {
let name = caps.get(1).unwrap().as_str();
let module = caps.get(2).unwrap().as_str();
let func = caps.get(3).unwrap().as_str();
let path = entry_pt_path.join(name);
make_script(&path, name, module, func);
if name != "wheel" {
util::print_color(&format!("Added a console script: {}", name), Color::Green);
}
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn download_and_install_package(
name: &str,
version: &Version,
url: &str,
filename: &str,
expected_digest: &str,
paths: &util::Paths,
package_type: PackageType,
rename: &Option<(u32, String)>,
) -> Result<(), reqwest::Error> {
if !paths.lib.exists() {
fs::create_dir_all(&paths.lib).expect("Problem creating lib directory");
}
if !paths.cache.exists() {
fs::create_dir_all(&paths.cache).expect("Problem creating cache directory");
}
let archive_path = paths.cache.join(filename);
if !archive_path.exists() {
let mut resp = reqwest::get(url)?; let mut out =
fs::File::create(&archive_path).expect("Failed to save downloaded package file");
if let Err(e) = io::copy(&mut resp, &mut out) {
fs::remove_file(&archive_path).expect("Problem removing the broken file");
util::abort(&format!("Problem downloading the package archive: {:?}", e));
}
}
let file = util::open_archive(&archive_path);
let reader = io::BufReader::new(&file);
let file_digest = sha256_digest(reader).unwrap_or_else(|_| {
util::abort(&format!("Problem reading hash for {}", filename));
unreachable!()
});
let file_digest_str = data_encoding::HEXUPPER.encode(file_digest.as_ref());
if file_digest_str.to_lowercase() != expected_digest.to_lowercase() {
util::print_color(&format!("Hash failed for {}. Expected: {}, Actual: {}. Continue with installation anyway? (yes / no)", filename, expected_digest.to_lowercase(), file_digest_str.to_lowercase()), Color::Red);
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("Unable to read user input Hash fail decision");
let input = input
.chars()
.next()
.expect("Problem reading input")
.to_string();
if input.to_lowercase().contains('y') {
} else {
util::abort("Exiting due to failed hash");
}
}
let archive_file = util::open_archive(&archive_path);
let rename = rename
.as_ref()
.map(|(_, new)| (name.to_owned(), new.to_owned()));
match package_type {
PackageType::Wheel => {
util::extract_zip(&archive_file, &paths.lib, &rename);
}
PackageType::Source => {
if archive_path.extension().unwrap() == "bz2" {
util::abort(&format!(
"Extracting source packages in the `.bz2` format isn't supported \
at this time: {:?}",
&archive_path
));
}
let tar = GzDecoder::new(&archive_file);
let mut archive = Archive::new(tar);
archive.set_preserve_mtime(false);
match archive.entries() {
Ok(entries) => {
for file in entries {
match file {
Ok(mut f) => {
match f.unpack_in(&paths.lib) {
Ok(_) => (),
Err(e) => {
print_color(
&format!(
"Problem unpacking file {:?}: {:?}",
f.path(),
e
),
Color::Yellow, );
let f_path =
f.path().expect("Problem getting path from archive");
let filename =
f_path.file_name().expect("Problem getting file name");
if filename
.to_str()
.unwrap()
.to_lowercase()
.contains("readme")
&& fs::File::create(&paths.lib.join(f.path().unwrap()))
.is_err()
{
print_color(
"Problem creating dummy readme",
Color::Yellow, );
}
}
};
}
Err(e) => {
println!(
"Problem opening the tar.gz archive: {:?}: {:?}, checking if it's a zip...",
&archive_file, e
);
util::extract_zip(&archive_file, &paths.lib, &None);
}
}
}
}
Err(e) => {
println!(
"Problem opening the tar.gz archive: {:?}: {:?}, checking if it's a zip...",
&archive_file, e
);
util::extract_zip(&archive_file, &paths.lib, &None);
}
}
let re = Regex::new(r"^(.*?)(?:\.tar\.gz|\.zip)$").unwrap();
let folder_name = re
.captures(filename)
.expect("Problem matching extracted folder name")
.get(1)
.unwrap_or_else(|| {
util::abort(&format!(
"Unable to find extracted folder name: {}",
filename
));
unreachable!()
})
.as_str();
let extracted_parent = paths.lib.join(folder_name);
replace_distutils(&extracted_parent.join("setup.py"));
#[cfg(target_os = "windows")]
{
let output = Command::new(paths.bin.join("python"))
.current_dir(&extracted_parent)
.args(&["setup.py", "bdist_wheel"])
.output()
.unwrap_or_else(|_| {
panic!(
"Problem running setup.py bdist_wheel in folder: {:?}. Py path: {:?}",
&extracted_parent,
paths.bin.join("python")
)
});
util::check_command_output_with(&output, |s| {
panic!(
"running setup.py bdist_wheel in folder {:?}. Py path: {:?}: {}",
&extracted_parent,
paths.bin.join("python"),
s
);
});
}
#[cfg(target_os = "linux")]
{
let output = Command::new("python3")
.current_dir(&extracted_parent)
.args(&["setup.py", "bdist_wheel"])
.output()
.unwrap_or_else(|_| {
panic!(
"Problem running setup.py bdist_wheel in folder: {:?}. Py path: {:?}",
&extracted_parent,
paths.bin.join("python")
)
});
util::check_command_output_with(&output, |s| {
panic!(
"running setup.py bdist_wheel in folder {:?}. Py path: {:?}: {}",
&extracted_parent,
paths.bin.join("python"),
s
);
});
}
#[cfg(target_os = "macos")]
{
let output = Command::new("python3")
.current_dir(&extracted_parent)
.args(&["setup.py", "bdist_wheel"])
.output()
.unwrap_or_else(|_| {
panic!(
"Problem running setup.py bdist_wheel in folder: {:?}. Py path: {:?}",
&extracted_parent,
paths.bin.join("python")
)
});
util::check_command_output_with(&output, |s| {
panic!(
"running setup.py bdist_wheel in folder {:?}. Py path: {:?}: {}",
&extracted_parent,
paths.bin.join("python"),
s
);
});
}
let dist_path = &extracted_parent.join("dist");
if !dist_path.exists() {
#[cfg(target_os = "windows")]
let error = &format!(
"Problem building {} from source. \
This may occur if a package that requires compiling has no wheels available \
for Windows, and the system is missing dependencies required to compile it, \
or if on WSL and installing to a mounted directory.",
name
);
#[cfg(target_os = "linux")]
let error = format!(
"Problem building {} from source. \
This may occur if a package that requires compiling has no wheels available \
for this OS and this system is missing dependencies required to compile it.\
Try running `pip install --upgrade wheel`, then try again",
name
);
#[cfg(target_os = "macos")]
let error = format!(
"Problem building {} from source. \
This may occur if a package that requires compiling has no wheels available \
for this OS and this system is missing dependencies required to compile it.
Try running `pip install --upgrade wheel`, then try again",
name
);
util::abort(&error);
}
let built_wheel_filename = util::find_first_file(dist_path)
.file_name()
.expect("Unable to find built wheel filename")
.to_str()
.unwrap()
.to_owned();
let moved_path = paths.lib.join(&built_wheel_filename);
let options = fs_extra::file::CopyOptions::new();
fs_extra::file::move_file(dist_path.join(&built_wheel_filename), &moved_path, &options)
.expect("Problem copying wheel built from source");
let file_created = fs::File::open(&moved_path).expect("Can't find created wheel.");
util::extract_zip(&file_created, &paths.lib, &rename);
if fs::remove_file(moved_path).is_err() {
util::abort(&format!(
"Problem removing this downloaded package: {:?}",
&built_wheel_filename
));
}
if fs::remove_dir_all(&extracted_parent).is_err() {
util::abort(&format!(
"Problem removing parent folder of this downloaded package: {:?}",
&extracted_parent
));
}
}
}
setup_scripts(name, version, &paths.lib, &paths.entry_pt);
Ok(())
}
pub fn uninstall(name_ins: &str, vers_ins: &Version, lib_path: &Path) {
#[cfg(target_os = "windows")]
println!(
"Uninstalling {}: {}...",
name_ins,
vers_ins.to_string_color()
);
#[cfg(target_os = "linux")]
println!("🗑 Uninstalling {}: {}...", name_ins, vers_ins.to_string());
#[cfg(target_os = "macos")]
println!("🗑 Uninstalling {}: {}...", name_ins, vers_ins.to_string());
let mut dist_info_path =
lib_path.join(format!("{}-{}.dist-info", name_ins, vers_ins.to_string()));
if !dist_info_path.exists() && (vers_ins.patch == Some(0) || vers_ins.patch == None) {
dist_info_path = lib_path.join(format!(
"{}-{}.dist-info",
name_ins,
vers_ins.to_string_med()
));
if !dist_info_path.exists() && (vers_ins.minor == Some(0) || vers_ins.minor == None) {
dist_info_path = lib_path.join(format!(
"{}-{}.dist-info",
name_ins,
vers_ins.to_string_short()
));
}
}
let egg_info_path = lib_path.join(format!("{}-{}.egg-info", name_ins, vers_ins.to_string()));
let folder_names = match fs::File::open(dist_info_path.join("top_level.txt")) {
Ok(f) => {
let mut names = vec![];
for line in io::BufReader::new(f).lines().flatten() {
names.push(line);
}
names
}
Err(_) => vec![name_ins.to_lowercase()],
};
for folder_name in folder_names {
if fs::remove_dir_all(lib_path.join(&folder_name)).is_err() {
if fs::remove_file(lib_path.join(&format!("{}.py", folder_name))).is_err() {
print_color(
&format!("Problem uninstalling {} {}", name_ins, vers_ins.to_string(),),
Color::Red, );
}
}
}
let meta_folder_removed = if fs::remove_dir_all(egg_info_path).is_ok() {
true
} else {
fs::remove_dir_all(dist_info_path).is_ok()
};
if !meta_folder_removed {
print_color(
&format!(
"Problem uninstalling metadata for {}: {}",
name_ins,
vers_ins.to_string_color(),
),
Color::Red, );
}
fs::remove_dir_all(lib_path.join(format!("{}-{}.data", name_ins, vers_ins.to_string())))
.unwrap_or(());
remove_scripts(&[name_ins.into()], &lib_path.join("../bin"));
}
pub fn rename_package_files(top_path: &Path, old: &str, new: &str) {
for entry in fs::read_dir(top_path).expect("Problem reading renamed package path") {
let entry = entry.expect("Problem reading file while renaming");
let path = entry.path();
if path.is_dir() {
rename_package_files(&path, old, new);
continue;
}
if !path.is_file() {
continue;
}
if path.extension().is_none() || path.extension().unwrap() != "py" {
continue;
}
let mut data = fs::read_to_string(&path).expect("Problem reading file while renaming");
data = data.replace(
&format!("from {} import", old),
&format!("from {} import", new),
);
data = data.replace(&format!("from {}.", old), &format!("from {}.", new));
data = data.replace(&format!("import {}", old), &format!("import {}", new));
data = data.replace(&format!("{}.", old), &format!("{}.", new));
fs::write(path, data).expect("Problem writing file while renaming");
}
}
pub fn rename_metadata(path: &Path, _old: &str, new: &str) {
let top_file = path.join("top_level.txt");
let top_data = new.to_owned();
fs::write(top_file, top_data).expect("Problem writing file while renaming");
}
pub fn download_and_install_git(
name: &str,
url: &str,
git_path: &Path,
paths: &util::Paths,
) -> util::Metadata {
if !git_path.exists() {
fs::create_dir_all(git_path).expect("Problem creating git path");
}
let folder_name = util::standardize_name(name); if !&git_path.join(&folder_name).exists() && commands::download_git_repo(url, git_path).is_err()
{
util::abort(&format!("Problem cloning this repo: {}", url));
}
let output = Command::new(paths.bin.join("python"))
.current_dir(&git_path.join(&folder_name))
.args(&["setup.py", "bdist_wheel"])
.output()
.expect("Problem running setup.py bdist_wheel");
util::check_command_output(&output, "running setup.py bdist_wheel");
let archive_path = util::find_first_file(&git_path.join(folder_name).join("dist"));
let filename = archive_path
.file_name()
.expect("Problem pulling filename from archive path");
let options = fs_extra::file::CopyOptions::new();
fs_extra::file::move_file(&archive_path, paths.lib.join(&filename), &options)
.expect("Problem moving the wheel.");
let archive_path = &paths.lib.join(&filename);
let archive_file = util::open_archive(archive_path);
util::extract_zip(&archive_file, &paths.lib, &None);
let re = Regex::new(r"^(.*?)-(.*?)-.*$").unwrap();
let dist_info = if let Some(caps) = re.captures(filename.to_str().unwrap()) {
format!(
"{}-{}.dist-info",
caps.get(1).unwrap().as_str(),
caps.get(2).unwrap().as_str()
)
} else {
util::abort("Unable to find the dist info path from wheel filename");
unreachable!();
};
let metadata = util::parse_metadata(&paths.lib.join(dist_info).join("METADATA"));
setup_scripts(name, &metadata.version, &paths.lib, &paths.entry_pt);
if fs::remove_file(&archive_path).is_err() {
util::abort(&format!(
"Problem removing this wheel built from a git repo: {:?}",
archive_path
));
}
metadata
}