mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
Add support for disabling installation from pre-built wheels (#956)
Adds support for disabling installation from pre-built wheels i.e. the package must be built from source locally. We will still always use pre-built wheels for metadata during resolution. Available via `--no-binary` and `--no-binary-package <name>` flags in `pip install` and `pip sync`. There is no flag for `pip compile` since no installation happens there. ``` --no-binary Don't install pre-built wheels. When enabled, all installed packages will be installed from a source distribution. The resolver will still use pre-built wheels for metadata. --no-binary-package <NO_BINARY_PACKAGE> Don't install pre-built wheels for a specific package. When enabled, the specified packages will be installed from a source distribution. The resolver will still use pre-built wheels for metadata. ``` When packages are already installed, the `--no-binary` flag will have no affect without the `--reinstall` flag. In the future, I'd like to change this by tracking if a local distribution is from a pre-built wheel or a locally-built wheel. However, this is significantly more complex and different than `pip`'s behavior so deferring for now. For reference, `pip`'s flag works as follows: ``` --no-binary <format_control> Do not use binary packages. Can be supplied multiple times, and each time adds to the existing value. Accepts either ":all:" to disable all binary packages, ":none:" to empty the set (notice the colons), or one or more package names with commas between them (no colons). Note that some packages are tricky to compile and may fail to install when this option is used on them. ``` Note we are not matching the exact `pip` interface here because it seems complicated to use. I think we may want to consider adjusting our interface for this behavior since we're not entirely compatible anyway e.g. I think `--force-build` and `--force-build-package` are clearer names. We could also consider matching the `pip` interface or only allowing `--no-binary <package>` for compatibility. We can of course do whatever we want in our _own_ install interfaces later. Additionally, we may want to further consider the semantics of `--no-binary`. For example, if I run `pip install pydantic --no-binary` I expect _just_ Pydantic to be installed without binaries but by default we will build all of Pydantic's dependencies too. This work was prompted by #895, as it is much easier to measure performance gains from building source distributions if we have a flag to ensure we actually build source distributions. Additionally, this is a flag I have used frequently in production to debug packages that ship Cythonized wheels.
This commit is contained in:
parent
8b49d900bd
commit
33b35f7020
23 changed files with 482 additions and 49 deletions
|
@ -4,6 +4,7 @@
|
|||
|
||||
use anyhow::Result;
|
||||
use futures::{stream, Stream, StreamExt, TryStreamExt};
|
||||
use puffin_traits::NoBinary;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use distribution_filename::DistFilename;
|
||||
|
@ -22,6 +23,7 @@ pub struct DistFinder<'a> {
|
|||
reporter: Option<Box<dyn Reporter>>,
|
||||
interpreter: &'a Interpreter,
|
||||
flat_index: &'a FlatIndex,
|
||||
no_binary: &'a NoBinary,
|
||||
}
|
||||
|
||||
impl<'a> DistFinder<'a> {
|
||||
|
@ -31,6 +33,7 @@ impl<'a> DistFinder<'a> {
|
|||
client: &'a RegistryClient,
|
||||
interpreter: &'a Interpreter,
|
||||
flat_index: &'a FlatIndex,
|
||||
no_binary: &'a NoBinary,
|
||||
) -> Self {
|
||||
Self {
|
||||
tags,
|
||||
|
@ -38,6 +41,7 @@ impl<'a> DistFinder<'a> {
|
|||
reporter: None,
|
||||
interpreter,
|
||||
flat_index,
|
||||
no_binary,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,7 +116,10 @@ impl<'a> DistFinder<'a> {
|
|||
Ok(Resolution::new(resolution))
|
||||
}
|
||||
|
||||
/// select a version that satisfies the requirement, preferring wheels to source distributions.
|
||||
/// Select a version that satisfies the requirement.
|
||||
///
|
||||
/// Wheels are preferred to source distributions unless `no_binary` excludes wheels
|
||||
/// for the requirement.
|
||||
fn select(
|
||||
&self,
|
||||
requirement: &Requirement,
|
||||
|
@ -120,6 +127,12 @@ impl<'a> DistFinder<'a> {
|
|||
index: &IndexUrl,
|
||||
flat_index: Option<&FlatDistributions>,
|
||||
) -> Option<Dist> {
|
||||
let no_binary = match self.no_binary {
|
||||
NoBinary::None => false,
|
||||
NoBinary::All => true,
|
||||
NoBinary::Packages(packages) => packages.contains(&requirement.name),
|
||||
};
|
||||
|
||||
// Prioritize the flat index by initializing the "best" matches with its entries.
|
||||
let matching_override = if let Some(flat_index) = flat_index {
|
||||
match &requirement.version_or_url {
|
||||
|
@ -159,36 +172,38 @@ impl<'a> DistFinder<'a> {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Find the most-compatible wheel
|
||||
for (wheel, file) in files.wheels {
|
||||
// Only add dists compatible with the python version.
|
||||
// This is relevant for source dists which give no other indication of their
|
||||
// compatibility and wheels which may be tagged `py3-none-any` but
|
||||
// have `requires-python: ">=3.9"`
|
||||
if !file
|
||||
.requires_python
|
||||
.as_ref()
|
||||
.map_or(true, |requires_python| {
|
||||
requires_python.contains(self.interpreter.version())
|
||||
})
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
best_version = Some(version.clone());
|
||||
if let Some(priority) = wheel.compatibility(self.tags) {
|
||||
if best_wheel
|
||||
if !no_binary {
|
||||
// Find the most-compatible wheel
|
||||
for (wheel, file) in files.wheels {
|
||||
// Only add dists compatible with the python version.
|
||||
// This is relevant for source dists which give no other indication of their
|
||||
// compatibility and wheels which may be tagged `py3-none-any` but
|
||||
// have `requires-python: ">=3.9"`
|
||||
if !file
|
||||
.requires_python
|
||||
.as_ref()
|
||||
.map_or(true, |(.., existing)| priority > *existing)
|
||||
.map_or(true, |requires_python| {
|
||||
requires_python.contains(self.interpreter.version())
|
||||
})
|
||||
{
|
||||
best_wheel = Some((
|
||||
Dist::from_registry(
|
||||
DistFilename::WheelFilename(wheel),
|
||||
file,
|
||||
index.clone(),
|
||||
),
|
||||
priority,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
best_version = Some(version.clone());
|
||||
if let Some(priority) = wheel.compatibility(self.tags) {
|
||||
if best_wheel
|
||||
.as_ref()
|
||||
.map_or(true, |(.., existing)| priority > *existing)
|
||||
{
|
||||
best_wheel = Some((
|
||||
Dist::from_registry(
|
||||
DistFilename::WheelFilename(wheel),
|
||||
file,
|
||||
index.clone(),
|
||||
),
|
||||
priority,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,6 +101,7 @@ impl<'a, Context: BuildContext + Send + Sync> Resolver<'a, DefaultResolverProvid
|
|||
.iter()
|
||||
.chain(manifest.constraints.iter())
|
||||
.collect(),
|
||||
build_context.no_binary(),
|
||||
);
|
||||
Self::new_custom_io(
|
||||
manifest,
|
||||
|
|
|
@ -10,7 +10,7 @@ use platform_tags::Tags;
|
|||
use puffin_client::{FlatIndex, RegistryClient};
|
||||
use puffin_distribution::{DistributionDatabase, DistributionDatabaseError};
|
||||
use puffin_normalize::PackageName;
|
||||
use puffin_traits::BuildContext;
|
||||
use puffin_traits::{BuildContext, NoBinary};
|
||||
use pypi_types::Metadata21;
|
||||
|
||||
use crate::python_requirement::PythonRequirement;
|
||||
|
@ -55,10 +55,12 @@ pub struct DefaultResolverProvider<'a, Context: BuildContext + Send + Sync> {
|
|||
python_requirement: PythonRequirement<'a>,
|
||||
exclude_newer: Option<DateTime<Utc>>,
|
||||
allowed_yanks: AllowedYanks,
|
||||
no_binary: &'a NoBinary,
|
||||
}
|
||||
|
||||
impl<'a, Context: BuildContext + Send + Sync> DefaultResolverProvider<'a, Context> {
|
||||
/// Reads the flat index entries and builds the provider.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
client: &'a RegistryClient,
|
||||
fetcher: DistributionDatabase<'a, Context>,
|
||||
|
@ -67,6 +69,7 @@ impl<'a, Context: BuildContext + Send + Sync> DefaultResolverProvider<'a, Contex
|
|||
python_requirement: PythonRequirement<'a>,
|
||||
exclude_newer: Option<DateTime<Utc>>,
|
||||
allowed_yanks: AllowedYanks,
|
||||
no_binary: &'a NoBinary,
|
||||
) -> Self {
|
||||
Self {
|
||||
client,
|
||||
|
@ -76,6 +79,7 @@ impl<'a, Context: BuildContext + Send + Sync> DefaultResolverProvider<'a, Contex
|
|||
python_requirement,
|
||||
exclude_newer,
|
||||
allowed_yanks,
|
||||
no_binary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -99,6 +103,7 @@ impl<'a, Context: BuildContext + Send + Sync> ResolverProvider
|
|||
&self.allowed_yanks,
|
||||
self.exclude_newer.as_ref(),
|
||||
self.flat_index.get(package_name).cloned(),
|
||||
self.no_binary,
|
||||
)),
|
||||
Err(
|
||||
err @ (puffin_client::Error::PackageNotFound(_)
|
||||
|
|
|
@ -10,6 +10,7 @@ use pep440_rs::Version;
|
|||
use platform_tags::Tags;
|
||||
use puffin_client::{FlatDistributions, SimpleMetadata};
|
||||
use puffin_normalize::PackageName;
|
||||
use puffin_traits::NoBinary;
|
||||
use puffin_warnings::warn_user_once;
|
||||
use pypi_types::{Hashes, Yanked};
|
||||
|
||||
|
@ -33,11 +34,19 @@ impl VersionMap {
|
|||
allowed_yanks: &AllowedYanks,
|
||||
exclude_newer: Option<&DateTime<Utc>>,
|
||||
flat_index: Option<FlatDistributions>,
|
||||
no_binary: &NoBinary,
|
||||
) -> Self {
|
||||
// If we have packages of the same name from find links, gives them priority, otherwise start empty
|
||||
let mut version_map: BTreeMap<Version, PrioritizedDistribution> =
|
||||
flat_index.map(Into::into).unwrap_or_default();
|
||||
|
||||
// Check if binaries are allowed for this package
|
||||
let no_binary = match no_binary {
|
||||
NoBinary::None => false,
|
||||
NoBinary::All => true,
|
||||
NoBinary::Packages(packages) => packages.contains(package_name),
|
||||
};
|
||||
|
||||
// Collect compatible distributions.
|
||||
for (version, files) in metadata {
|
||||
for (filename, file) in files.all() {
|
||||
|
@ -73,6 +82,11 @@ impl VersionMap {
|
|||
let hash = file.hashes.clone();
|
||||
match filename {
|
||||
DistFilename::WheelFilename(filename) => {
|
||||
// If pre-built binaries are disabled, skip this wheel
|
||||
if no_binary {
|
||||
continue;
|
||||
};
|
||||
|
||||
// To be compatible, the wheel must both have compatible tags _and_ have a
|
||||
// compatible Python requirement.
|
||||
let priority = filename.compatibility(tags).filter(|_| {
|
||||
|
|
|
@ -21,7 +21,7 @@ use puffin_resolver::{
|
|||
DisplayResolutionGraph, InMemoryIndex, Manifest, PreReleaseMode, ResolutionGraph,
|
||||
ResolutionMode, ResolutionOptions, Resolver,
|
||||
};
|
||||
use puffin_traits::{BuildContext, BuildKind, SetupPyStrategy, SourceBuildTrait};
|
||||
use puffin_traits::{BuildContext, BuildKind, NoBinary, SetupPyStrategy, SourceBuildTrait};
|
||||
|
||||
// Exclude any packages uploaded after this date.
|
||||
static EXCLUDE_NEWER: Lazy<DateTime<Utc>> = Lazy::new(|| {
|
||||
|
@ -54,6 +54,10 @@ impl BuildContext for DummyContext {
|
|||
false
|
||||
}
|
||||
|
||||
fn no_binary(&self) -> &NoBinary {
|
||||
&NoBinary::None
|
||||
}
|
||||
|
||||
fn setup_py_strategy(&self) -> SetupPyStrategy {
|
||||
SetupPyStrategy::default()
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue