mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Unify site-packages into distribution enum (#136)
Gets rid of the custom `DistInfo` struct in the site-packages abstraction in favor of a new kind of distribution (`InstalledDistribution`). No change in behavior.
This commit is contained in:
parent
bd01fb490e
commit
7ef6c0315c
16 changed files with 202 additions and 166 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1310,7 +1310,6 @@ dependencies = [
|
||||||
"rayon",
|
"rayon",
|
||||||
"reflink-copy",
|
"reflink-copy",
|
||||||
"regex",
|
"regex",
|
||||||
"rfc2047-decoder",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
|
|
@ -36,7 +36,6 @@ pyo3 = { version = "0.19.2", features = ["extension-module", "abi3-py37"], optio
|
||||||
rayon = { version = "1.8.0", optional = true }
|
rayon = { version = "1.8.0", optional = true }
|
||||||
reflink-copy = { workspace = true }
|
reflink-copy = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
rfc2047-decoder = { workspace = true }
|
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
sha2 = { workspace = true }
|
sha2 = { workspace = true }
|
||||||
|
|
|
@ -13,7 +13,7 @@ use pep508_rs::Requirement;
|
||||||
use platform_host::Platform;
|
use platform_host::Platform;
|
||||||
use platform_tags::Tags;
|
use platform_tags::Tags;
|
||||||
use puffin_client::PypiClientBuilder;
|
use puffin_client::PypiClientBuilder;
|
||||||
use puffin_installer::{Downloader, LocalDistribution, LocalIndex, RemoteDistribution, Unzipper};
|
use puffin_installer::{CachedDistribution, Downloader, LocalIndex, RemoteDistribution, Unzipper};
|
||||||
use puffin_interpreter::PythonExecutable;
|
use puffin_interpreter::PythonExecutable;
|
||||||
use puffin_package::package_name::PackageName;
|
use puffin_package::package_name::PackageName;
|
||||||
use puffin_resolver::WheelFinder;
|
use puffin_resolver::WheelFinder;
|
||||||
|
@ -440,7 +440,7 @@ async fn resolve_and_install(
|
||||||
} else {
|
} else {
|
||||||
LocalIndex::default()
|
LocalIndex::default()
|
||||||
};
|
};
|
||||||
let (cached, uncached): (Vec<LocalDistribution>, Vec<Requirement>) =
|
let (cached, uncached): (Vec<CachedDistribution>, Vec<Requirement>) =
|
||||||
requirements.iter().partition_map(|requirement| {
|
requirements.iter().partition_map(|requirement| {
|
||||||
let package = PackageName::normalize(&requirement.name);
|
let package = PackageName::normalize(&requirement.name);
|
||||||
if let Some(distribution) = local_index
|
if let Some(distribution) = local_index
|
||||||
|
|
|
@ -4,7 +4,8 @@ use anyhow::Result;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use platform_host::Platform;
|
use platform_host::Platform;
|
||||||
use puffin_interpreter::{PythonExecutable, SitePackages};
|
use puffin_installer::SitePackages;
|
||||||
|
use puffin_interpreter::PythonExecutable;
|
||||||
|
|
||||||
use crate::commands::ExitStatus;
|
use crate::commands::ExitStatus;
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
|
|
|
@ -2,17 +2,19 @@ use std::fmt::Write;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use pep508_rs::Requirement;
|
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
|
use pep508_rs::Requirement;
|
||||||
use platform_host::Platform;
|
use platform_host::Platform;
|
||||||
use platform_tags::Tags;
|
use platform_tags::Tags;
|
||||||
use puffin_client::PypiClientBuilder;
|
use puffin_client::PypiClientBuilder;
|
||||||
use puffin_installer::{LocalDistribution, LocalIndex, RemoteDistribution};
|
use puffin_installer::{
|
||||||
use puffin_interpreter::{Distribution, PythonExecutable, SitePackages};
|
CachedDistribution, Distribution, InstalledDistribution, LocalIndex, RemoteDistribution,
|
||||||
|
SitePackages,
|
||||||
|
};
|
||||||
|
use puffin_interpreter::PythonExecutable;
|
||||||
use puffin_package::package_name::PackageName;
|
use puffin_package::package_name::PackageName;
|
||||||
use puffin_package::requirements_txt::RequirementsTxt;
|
use puffin_package::requirements_txt::RequirementsTxt;
|
||||||
use puffin_resolver::Resolution;
|
use puffin_resolver::Resolution;
|
||||||
|
@ -230,37 +232,35 @@ pub(crate) async fn sync_requirements(
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for dist in extraneous
|
for event in extraneous
|
||||||
.iter()
|
.into_iter()
|
||||||
.map(|dist_info| PackageModification {
|
.map(|distribution| ChangeEvent {
|
||||||
name: dist_info.name(),
|
distribution: Distribution::from(distribution),
|
||||||
version: dist_info.version(),
|
kind: ChangeEventKind::Remove,
|
||||||
modification: Modification::Remove,
|
|
||||||
})
|
})
|
||||||
.chain(wheels.iter().map(|dist_info| PackageModification {
|
.chain(wheels.into_iter().map(|distribution| ChangeEvent {
|
||||||
name: dist_info.name(),
|
distribution: Distribution::from(distribution),
|
||||||
version: dist_info.version(),
|
kind: ChangeEventKind::Add,
|
||||||
modification: Modification::Add,
|
|
||||||
}))
|
}))
|
||||||
.sorted_unstable_by_key(|modification| modification.name)
|
.sorted_unstable_by_key(|event| event.distribution.name().clone())
|
||||||
{
|
{
|
||||||
match dist.modification {
|
match event.kind {
|
||||||
Modification::Add => {
|
ChangeEventKind::Add => {
|
||||||
writeln!(
|
writeln!(
|
||||||
printer,
|
printer,
|
||||||
" {} {}{}",
|
" {} {}{}",
|
||||||
"+".green(),
|
"+".green(),
|
||||||
dist.name.as_ref().white().bold(),
|
event.distribution.name().white().bold(),
|
||||||
format!("@{}", dist.version).dimmed()
|
format!("@{}", event.distribution.version()).dimmed()
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
Modification::Remove => {
|
ChangeEventKind::Remove => {
|
||||||
writeln!(
|
writeln!(
|
||||||
printer,
|
printer,
|
||||||
" {} {}{}",
|
" {} {}{}",
|
||||||
"-".red(),
|
"-".red(),
|
||||||
dist.name.as_ref().white().bold(),
|
event.distribution.name().white().bold(),
|
||||||
format!("@{}", dist.version).dimmed()
|
format!("@{}", event.distribution.version()).dimmed()
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -273,7 +273,7 @@ pub(crate) async fn sync_requirements(
|
||||||
struct PartitionedRequirements {
|
struct PartitionedRequirements {
|
||||||
/// The distributions that are not already installed in the current environment, but are
|
/// The distributions that are not already installed in the current environment, but are
|
||||||
/// available in the local cache.
|
/// available in the local cache.
|
||||||
local: Vec<LocalDistribution>,
|
local: Vec<CachedDistribution>,
|
||||||
|
|
||||||
/// The distributions that are not already installed in the current environment, and are
|
/// The distributions that are not already installed in the current environment, and are
|
||||||
/// not available in the local cache.
|
/// not available in the local cache.
|
||||||
|
@ -281,7 +281,7 @@ struct PartitionedRequirements {
|
||||||
|
|
||||||
/// The distributions that are already installed in the current environment, and are
|
/// The distributions that are already installed in the current environment, and are
|
||||||
/// _not_ necessary to satisfy the requirements.
|
/// _not_ necessary to satisfy the requirements.
|
||||||
extraneous: Vec<Distribution>,
|
extraneous: Vec<InstalledDistribution>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartitionedRequirements {
|
impl PartitionedRequirements {
|
||||||
|
@ -354,7 +354,7 @@ impl PartitionedRequirements {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Modification {
|
enum ChangeEventKind {
|
||||||
/// The package was added to the environment.
|
/// The package was added to the environment.
|
||||||
Add,
|
Add,
|
||||||
/// The package was removed from the environment.
|
/// The package was removed from the environment.
|
||||||
|
@ -362,8 +362,7 @@ enum Modification {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct PackageModification<'a> {
|
struct ChangeEvent {
|
||||||
name: &'a PackageName,
|
distribution: Distribution,
|
||||||
version: &'a pep440_rs::Version,
|
kind: ChangeEventKind,
|
||||||
modification: Modification,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ pub(crate) async fn pip_uninstall(
|
||||||
.collect::<Result<Vec<Requirement>>>()?;
|
.collect::<Result<Vec<Requirement>>>()?;
|
||||||
|
|
||||||
// Index the current `site-packages` directory.
|
// Index the current `site-packages` directory.
|
||||||
let site_packages = puffin_interpreter::SitePackages::from_executable(&python).await?;
|
let site_packages = puffin_installer::SitePackages::from_executable(&python).await?;
|
||||||
|
|
||||||
// Sort and deduplicate the requirements.
|
// Sort and deduplicate the requirements.
|
||||||
let packages = {
|
let packages = {
|
||||||
|
|
|
@ -13,7 +13,8 @@ use wheel_filename::WheelFilename;
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Distribution {
|
pub enum Distribution {
|
||||||
Remote(RemoteDistribution),
|
Remote(RemoteDistribution),
|
||||||
Local(LocalDistribution),
|
Cached(CachedDistribution),
|
||||||
|
Installed(InstalledDistribution),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Distribution {
|
impl Distribution {
|
||||||
|
@ -21,7 +22,8 @@ impl Distribution {
|
||||||
pub fn name(&self) -> &PackageName {
|
pub fn name(&self) -> &PackageName {
|
||||||
match self {
|
match self {
|
||||||
Self::Remote(dist) => dist.name(),
|
Self::Remote(dist) => dist.name(),
|
||||||
Self::Local(dist) => dist.name(),
|
Self::Cached(dist) => dist.name(),
|
||||||
|
Self::Installed(dist) => dist.name(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +31,8 @@ impl Distribution {
|
||||||
pub fn version(&self) -> &Version {
|
pub fn version(&self) -> &Version {
|
||||||
match self {
|
match self {
|
||||||
Self::Remote(dist) => dist.version(),
|
Self::Remote(dist) => dist.version(),
|
||||||
Self::Local(dist) => dist.version(),
|
Self::Cached(dist) => dist.version(),
|
||||||
|
Self::Installed(dist) => dist.version(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,11 +42,30 @@ impl Distribution {
|
||||||
pub fn id(&self) -> String {
|
pub fn id(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Self::Remote(dist) => dist.id(),
|
Self::Remote(dist) => dist.id(),
|
||||||
Self::Local(dist) => dist.id(),
|
Self::Cached(dist) => dist.id(),
|
||||||
|
Self::Installed(dist) => dist.id(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<RemoteDistribution> for Distribution {
|
||||||
|
fn from(dist: RemoteDistribution) -> Self {
|
||||||
|
Self::Remote(dist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CachedDistribution> for Distribution {
|
||||||
|
fn from(dist: CachedDistribution) -> Self {
|
||||||
|
Self::Cached(dist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<InstalledDistribution> for Distribution {
|
||||||
|
fn from(dist: InstalledDistribution) -> Self {
|
||||||
|
Self::Installed(dist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A built distribution (wheel) that exists as a remote file (e.g., on `PyPI`).
|
/// A built distribution (wheel) that exists as a remote file (e.g., on `PyPI`).
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RemoteDistribution {
|
pub struct RemoteDistribution {
|
||||||
|
@ -82,16 +104,16 @@ impl RemoteDistribution {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A built distribution (wheel) that exists as a local file (e.g., in the wheel cache).
|
/// A built distribution (wheel) that exists in a local cache.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct LocalDistribution {
|
pub struct CachedDistribution {
|
||||||
name: PackageName,
|
name: PackageName,
|
||||||
version: Version,
|
version: Version,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocalDistribution {
|
impl CachedDistribution {
|
||||||
/// Initialize a new local distribution.
|
/// Initialize a new cached distribution.
|
||||||
pub fn new(name: PackageName, version: Version, path: PathBuf) -> Self {
|
pub fn new(name: PackageName, version: Version, path: PathBuf) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name,
|
name,
|
||||||
|
@ -100,7 +122,7 @@ impl LocalDistribution {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to parse a cached distribution from a directory name (like `django-5.0a1`).
|
/// Try to parse a distribution from a cached directory name (like `django-5.0a1`).
|
||||||
pub(crate) fn try_from_path(path: &Path) -> Result<Option<Self>> {
|
pub(crate) fn try_from_path(path: &Path) -> Result<Option<Self>> {
|
||||||
let Some(file_name) = path.file_name() else {
|
let Some(file_name) = path.file_name() else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
@ -116,7 +138,7 @@ impl LocalDistribution {
|
||||||
let version = Version::from_str(version).map_err(|err| anyhow!(err))?;
|
let version = Version::from_str(version).map_err(|err| anyhow!(err))?;
|
||||||
let path = path.to_path_buf();
|
let path = path.to_path_buf();
|
||||||
|
|
||||||
Ok(Some(LocalDistribution {
|
Ok(Some(CachedDistribution {
|
||||||
name,
|
name,
|
||||||
version,
|
version,
|
||||||
path,
|
path,
|
||||||
|
@ -139,3 +161,67 @@ impl LocalDistribution {
|
||||||
format!("{}-{}", DistInfoName::from(self.name()), self.version())
|
format!("{}-{}", DistInfoName::from(self.name()), self.version())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A built distribution (wheel) that exists in a virtual environment.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct InstalledDistribution {
|
||||||
|
name: PackageName,
|
||||||
|
version: Version,
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InstalledDistribution {
|
||||||
|
/// Initialize a new installed distribution.
|
||||||
|
pub fn new(name: PackageName, version: Version, path: PathBuf) -> Self {
|
||||||
|
Self {
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to parse a distribution from a `.dist-info` directory name (like `django-5.0a1.dist-info`).
|
||||||
|
///
|
||||||
|
/// See: <https://packaging.python.org/en/latest/specifications/recording-installed-packages/#recording-installed-packages>
|
||||||
|
pub(crate) fn try_from_path(path: &Path) -> Result<Option<Self>> {
|
||||||
|
if path.extension().is_some_and(|ext| ext == "dist-info") {
|
||||||
|
let Some(file_stem) = path.file_stem() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let Some(file_stem) = file_stem.to_str() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let Some((name, version)) = file_stem.split_once('-') else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = PackageName::normalize(name);
|
||||||
|
let version = Version::from_str(version).map_err(|err| anyhow!(err))?;
|
||||||
|
let path = path.to_path_buf();
|
||||||
|
|
||||||
|
return Ok(Some(Self {
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
path,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &PackageName {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn version(&self) -> &Version {
|
||||||
|
&self.version
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &Path {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> String {
|
||||||
|
format!("{}-{}", DistInfoName::from(self.name()), self.version())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -100,7 +100,7 @@ async fn fetch_wheel(
|
||||||
// Read from the cache, if possible.
|
// Read from the cache, if possible.
|
||||||
if let Some(cache) = cache.as_ref() {
|
if let Some(cache) = cache.as_ref() {
|
||||||
if let Ok(buffer) = cacache::read_hash(&cache, &sri).await {
|
if let Ok(buffer) = cacache::read_hash(&cache, &sri).await {
|
||||||
debug!("Extracted wheel from cache: {:?}", remote.file().filename);
|
debug!("Extracted wheel from cache: {}", remote.file().filename);
|
||||||
return Ok(InMemoryDistribution { remote, buffer });
|
return Ok(InMemoryDistribution { remote, buffer });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,11 @@ use anyhow::Result;
|
||||||
use puffin_package::package_name::PackageName;
|
use puffin_package::package_name::PackageName;
|
||||||
|
|
||||||
use crate::cache::WheelCache;
|
use crate::cache::WheelCache;
|
||||||
use crate::distribution::LocalDistribution;
|
use crate::distribution::CachedDistribution;
|
||||||
|
|
||||||
/// A local index of cached distributions.
|
/// A local index of cached distributions.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct LocalIndex(HashMap<PackageName, LocalDistribution>);
|
pub struct LocalIndex(HashMap<PackageName, CachedDistribution>);
|
||||||
|
|
||||||
impl LocalIndex {
|
impl LocalIndex {
|
||||||
/// Build an index of cached distributions from a directory.
|
/// Build an index of cached distributions from a directory.
|
||||||
|
@ -24,7 +24,7 @@ impl LocalIndex {
|
||||||
|
|
||||||
while let Some(entry) = dir.next_entry().await? {
|
while let Some(entry) = dir.next_entry().await? {
|
||||||
if entry.file_type().await?.is_dir() {
|
if entry.file_type().await?.is_dir() {
|
||||||
if let Some(dist_info) = LocalDistribution::try_from_path(&entry.path())? {
|
if let Some(dist_info) = CachedDistribution::try_from_path(&entry.path())? {
|
||||||
index.insert(dist_info.name().clone(), dist_info);
|
index.insert(dist_info.name().clone(), dist_info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ impl LocalIndex {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a distribution from the index, if it exists.
|
/// Returns a distribution from the index, if it exists.
|
||||||
pub fn get(&self, name: &PackageName) -> Option<&LocalDistribution> {
|
pub fn get(&self, name: &PackageName) -> Option<&CachedDistribution> {
|
||||||
self.0.get(name)
|
self.0.get(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ use pep440_rs::Version;
|
||||||
use puffin_interpreter::PythonExecutable;
|
use puffin_interpreter::PythonExecutable;
|
||||||
use puffin_package::package_name::PackageName;
|
use puffin_package::package_name::PackageName;
|
||||||
|
|
||||||
use crate::LocalDistribution;
|
use crate::CachedDistribution;
|
||||||
|
|
||||||
pub struct Installer<'a> {
|
pub struct Installer<'a> {
|
||||||
python: &'a PythonExecutable,
|
python: &'a PythonExecutable,
|
||||||
|
@ -31,7 +31,7 @@ impl<'a> Installer<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Install a set of wheels into a Python virtual environment.
|
/// Install a set of wheels into a Python virtual environment.
|
||||||
pub fn install(self, wheels: &[LocalDistribution]) -> Result<()> {
|
pub fn install(self, wheels: &[CachedDistribution]) -> Result<()> {
|
||||||
tokio::task::block_in_place(|| {
|
tokio::task::block_in_place(|| {
|
||||||
wheels.par_iter().try_for_each(|wheel| {
|
wheels.par_iter().try_for_each(|wheel| {
|
||||||
let location = install_wheel_rs::InstallLocation::new(
|
let location = install_wheel_rs::InstallLocation::new(
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
pub use distribution::{Distribution, LocalDistribution, RemoteDistribution};
|
pub use distribution::{
|
||||||
|
CachedDistribution, Distribution, InstalledDistribution, RemoteDistribution,
|
||||||
|
};
|
||||||
pub use downloader::{Downloader, Reporter as DownloadReporter};
|
pub use downloader::{Downloader, Reporter as DownloadReporter};
|
||||||
pub use index::LocalIndex;
|
pub use index::LocalIndex;
|
||||||
pub use installer::{Installer, Reporter as InstallReporter};
|
pub use installer::{Installer, Reporter as InstallReporter};
|
||||||
|
pub use site_packages::SitePackages;
|
||||||
pub use uninstall::uninstall;
|
pub use uninstall::uninstall;
|
||||||
pub use unzipper::{Reporter as UnzipReporter, Unzipper};
|
pub use unzipper::{Reporter as UnzipReporter, Unzipper};
|
||||||
|
|
||||||
|
@ -10,6 +13,7 @@ mod distribution;
|
||||||
mod downloader;
|
mod downloader;
|
||||||
mod index;
|
mod index;
|
||||||
mod installer;
|
mod installer;
|
||||||
|
mod site_packages;
|
||||||
mod uninstall;
|
mod uninstall;
|
||||||
mod unzipper;
|
mod unzipper;
|
||||||
mod vendor;
|
mod vendor;
|
||||||
|
|
54
crates/puffin-installer/src/site_packages.rs
Normal file
54
crates/puffin-installer/src/site_packages.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use fs_err::tokio as fs;
|
||||||
|
|
||||||
|
use puffin_interpreter::PythonExecutable;
|
||||||
|
use puffin_package::package_name::PackageName;
|
||||||
|
|
||||||
|
use crate::InstalledDistribution;
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct SitePackages(BTreeMap<PackageName, InstalledDistribution>);
|
||||||
|
|
||||||
|
impl SitePackages {
|
||||||
|
/// Build an index of installed packages from the given Python executable.
|
||||||
|
pub async fn from_executable(python: &PythonExecutable) -> Result<Self> {
|
||||||
|
let mut index = BTreeMap::new();
|
||||||
|
|
||||||
|
let mut dir = fs::read_dir(python.site_packages()).await?;
|
||||||
|
while let Some(entry) = dir.next_entry().await? {
|
||||||
|
if entry.file_type().await?.is_dir() {
|
||||||
|
if let Some(dist_info) = InstalledDistribution::try_from_path(&entry.path())? {
|
||||||
|
index.insert(dist_info.name().clone(), dist_info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over the installed packages.
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = (&PackageName, &InstalledDistribution)> {
|
||||||
|
self.0.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the version of the given package, if it is installed.
|
||||||
|
pub fn get(&self, name: &PackageName) -> Option<&InstalledDistribution> {
|
||||||
|
self.0.get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the given package from the index, returning its version if it was installed.
|
||||||
|
pub fn remove(&mut self, name: &PackageName) -> Option<InstalledDistribution> {
|
||||||
|
self.0.remove(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for SitePackages {
|
||||||
|
type Item = (PackageName, InstalledDistribution);
|
||||||
|
type IntoIter = std::collections::btree_map::IntoIter<PackageName, InstalledDistribution>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.0.into_iter()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use puffin_interpreter::Distribution;
|
use crate::InstalledDistribution;
|
||||||
|
|
||||||
/// Uninstall a package from the specified Python environment.
|
/// Uninstall a package from the specified Python environment.
|
||||||
pub async fn uninstall(distribution: &Distribution) -> Result<install_wheel_rs::Uninstall> {
|
pub async fn uninstall(
|
||||||
|
distribution: &InstalledDistribution,
|
||||||
|
) -> Result<install_wheel_rs::Uninstall> {
|
||||||
let uninstall = tokio::task::spawn_blocking({
|
let uninstall = tokio::task::spawn_blocking({
|
||||||
let path = distribution.path().to_owned();
|
let path = distribution.path().to_owned();
|
||||||
move || install_wheel_rs::uninstall_wheel(&path)
|
move || install_wheel_rs::uninstall_wheel(&path)
|
||||||
|
|
|
@ -14,7 +14,7 @@ use puffin_package::package_name::PackageName;
|
||||||
use crate::cache::WheelCache;
|
use crate::cache::WheelCache;
|
||||||
use crate::downloader::InMemoryDistribution;
|
use crate::downloader::InMemoryDistribution;
|
||||||
use crate::vendor::CloneableSeekableReader;
|
use crate::vendor::CloneableSeekableReader;
|
||||||
use crate::LocalDistribution;
|
use crate::CachedDistribution;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Unzipper {
|
pub struct Unzipper {
|
||||||
|
@ -35,7 +35,7 @@ impl Unzipper {
|
||||||
&self,
|
&self,
|
||||||
downloads: Vec<InMemoryDistribution>,
|
downloads: Vec<InMemoryDistribution>,
|
||||||
target: &Path,
|
target: &Path,
|
||||||
) -> Result<Vec<LocalDistribution>> {
|
) -> Result<Vec<CachedDistribution>> {
|
||||||
// Create the wheel cache subdirectory, if necessary.
|
// Create the wheel cache subdirectory, if necessary.
|
||||||
let wheel_cache = WheelCache::new(target);
|
let wheel_cache = WheelCache::new(target);
|
||||||
wheel_cache.init().await?;
|
wheel_cache.init().await?;
|
||||||
|
@ -63,7 +63,7 @@ impl Unzipper {
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
wheels.push(LocalDistribution::new(
|
wheels.push(CachedDistribution::new(
|
||||||
remote.name().clone(),
|
remote.name().clone(),
|
||||||
remote.version().clone(),
|
remote.version().clone(),
|
||||||
wheel_cache.entry(&remote.id()),
|
wheel_cache.entry(&remote.id()),
|
||||||
|
|
|
@ -7,11 +7,9 @@ use pep508_rs::MarkerEnvironment;
|
||||||
use platform_host::Platform;
|
use platform_host::Platform;
|
||||||
|
|
||||||
use crate::python_platform::PythonPlatform;
|
use crate::python_platform::PythonPlatform;
|
||||||
pub use crate::site_packages::{Distribution, SitePackages};
|
|
||||||
|
|
||||||
mod markers;
|
mod markers;
|
||||||
mod python_platform;
|
mod python_platform;
|
||||||
mod site_packages;
|
|
||||||
mod virtual_env;
|
mod virtual_env;
|
||||||
|
|
||||||
/// A Python executable and its associated platform markers.
|
/// A Python executable and its associated platform markers.
|
||||||
|
|
|
@ -1,106 +0,0 @@
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
use fs_err::tokio as fs;
|
|
||||||
|
|
||||||
use pep440_rs::Version;
|
|
||||||
use puffin_package::package_name::PackageName;
|
|
||||||
|
|
||||||
use crate::PythonExecutable;
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct SitePackages(BTreeMap<PackageName, Distribution>);
|
|
||||||
|
|
||||||
impl SitePackages {
|
|
||||||
/// Build an index of installed packages from the given Python executable.
|
|
||||||
pub async fn from_executable(python: &PythonExecutable) -> Result<Self> {
|
|
||||||
let mut index = BTreeMap::new();
|
|
||||||
|
|
||||||
let mut dir = fs::read_dir(python.site_packages()).await?;
|
|
||||||
while let Some(entry) = dir.next_entry().await? {
|
|
||||||
if entry.file_type().await?.is_dir() {
|
|
||||||
if let Some(dist_info) = Distribution::try_from_path(&entry.path())? {
|
|
||||||
index.insert(dist_info.name().clone(), dist_info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self(index))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an iterator over the installed packages.
|
|
||||||
pub fn iter(&self) -> impl Iterator<Item = (&PackageName, &Distribution)> {
|
|
||||||
self.0.iter()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the version of the given package, if it is installed.
|
|
||||||
pub fn get(&self, name: &PackageName) -> Option<&Distribution> {
|
|
||||||
self.0.get(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove the given package from the index, returning its version if it was installed.
|
|
||||||
pub fn remove(&mut self, name: &PackageName) -> Option<Distribution> {
|
|
||||||
self.0.remove(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoIterator for SitePackages {
|
|
||||||
type Item = (PackageName, Distribution);
|
|
||||||
type IntoIter = std::collections::btree_map::IntoIter<PackageName, Distribution>;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
self.0.into_iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Distribution {
|
|
||||||
name: PackageName,
|
|
||||||
version: Version,
|
|
||||||
path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Distribution {
|
|
||||||
/// Try to parse a (potential) `dist-info` directory into a package name and version.
|
|
||||||
///
|
|
||||||
/// See: <https://packaging.python.org/en/latest/specifications/recording-installed-packages/#recording-installed-packages>
|
|
||||||
fn try_from_path(path: &Path) -> Result<Option<Self>> {
|
|
||||||
if path.extension().is_some_and(|ext| ext == "dist-info") {
|
|
||||||
let Some(file_stem) = path.file_stem() else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
let Some(file_stem) = file_stem.to_str() else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
let Some((name, version)) = file_stem.split_once('-') else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let name = PackageName::normalize(name);
|
|
||||||
let version = Version::from_str(version).map_err(|err| anyhow!(err))?;
|
|
||||||
let path = path.to_path_buf();
|
|
||||||
|
|
||||||
return Ok(Some(Distribution {
|
|
||||||
name,
|
|
||||||
version,
|
|
||||||
path,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn name(&self) -> &PackageName {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn version(&self) -> &Version {
|
|
||||||
&self.version
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn path(&self) -> &Path {
|
|
||||||
&self.path
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue