Files
uv/crates/gourgeist/src/virtualenv_cache.rs
T
Charlie Marsh 7caf5f42b8 Copy over gourgeist crate (#61)
This PR copies over the `gourgeist` crate at commit
`e64c17a263dac6933702dc8d155425c053fe885a` with no modifications.

It won't pass CI, but modifications will intentionally be confined to
later PRs.
2023-10-08 14:37:09 -04:00

110 lines
3.9 KiB
Rust

//! Deprecated, use only as template when implementing caching
use crate::Error;
use camino::{Utf8Path, Utf8PathBuf};
use dirs::data_dir;
use fs_err as fs;
use std::io;
use std::path::Path;
use tracing::debug;
/// Install wheel, pip and setuptools from the cache
pub(crate) fn install_base_packages(
bin_dir: &Utf8Path,
venv_python: &Utf8Path,
site_packages: &Utf8Path,
) -> Result<(), Error> {
// Install packages
// TODO: Implement our own logic:
// * Our own cache and logic to detect whether a wheel is present
// * Check if the version is recent (e.g. update if older than 1 month)
// * Query pypi API if no, parse versions (pep440) and their metadata
// * Download compatible wheel (py3-none-any should do)
// * Install into the cache directory
let prefix = "virtualenv/wheel/3.11/image/1/CopyPipInstall/";
let wheel_tag = "py3-none-any";
let packages = &[
("pip", "23.2.1"),
("setuptools", "68.2.2"),
("wheel", "0.41.2"),
];
let virtualenv_data_dir: Utf8PathBuf = data_dir().unwrap().try_into().unwrap();
for (name, version) in packages {
// TODO: acquire lock
let unpacked_wheel = virtualenv_data_dir
.join(prefix)
.join(format!("{name}-{version}-{wheel_tag}"));
debug!("Installing {name} by copying from {unpacked_wheel}");
copy_dir_all(&unpacked_wheel, site_packages.as_std_path())?;
// Generate launcher
// virtualenv for some reason creates extra entrypoints that we don't
// https://github.com/pypa/virtualenv/blob/025e96fbad37f85617364002ae2a0064b09fc984/src/virtualenv/seed/embed/via_app_data/pip_install/base.py#L74-L95
let ini_text = fs::read_to_string(
site_packages
.join(format!("{name}-{version}.dist-info"))
.join("entry_points.txt"),
)?;
let entry_points_mapping = configparser::ini::Ini::new_cs()
.read(ini_text)
.map_err(|err| format!("{name} entry_points.txt is invalid: {}", err))
.unwrap();
for (key, value) in entry_points_mapping
.get("console_scripts")
.cloned()
.unwrap_or_default()
{
let (import_from, function) = value
.as_ref()
.and_then(|value| value.split_once(':'))
.ok_or_else(|| {
format!("{name} entry_points.txt {key} has an invalid value {value:?}")
})
.unwrap();
let launcher = bin_dir.join(key);
let launcher_script = unix_launcher_script(venv_python, import_from, function);
fs::write(&launcher, launcher_script)?;
// We need to make the launcher executable
#[cfg(target_family = "unix")]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(launcher, std::fs::Permissions::from_mode(0o755))?;
}
}
}
Ok(())
}
/// https://stackoverflow.com/a/65192210/3549270
pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
fs::create_dir_all(&dst)?;
for entry in fs::read_dir(src.as_ref())? {
let entry = entry?;
let ty = entry.file_type()?;
if ty.is_dir() {
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
} else {
fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
}
}
Ok(())
}
/// Template for the console scripts in the `bin` directory
pub fn unix_launcher_script(python: &Utf8Path, import_from: &str, function: &str) -> String {
format!(
r#"#!{python}
# -*- coding: utf-8 -*-
import re
import sys
from {import_from} import {function}
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit({function}())
"#,
python = python,
import_from = import_from,
function = function
)
}