diff --git a/crates/puffin-cli/src/commands/compile.rs b/crates/puffin-cli/src/commands/compile.rs index 44cc3eed5..7506628a3 100644 --- a/crates/puffin-cli/src/commands/compile.rs +++ b/crates/puffin-cli/src/commands/compile.rs @@ -52,7 +52,7 @@ pub(crate) async fn compile(src: &Path, cache: Option<&Path>) -> Result, flags: SyncFlags) -> python.executable().display() ); - // Remove any already-installed packages. - let requirements = if flags.intersects(SyncFlags::IGNORE_INSTALLED) { - requirements + // Determine the current environment markers. + let markers = python.markers(); + let tags = Tags::from_env(python.platform(), python.simple_version())?; + + // Index all the already-installed packages in site-packages. + let site_packages = if flags.intersects(SyncFlags::IGNORE_INSTALLED) { + SitePackages::default() } else { - let site_packages = SitePackages::from_executable(&python).await?; - requirements.filter(|requirement| { - let package = PackageName::normalize(&requirement.name); - if let Some(version) = site_packages.get(&package) { - #[allow(clippy::print_stdout)] - { - info!("Requirement already satisfied: {package} ({version})"); - } - false - } else { - true - } - }) + SitePackages::from_executable(&python).await? }; + // Index all the already-downloaded wheels in the cache. + let local_index = if let Some(cache) = cache { + LocalIndex::from_directory(cache).await? + } else { + LocalIndex::default() + }; + + let requirements = requirements + .iter() + .filter_map(|requirement| { + let package = PackageName::normalize(&requirement.name); + + // Filter out already-installed packages. + if let Some(version) = site_packages.get(&package) { + info!("Requirement already satisfied: {package} ({version})"); + return None; + } + + // Identify any locally-available distributions that satisfy the requirement. + if let Some(distribution) = local_index + .get(&package) + .filter(|dist| requirement.is_satisfied_by(dist.version())) + { + debug!( + "Requirement already cached: {} ({})", + distribution.name(), + distribution.version() + ); + return Some(Requirement::Local(distribution.clone())); + } + + debug!("Identified uncached requirement: {}", requirement); + Some(Requirement::Remote(requirement.clone())) + }) + .collect::>(); + if requirements.is_empty() { let s = if initial_requirements == 1 { "" } else { "s" }; info!( @@ -72,42 +100,7 @@ pub(crate) async fn sync(src: &Path, cache: Option<&Path>, flags: SyncFlags) -> return Ok(ExitStatus::Success); } - // Detect any cached wheels. - let (uncached, cached) = if let Some(cache) = cache { - let mut cached = Vec::with_capacity(requirements.len()); - let mut uncached = Vec::with_capacity(requirements.len()); - - let index = puffin_installer::LocalIndex::from_directory(cache).await?; - for requirement in requirements { - let package = PackageName::normalize(&requirement.name); - if let Some(distribution) = index - .get(&package) - .filter(|dist| requirement.is_satisfied_by(dist.version())) - { - debug!( - "Requirement already cached: {} ({})", - distribution.name(), - distribution.version() - ); - cached.push(distribution.clone()); - } else { - debug!("Identified uncached requirement: {}", requirement); - uncached.push(requirement); - } - } - - (Requirements::new(uncached), cached) - } else { - (requirements, Vec::new()) - }; - - // Determine the current environment markers. - let markers = python.markers(); - - // Determine the compatible platform tags. - let tags = Tags::from_env(python.platform(), python.simple_version())?; - - // Instantiate a client. + // Resolve the dependencies. let client = { let mut pypi_client = PypiClientBuilder::default(); if let Some(cache) = cache { @@ -115,25 +108,27 @@ pub(crate) async fn sync(src: &Path, cache: Option<&Path>, flags: SyncFlags) -> } pypi_client.build() }; + let resolution = puffin_resolver::resolve( + requirements + .iter() + .filter_map(|requirement| match requirement { + Requirement::Remote(requirement) => Some(requirement), + Requirement::Local(_) => None, + }), + markers, + &tags, + &client, + puffin_resolver::ResolveFlags::NO_DEPS, + ) + .await?; - // Resolve the dependencies. - let resolution = if uncached.is_empty() { - puffin_resolver::Resolution::empty() - } else { - puffin_resolver::resolve( - &uncached, - markers, - &tags, - &client, - puffin_resolver::ResolveFlags::NO_DEPS, - ) - .await? - }; - - // Install into the current environment. - let wheels = cached + // Install the resolved distributions. + let wheels = requirements .into_iter() - .map(|local| Ok(Distribution::Local(local))) + .filter_map(|requirement| match requirement { + Requirement::Remote(_) => None, + Requirement::Local(distribution) => Some(Ok(Distribution::Local(distribution))), + }) .chain( resolution .into_files() @@ -152,3 +147,11 @@ pub(crate) async fn sync(src: &Path, cache: Option<&Path>, flags: SyncFlags) -> Ok(ExitStatus::Success) } + +#[derive(Debug)] +enum Requirement { + /// A requirement that must be downloaded from PyPI. + Remote(pep508_rs::Requirement), + /// A requirement that is already available locally. + Local(LocalDistribution), +} diff --git a/crates/puffin-installer/src/index.rs b/crates/puffin-installer/src/index.rs index 5d0a20584..d48b326f1 100644 --- a/crates/puffin-installer/src/index.rs +++ b/crates/puffin-installer/src/index.rs @@ -1,21 +1,21 @@ -use std::collections::BTreeMap; +use std::collections::HashMap; use std::path::Path; use anyhow::Result; -use crate::cache::WheelCache; use puffin_package::package_name::PackageName; +use crate::cache::WheelCache; use crate::distribution::LocalDistribution; /// A local index of cached distributions. -#[derive(Debug)] -pub struct LocalIndex(BTreeMap); +#[derive(Debug, Default)] +pub struct LocalIndex(HashMap); impl LocalIndex { /// Build an index of cached distributions from a directory. pub async fn from_directory(path: &Path) -> Result { - let mut index = BTreeMap::new(); + let mut index = HashMap::new(); let cache = WheelCache::new(path); let Ok(mut dir) = cache.read_dir().await else { diff --git a/crates/puffin-installer/src/lib.rs b/crates/puffin-installer/src/lib.rs index a407de16e..21532e843 100644 --- a/crates/puffin-installer/src/lib.rs +++ b/crates/puffin-installer/src/lib.rs @@ -1,4 +1,4 @@ -pub use distribution::{Distribution, RemoteDistribution}; +pub use distribution::{Distribution, LocalDistribution, RemoteDistribution}; pub use index::LocalIndex; pub use install::install; diff --git a/crates/puffin-interpreter/src/site_packages.rs b/crates/puffin-interpreter/src/site_packages.rs index 76cdc8904..cbe302e07 100644 --- a/crates/puffin-interpreter/src/site_packages.rs +++ b/crates/puffin-interpreter/src/site_packages.rs @@ -9,7 +9,7 @@ use puffin_package::package_name::PackageName; use crate::PythonExecutable; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct SitePackages(BTreeMap); impl SitePackages { diff --git a/crates/puffin-resolver/src/lib.rs b/crates/puffin-resolver/src/lib.rs index 6df99ce0d..cd627a06a 100644 --- a/crates/puffin-resolver/src/lib.rs +++ b/crates/puffin-resolver/src/lib.rs @@ -14,17 +14,12 @@ use platform_tags::Tags; use puffin_client::{File, PypiClient, SimpleJson}; use puffin_package::metadata::Metadata21; use puffin_package::package_name::PackageName; -use puffin_package::requirements::Requirements; use wheel_filename::WheelFilename; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Resolution(HashMap); impl Resolution { - pub fn empty() -> Self { - Self(HashMap::new()) - } - /// Iterate over the pinned packages in this resolution. pub fn iter(&self) -> impl Iterator { self.0.iter() @@ -86,7 +81,7 @@ impl From> for ResolveError { /// Resolve a set of requirements into a set of pinned versions. pub async fn resolve( - requirements: &Requirements, + requirements: impl Iterator, markers: &MarkerEnvironment, tags: &Tags, client: &PypiClient, @@ -116,16 +111,20 @@ pub async fn resolve( .ready_chunks(32); // Push all the requirements into the package sink. - let mut in_flight: HashSet = HashSet::with_capacity(requirements.len()); - for requirement in requirements.iter() { + let mut in_flight: HashSet = HashSet::new(); + for requirement in requirements { debug!("--> adding root dependency: {}", requirement); package_sink.unbounded_send(Request::Package(requirement.clone()))?; in_flight.insert(PackageName::normalize(&requirement.name)); } + if in_flight.is_empty() { + return Ok(Resolution::default()); + } + // Resolve the requirements. let mut resolution: HashMap = - HashMap::with_capacity(requirements.len()); + HashMap::with_capacity(in_flight.len()); while let Some(chunk) = package_stream.next().await { for result in chunk {