Implement pip compatible --no-binary and --only-binary options (#1268)

Updates our `--no-binary` option and adds a `--only-binary` option for
compatibility with `pip` which uses `:all:`, `:none:` and `<name>` for
specifying packages.

This required adding support for `--only-binary <name>` into our
resolver, previously it was only a boolean toggle.

Retains`--no-build` which is equivalent to `--only-binary :all:`. This
is common enough for safety that I would prefer it is available without
pip's awkward `:all:` syntax.

---------

Co-authored-by: konsti <konstin@mailbox.org>
This commit is contained in:
Zanie Blue 2024-02-11 19:31:41 -06:00 committed by GitHub
parent d98b3c3070
commit a37b08808e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 440 additions and 141 deletions

View file

@ -132,7 +132,7 @@ pub enum BuiltDist {
Path(PathBuiltDist), Path(PathBuiltDist),
} }
/// A source distribution, with its three possible origins (index, url, path, git) /// A source distribution, with its possible origins (index, url, path, git)
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
pub enum SourceDist { pub enum SourceDist {

View file

@ -14,7 +14,7 @@ use puffin_dispatch::BuildDispatch;
use puffin_installer::NoBinary; use puffin_installer::NoBinary;
use puffin_interpreter::Virtualenv; use puffin_interpreter::Virtualenv;
use puffin_resolver::InMemoryIndex; use puffin_resolver::InMemoryIndex;
use puffin_traits::{BuildContext, BuildKind, InFlight, SetupPyStrategy}; use puffin_traits::{BuildContext, BuildKind, InFlight, NoBuild, SetupPyStrategy};
#[derive(Parser)] #[derive(Parser)]
pub(crate) struct BuildArgs { pub(crate) struct BuildArgs {
@ -72,7 +72,7 @@ pub(crate) async fn build(args: BuildArgs) -> Result<PathBuf> {
&in_flight, &in_flight,
venv.python_executable(), venv.python_executable(),
setup_py, setup_py,
false, &NoBuild::None,
&NoBinary::None, &NoBinary::None,
); );

View file

@ -25,7 +25,7 @@ use puffin_installer::{Downloader, NoBinary};
use puffin_interpreter::Virtualenv; use puffin_interpreter::Virtualenv;
use puffin_normalize::PackageName; use puffin_normalize::PackageName;
use puffin_resolver::{DistFinder, InMemoryIndex}; use puffin_resolver::{DistFinder, InMemoryIndex};
use puffin_traits::{BuildContext, InFlight, SetupPyStrategy}; use puffin_traits::{BuildContext, InFlight, NoBuild, SetupPyStrategy};
#[derive(Parser)] #[derive(Parser)]
pub(crate) struct InstallManyArgs { pub(crate) struct InstallManyArgs {
@ -66,6 +66,12 @@ pub(crate) async fn install_many(args: InstallManyArgs) -> Result<()> {
let in_flight = InFlight::default(); let in_flight = InFlight::default();
let tags = venv.interpreter().tags()?; let tags = venv.interpreter().tags()?;
let no_build = if args.no_build {
NoBuild::All
} else {
NoBuild::None
};
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
&cache, &cache,
@ -76,7 +82,7 @@ pub(crate) async fn install_many(args: InstallManyArgs) -> Result<()> {
&in_flight, &in_flight,
venv.python_executable(), venv.python_executable(),
setup_py, setup_py,
args.no_build, &no_build,
&NoBinary::None, &NoBinary::None,
); );
@ -129,7 +135,9 @@ async fn install_chunk(
info!("Failed to find {} wheel(s)", failures.len()); info!("Failed to find {} wheel(s)", failures.len());
} }
let wheels_and_source_dist = resolution.len(); let wheels_and_source_dist = resolution.len();
let resolution = if build_dispatch.no_build() { let resolution = if build_dispatch.no_build().is_none() {
resolution
} else {
let only_wheels: FxHashMap<_, _> = resolution let only_wheels: FxHashMap<_, _> = resolution
.into_iter() .into_iter()
.filter(|(_, dist)| match dist { .filter(|(_, dist)| match dist {
@ -142,9 +150,8 @@ async fn install_chunk(
wheels_and_source_dist - only_wheels.len() wheels_and_source_dist - only_wheels.len()
); );
only_wheels only_wheels
} else {
resolution
}; };
let dists = Resolution::new(resolution) let dists = Resolution::new(resolution)
.into_distributions() .into_distributions()
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View file

@ -18,7 +18,7 @@ use puffin_dispatch::BuildDispatch;
use puffin_installer::NoBinary; use puffin_installer::NoBinary;
use puffin_interpreter::Virtualenv; use puffin_interpreter::Virtualenv;
use puffin_resolver::{InMemoryIndex, Manifest, Options, Resolver}; use puffin_resolver::{InMemoryIndex, Manifest, Options, Resolver};
use puffin_traits::{InFlight, SetupPyStrategy}; use puffin_traits::{InFlight, NoBuild, SetupPyStrategy};
#[derive(ValueEnum, Default, Clone)] #[derive(ValueEnum, Default, Clone)]
pub(crate) enum ResolveCliFormat { pub(crate) enum ResolveCliFormat {
@ -69,6 +69,12 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> {
let index = InMemoryIndex::default(); let index = InMemoryIndex::default();
let in_flight = InFlight::default(); let in_flight = InFlight::default();
let no_build = if args.no_build {
NoBuild::All
} else {
NoBuild::None
};
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
&cache, &cache,
@ -79,7 +85,7 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> {
&in_flight, &in_flight,
venv.python_executable(), venv.python_executable(),
SetupPyStrategy::default(), SetupPyStrategy::default(),
args.no_build, &no_build,
&NoBinary::None, &NoBinary::None,
); );

View file

@ -21,7 +21,7 @@ use puffin_installer::NoBinary;
use puffin_interpreter::Virtualenv; use puffin_interpreter::Virtualenv;
use puffin_normalize::PackageName; use puffin_normalize::PackageName;
use puffin_resolver::InMemoryIndex; use puffin_resolver::InMemoryIndex;
use puffin_traits::{BuildContext, InFlight, SetupPyStrategy}; use puffin_traits::{BuildContext, InFlight, NoBuild, SetupPyStrategy};
#[derive(Parser)] #[derive(Parser)]
pub(crate) struct ResolveManyArgs { pub(crate) struct ResolveManyArgs {
@ -82,6 +82,12 @@ pub(crate) async fn resolve_many(args: ResolveManyArgs) -> Result<()> {
header_span.pb_set_length(total as u64); header_span.pb_set_length(total as u64);
let _header_span_enter = header_span.enter(); let _header_span_enter = header_span.enter();
let no_build = if args.no_build {
NoBuild::All
} else {
NoBuild::None
};
let mut tasks = futures::stream::iter(requirements) let mut tasks = futures::stream::iter(requirements)
.map(|requirement| { .map(|requirement| {
async { async {
@ -102,7 +108,7 @@ pub(crate) async fn resolve_many(args: ResolveManyArgs) -> Result<()> {
&in_flight, &in_flight,
venv.python_executable(), venv.python_executable(),
setup_py, setup_py,
args.no_build, &no_build,
&NoBinary::None, &NoBinary::None,
); );

View file

@ -9,7 +9,7 @@ use anyhow::{bail, Context, Result};
use itertools::Itertools; use itertools::Itertools;
use tracing::{debug, instrument}; use tracing::{debug, instrument};
use distribution_types::{IndexLocations, Name, Resolution}; use distribution_types::{IndexLocations, Name, Resolution, SourceDist};
use futures::FutureExt; use futures::FutureExt;
use pep508_rs::Requirement; use pep508_rs::Requirement;
use puffin_build::{SourceBuild, SourceBuildContext}; use puffin_build::{SourceBuild, SourceBuildContext};
@ -18,7 +18,7 @@ use puffin_client::{FlatIndex, RegistryClient};
use puffin_installer::{Downloader, Installer, NoBinary, Plan, Planner, Reinstall, SitePackages}; use puffin_installer::{Downloader, Installer, NoBinary, Plan, Planner, Reinstall, SitePackages};
use puffin_interpreter::{Interpreter, Virtualenv}; use puffin_interpreter::{Interpreter, Virtualenv};
use puffin_resolver::{InMemoryIndex, Manifest, Options, Resolver}; use puffin_resolver::{InMemoryIndex, Manifest, Options, Resolver};
use puffin_traits::{BuildContext, BuildKind, InFlight, SetupPyStrategy}; use puffin_traits::{BuildContext, BuildKind, InFlight, NoBuild, SetupPyStrategy};
/// The main implementation of [`BuildContext`], used by the CLI, see [`BuildContext`] /// The main implementation of [`BuildContext`], used by the CLI, see [`BuildContext`]
/// documentation. /// documentation.
@ -32,7 +32,7 @@ pub struct BuildDispatch<'a> {
in_flight: &'a InFlight, in_flight: &'a InFlight,
base_python: PathBuf, base_python: PathBuf,
setup_py: SetupPyStrategy, setup_py: SetupPyStrategy,
no_build: bool, no_build: &'a NoBuild,
no_binary: &'a NoBinary, no_binary: &'a NoBinary,
source_build_context: SourceBuildContext, source_build_context: SourceBuildContext,
options: Options, options: Options,
@ -50,7 +50,7 @@ impl<'a> BuildDispatch<'a> {
in_flight: &'a InFlight, in_flight: &'a InFlight,
base_python: PathBuf, base_python: PathBuf,
setup_py: SetupPyStrategy, setup_py: SetupPyStrategy,
no_build: bool, no_build: &'a NoBuild,
no_binary: &'a NoBinary, no_binary: &'a NoBinary,
) -> Self { ) -> Self {
Self { Self {
@ -92,7 +92,7 @@ impl<'a> BuildContext for BuildDispatch<'a> {
&self.base_python &self.base_python
} }
fn no_build(&self) -> bool { fn no_build(&self) -> &NoBuild {
self.no_build self.no_build
} }
@ -246,10 +246,29 @@ impl<'a> BuildContext for BuildDispatch<'a> {
source: &'data Path, source: &'data Path,
subdirectory: Option<&'data Path>, subdirectory: Option<&'data Path>,
package_id: &'data str, package_id: &'data str,
dist: Option<&'data SourceDist>,
build_kind: BuildKind, build_kind: BuildKind,
) -> Result<SourceBuild> { ) -> Result<SourceBuild> {
if self.no_build { match self.no_build {
bail!("Building source distributions is disabled"); NoBuild::All => bail!("Building source distributions is disabled"),
NoBuild::None => {}
NoBuild::Packages(packages) => {
if let Some(dist) = dist {
// We can only prevent builds by name for packages with names
// which is unknown before build of editable source distributions
if packages.contains(dist.name()) {
bail!(
"Building source distributions for {} is disabled",
dist.name()
);
}
} else {
debug_assert!(
matches!(build_kind, BuildKind::Editable),
"Only editable builds are exempt from 'no build' checks"
);
}
}
} }
let builder = SourceBuild::setup( let builder = SourceBuild::setup(

View file

@ -16,7 +16,7 @@ use puffin_cache::{Cache, CacheBucket, Timestamp, WheelCache};
use puffin_client::{CacheControl, CachedClientError, RegistryClient}; use puffin_client::{CacheControl, CachedClientError, RegistryClient};
use puffin_fs::metadata_if_exists; use puffin_fs::metadata_if_exists;
use puffin_git::GitSource; use puffin_git::GitSource;
use puffin_traits::{BuildContext, NoBinary}; use puffin_traits::{BuildContext, NoBinary, NoBuild};
use pypi_types::Metadata21; use pypi_types::Metadata21;
use crate::download::{BuiltWheel, UnzippedWheel}; use crate::download::{BuiltWheel, UnzippedWheel};
@ -340,8 +340,13 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context>
Ok((self.client.wheel_metadata(built_dist).boxed().await?, None)) Ok((self.client.wheel_metadata(built_dist).boxed().await?, None))
} }
Dist::Source(source_dist) => { Dist::Source(source_dist) => {
let no_build = match self.build_context.no_build() {
NoBuild::All => true,
NoBuild::None => false,
NoBuild::Packages(packages) => packages.contains(source_dist.name()),
};
// Optimization: Skip source dist download when we must not build them anyway. // Optimization: Skip source dist download when we must not build them anyway.
if self.build_context.no_build() { if no_build {
return Err(Error::NoBuild); return Err(Error::NoBuild);
} }

View file

@ -27,7 +27,7 @@ use puffin_cache::{
use puffin_client::{CacheControl, CachedClient, CachedClientError, DataWithCachePolicy}; use puffin_client::{CacheControl, CachedClient, CachedClientError, DataWithCachePolicy};
use puffin_fs::{write_atomic, LockedFile}; use puffin_fs::{write_atomic, LockedFile};
use puffin_git::{Fetch, GitSource}; use puffin_git::{Fetch, GitSource};
use puffin_traits::{BuildContext, BuildKind, SourceBuildTrait}; use puffin_traits::{BuildContext, BuildKind, NoBuild, SourceBuildTrait};
use pypi_types::Metadata21; use pypi_types::Metadata21;
use crate::error::Error; use crate::error::Error;
@ -823,7 +823,13 @@ impl<'a, T: BuildContext> SourceDistCachedBuilder<'a, T> {
) -> Result<(String, WheelFilename, Metadata21), Error> { ) -> Result<(String, WheelFilename, Metadata21), Error> {
debug!("Building: {dist}"); debug!("Building: {dist}");
if self.build_context.no_build() { // Guard against build of source distributions when disabled
let no_build = match self.build_context.no_build() {
NoBuild::All => true,
NoBuild::None => false,
NoBuild::Packages(packages) => packages.contains(dist.name()),
};
if no_build {
return Err(Error::NoBuild); return Err(Error::NoBuild);
} }
@ -837,6 +843,7 @@ impl<'a, T: BuildContext> SourceDistCachedBuilder<'a, T> {
source_dist, source_dist,
subdirectory, subdirectory,
&dist.to_string(), &dist.to_string(),
Some(dist),
BuildKind::Wheel, BuildKind::Wheel,
) )
.await .await
@ -878,6 +885,7 @@ impl<'a, T: BuildContext> SourceDistCachedBuilder<'a, T> {
source_dist, source_dist,
subdirectory, subdirectory,
&dist.to_string(), &dist.to_string(),
Some(dist),
BuildKind::Wheel, BuildKind::Wheel,
) )
.await .await
@ -923,6 +931,7 @@ impl<'a, T: BuildContext> SourceDistCachedBuilder<'a, T> {
&editable.path, &editable.path,
None, None,
&editable.to_string(), &editable.to_string(),
None,
BuildKind::Editable, BuildKind::Editable,
) )
.await .await

View file

@ -10,7 +10,7 @@ use anyhow::Result;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use distribution_types::{IndexLocations, Resolution}; use distribution_types::{IndexLocations, Resolution, SourceDist};
use pep508_rs::{MarkerEnvironment, Requirement, StringVersion}; use pep508_rs::{MarkerEnvironment, Requirement, StringVersion};
use platform_host::{Arch, Os, Platform}; use platform_host::{Arch, Os, Platform};
use platform_tags::Tags; use platform_tags::Tags;
@ -21,7 +21,9 @@ use puffin_resolver::{
DisplayResolutionGraph, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode, DisplayResolutionGraph, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode,
ResolutionGraph, ResolutionMode, Resolver, ResolutionGraph, ResolutionMode, Resolver,
}; };
use puffin_traits::{BuildContext, BuildKind, NoBinary, SetupPyStrategy, SourceBuildTrait}; use puffin_traits::{
BuildContext, BuildKind, NoBinary, NoBuild, SetupPyStrategy, SourceBuildTrait,
};
// Exclude any packages uploaded after this date. // Exclude any packages uploaded after this date.
static EXCLUDE_NEWER: Lazy<DateTime<Utc>> = Lazy::new(|| { static EXCLUDE_NEWER: Lazy<DateTime<Utc>> = Lazy::new(|| {
@ -61,8 +63,8 @@ impl BuildContext for DummyContext {
panic!("The test should not need to build source distributions") panic!("The test should not need to build source distributions")
} }
fn no_build(&self) -> bool { fn no_build(&self) -> &NoBuild {
false &NoBuild::None
} }
fn no_binary(&self) -> &NoBinary { fn no_binary(&self) -> &NoBinary {
@ -94,6 +96,7 @@ impl BuildContext for DummyContext {
_source: &'a Path, _source: &'a Path,
_subdirectory: Option<&'a Path>, _subdirectory: Option<&'a Path>,
_package_id: &'a str, _package_id: &'a str,
_dist: Option<&'a SourceDist>,
_build_kind: BuildKind, _build_kind: BuildKind,
) -> Result<Self::SourceDistBuilder> { ) -> Result<Self::SourceDistBuilder> {
Ok(DummyBuilder) Ok(DummyBuilder)

View file

@ -3,10 +3,11 @@
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::future::Future; use std::future::Future;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::Result; use anyhow::Result;
use distribution_types::{CachedDist, DistributionId, IndexLocations, Resolution}; use distribution_types::{CachedDist, DistributionId, IndexLocations, Resolution, SourceDist};
use once_map::OnceMap; use once_map::OnceMap;
use pep508_rs::Requirement; use pep508_rs::Requirement;
use puffin_cache::Cache; use puffin_cache::Cache;
@ -67,7 +68,7 @@ pub trait BuildContext: Sync {
/// Whether source distribution building is disabled. This [`BuildContext::setup_build`] calls /// Whether source distribution building is disabled. This [`BuildContext::setup_build`] calls
/// will fail in this case. This method exists to avoid fetching source distributions if we know /// will fail in this case. This method exists to avoid fetching source distributions if we know
/// we can't build them /// we can't build them
fn no_build(&self) -> bool; fn no_build(&self) -> &NoBuild;
/// Whether using pre-built wheels is disabled. /// Whether using pre-built wheels is disabled.
fn no_binary(&self) -> &NoBinary; fn no_binary(&self) -> &NoBinary;
@ -98,11 +99,13 @@ pub trait BuildContext: Sync {
/// For PEP 517 builds, this calls `get_requires_for_build_wheel`. /// For PEP 517 builds, this calls `get_requires_for_build_wheel`.
/// ///
/// `package_id` is for error reporting only. /// `package_id` is for error reporting only.
/// `dist` is for safety checks and may be null for editable builds.
fn setup_build<'a>( fn setup_build<'a>(
&'a self, &'a self,
source: &'a Path, source: &'a Path,
subdirectory: Option<&'a Path>, subdirectory: Option<&'a Path>,
package_id: &'a str, package_id: &'a str,
dist: Option<&'a SourceDist>,
build_kind: BuildKind, build_kind: BuildKind,
) -> impl Future<Output = Result<Self::SourceDistBuilder>> + Send + 'a; ) -> impl Future<Output = Result<Self::SourceDistBuilder>> + Send + 'a;
} }
@ -163,6 +166,62 @@ impl Display for BuildKind {
} }
} }
#[derive(Debug, Clone)]
pub enum PackageNameSpecifier {
All,
None,
Package(PackageName),
}
impl FromStr for PackageNameSpecifier {
type Err = puffin_normalize::InvalidNameError;
fn from_str(name: &str) -> Result<Self, Self::Err> {
match name {
":all:" => Ok(Self::All),
":none:" => Ok(Self::None),
_ => Ok(Self::Package(PackageName::from_str(name)?)),
}
}
}
#[derive(Debug, Clone)]
pub enum PackageNameSpecifiers {
All,
None,
Packages(Vec<PackageName>),
}
impl PackageNameSpecifiers {
fn from_iter(specifiers: impl Iterator<Item = PackageNameSpecifier>) -> Self {
let mut packages = Vec::new();
let mut all: bool = false;
for specifier in specifiers {
match specifier {
PackageNameSpecifier::None => {
packages.clear();
all = false;
}
PackageNameSpecifier::All => {
all = true;
}
PackageNameSpecifier::Package(name) => {
packages.push(name);
}
}
}
if all {
Self::All
} else if packages.is_empty() {
Self::None
} else {
Self::Packages(packages)
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum NoBinary { pub enum NoBinary {
/// Allow installation of any wheel. /// Allow installation of any wheel.
@ -177,13 +236,117 @@ pub enum NoBinary {
impl NoBinary { impl NoBinary {
/// Determine the binary installation strategy to use. /// Determine the binary installation strategy to use.
pub fn from_args(no_binary: bool, no_binary_package: Vec<PackageName>) -> Self { pub fn from_args(no_binary: Vec<PackageNameSpecifier>) -> Self {
if no_binary { let combined = PackageNameSpecifiers::from_iter(no_binary.into_iter());
Self::All match combined {
} else if !no_binary_package.is_empty() { PackageNameSpecifiers::All => Self::All,
Self::Packages(no_binary_package) PackageNameSpecifiers::None => Self::None,
} else { PackageNameSpecifiers::Packages(packages) => Self::Packages(packages),
Self::None
} }
} }
} }
impl NoBinary {
/// Returns `true` if all wheels are allowed.
pub fn is_none(&self) -> bool {
matches!(self, Self::None)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum NoBuild {
/// Allow building wheels from any source distribution.
None,
/// Do not allow building wheels from any source distribution.
All,
/// Do not allow building wheels from the given package's source distributions.
Packages(Vec<PackageName>),
}
impl NoBuild {
/// Determine the build strategy to use.
pub fn from_args(only_binary: Vec<PackageNameSpecifier>, no_build: bool) -> Self {
if no_build {
Self::All
} else {
let combined = PackageNameSpecifiers::from_iter(only_binary.into_iter());
match combined {
PackageNameSpecifiers::All => Self::All,
PackageNameSpecifiers::None => Self::None,
PackageNameSpecifiers::Packages(packages) => Self::Packages(packages),
}
}
}
}
impl NoBuild {
/// Returns `true` if all builds are allowed.
pub fn is_none(&self) -> bool {
matches!(self, Self::None)
}
}
#[cfg(test)]
mod tests {
use anyhow::Error;
use super::*;
#[test]
fn no_build_from_args() -> Result<(), Error> {
assert_eq!(
NoBuild::from_args(vec![PackageNameSpecifier::from_str(":all:")?], false),
NoBuild::All,
);
assert_eq!(
NoBuild::from_args(vec![PackageNameSpecifier::from_str(":all:")?], true),
NoBuild::All,
);
assert_eq!(
NoBuild::from_args(vec![PackageNameSpecifier::from_str(":none:")?], true),
NoBuild::All,
);
assert_eq!(
NoBuild::from_args(vec![PackageNameSpecifier::from_str(":none:")?], false),
NoBuild::None,
);
assert_eq!(
NoBuild::from_args(
vec![
PackageNameSpecifier::from_str("foo")?,
PackageNameSpecifier::from_str("bar")?
],
false
),
NoBuild::Packages(vec![
PackageName::from_str("foo")?,
PackageName::from_str("bar")?
]),
);
assert_eq!(
NoBuild::from_args(
vec![
PackageNameSpecifier::from_str("test")?,
PackageNameSpecifier::All
],
false
),
NoBuild::All,
);
assert_eq!(
NoBuild::from_args(
vec![
PackageNameSpecifier::from_str("foo")?,
PackageNameSpecifier::from_str(":none:")?,
PackageNameSpecifier::from_str("bar")?
],
false
),
NoBuild::Packages(vec![PackageName::from_str("bar")?]),
);
Ok(())
}
}

View file

@ -30,7 +30,7 @@ use puffin_resolver::{
DisplayResolutionGraph, InMemoryIndex, Manifest, OptionsBuilder, PreReleaseMode, DisplayResolutionGraph, InMemoryIndex, Manifest, OptionsBuilder, PreReleaseMode,
ResolutionMode, Resolver, ResolutionMode, Resolver,
}; };
use puffin_traits::{InFlight, SetupPyStrategy}; use puffin_traits::{InFlight, NoBuild, SetupPyStrategy};
use puffin_warnings::warn_user; use puffin_warnings::warn_user;
use requirements_txt::EditableRequirement; use requirements_txt::EditableRequirement;
@ -59,7 +59,7 @@ pub(crate) async fn pip_compile(
include_find_links: bool, include_find_links: bool,
index_locations: IndexLocations, index_locations: IndexLocations,
setup_py: SetupPyStrategy, setup_py: SetupPyStrategy,
no_build: bool, no_build: &NoBuild,
python_version: Option<PythonVersion>, python_version: Option<PythonVersion>,
exclude_newer: Option<DateTime<Utc>>, exclude_newer: Option<DateTime<Utc>>,
cache: Cache, cache: Cache,
@ -152,7 +152,7 @@ pub(crate) async fn pip_compile(
python_version.major() == interpreter.python_major() python_version.major() == interpreter.python_major()
&& python_version.minor() == interpreter.python_minor() && python_version.minor() == interpreter.python_minor()
}; };
if !no_build if no_build.is_none()
&& python_version.version() != interpreter.python_version() && python_version.version() != interpreter.python_version()
&& (python_version.patch().is_some() || !matches_without_patch) && (python_version.patch().is_some() || !matches_without_patch)
{ {

View file

@ -29,7 +29,7 @@ use puffin_resolver::{
DependencyMode, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode, DependencyMode, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode,
ResolutionGraph, ResolutionMode, Resolver, ResolutionGraph, ResolutionMode, Resolver,
}; };
use puffin_traits::{InFlight, SetupPyStrategy}; use puffin_traits::{InFlight, NoBuild, SetupPyStrategy};
use pypi_types::Yanked; use pypi_types::Yanked;
use requirements_txt::EditableRequirement; use requirements_txt::EditableRequirement;
@ -52,7 +52,7 @@ pub(crate) async fn pip_install(
reinstall: &Reinstall, reinstall: &Reinstall,
link_mode: LinkMode, link_mode: LinkMode,
setup_py: SetupPyStrategy, setup_py: SetupPyStrategy,
no_build: bool, no_build: &NoBuild,
no_binary: &NoBinary, no_binary: &NoBinary,
strict: bool, strict: bool,
exclude_newer: Option<DateTime<Utc>>, exclude_newer: Option<DateTime<Utc>>,

View file

@ -18,7 +18,7 @@ use puffin_installer::{
}; };
use puffin_interpreter::Virtualenv; use puffin_interpreter::Virtualenv;
use puffin_resolver::InMemoryIndex; use puffin_resolver::InMemoryIndex;
use puffin_traits::{InFlight, SetupPyStrategy}; use puffin_traits::{InFlight, NoBuild, SetupPyStrategy};
use pypi_types::Yanked; use pypi_types::Yanked;
use requirements_txt::EditableRequirement; use requirements_txt::EditableRequirement;
@ -35,7 +35,7 @@ pub(crate) async fn pip_sync(
link_mode: LinkMode, link_mode: LinkMode,
index_locations: IndexLocations, index_locations: IndexLocations,
setup_py: SetupPyStrategy, setup_py: SetupPyStrategy,
no_build: bool, no_build: &NoBuild,
no_binary: &NoBinary, no_binary: &NoBinary,
strict: bool, strict: bool,
cache: Cache, cache: Cache,

View file

@ -18,7 +18,7 @@ use puffin_fs::Normalized;
use puffin_installer::NoBinary; use puffin_installer::NoBinary;
use puffin_interpreter::{find_default_python, find_requested_python, Error}; use puffin_interpreter::{find_default_python, find_requested_python, Error};
use puffin_resolver::InMemoryIndex; use puffin_resolver::InMemoryIndex;
use puffin_traits::{BuildContext, InFlight, SetupPyStrategy}; use puffin_traits::{BuildContext, InFlight, NoBuild, SetupPyStrategy};
use crate::commands::ExitStatus; use crate::commands::ExitStatus;
use crate::printer::Printer; use crate::printer::Printer;
@ -135,7 +135,7 @@ async fn venv_impl(
&in_flight, &in_flight,
venv.python_executable(), venv.python_executable(),
SetupPyStrategy::default(), SetupPyStrategy::default(),
true, &NoBuild::All,
&NoBinary::None, &NoBinary::None,
); );

View file

@ -17,7 +17,7 @@ use puffin_installer::{NoBinary, Reinstall};
use puffin_interpreter::PythonVersion; use puffin_interpreter::PythonVersion;
use puffin_normalize::{ExtraName, PackageName}; use puffin_normalize::{ExtraName, PackageName};
use puffin_resolver::{DependencyMode, PreReleaseMode, ResolutionMode}; use puffin_resolver::{DependencyMode, PreReleaseMode, ResolutionMode};
use puffin_traits::SetupPyStrategy; use puffin_traits::{NoBuild, PackageNameSpecifier, SetupPyStrategy};
use requirements::ExtrasSpecification; use requirements::ExtrasSpecification;
use crate::commands::{extra_name_with_clap_error, ExitStatus, Upgrade}; use crate::commands::{extra_name_with_clap_error, ExitStatus, Upgrade};
@ -259,9 +259,22 @@ struct PipCompileArgs {
/// When enabled, resolving will not run arbitrary code. The cached wheels of already-built /// When enabled, resolving will not run arbitrary code. The cached wheels of already-built
/// source distributions will be reused, but operations that require building distributions will /// source distributions will be reused, but operations that require building distributions will
/// exit with an error. /// exit with an error.
#[clap(long)] ///
/// Alias for `--only-binary :all:`.
#[clap(long, conflicts_with = "only_binary")]
no_build: bool, no_build: bool,
/// Only use pre-built wheels; don't build source distributions.
///
/// When enabled, resolving will not run code from the given packages. The cached wheels of already-built
/// source distributions will be reused, but operations that require building distributions will
/// exit with an error.
///
/// Multiple packages may be provided. Disable binaries for all packages with `:all:`.
/// Clear previously specified packages with `:none:`.
#[clap(long, conflicts_with = "no_build")]
only_binary: Vec<PackageNameSpecifier>,
/// The minimum Python version that should be supported by the compiled requirements (e.g., /// The minimum Python version that should be supported by the compiled requirements (e.g.,
/// `3.7` or `3.7.9`). /// `3.7` or `3.7.9`).
/// ///
@ -353,22 +366,31 @@ struct PipSyncArgs {
/// When enabled, resolving will not run arbitrary code. The cached wheels of already-built /// When enabled, resolving will not run arbitrary code. The cached wheels of already-built
/// source distributions will be reused, but operations that require building distributions will /// source distributions will be reused, but operations that require building distributions will
/// exit with an error. /// exit with an error.
#[clap(long)] ///
/// Alias for `--only-binary :all:`.
#[clap(long, conflicts_with = "no_binary", conflicts_with = "only_binary")]
no_build: bool, no_build: bool,
/// Don't install pre-built wheels. /// Don't install pre-built wheels.
/// ///
/// When enabled, all installed packages will be installed from a source distribution. The resolver /// The given packages will be installed from a source distribution. The resolver
/// will still use pre-built wheels for metadata. /// will still use pre-built wheels for metadata.
#[clap(long)]
no_binary: bool,
/// Don't install pre-built wheels for a specific package.
/// ///
/// When enabled, the specified packages will be installed from a source distribution. The resolver /// Multiple packages may be provided. Disable binaries for all packages with `:all:`.
/// will still use pre-built wheels for metadata. /// Clear previously specified packages with `:none:`.
#[clap(long)] #[clap(long, conflicts_with = "no_build")]
no_binary_package: Vec<PackageName>, no_binary: Vec<PackageNameSpecifier>,
/// Only use pre-built wheels; don't build source distributions.
///
/// When enabled, resolving will not run code from the given packages. The cached wheels of already-built
/// source distributions will be reused, but operations that require building distributions will
/// exit with an error.
///
/// Multiple packages may be provided. Disable binaries for all packages with `:all:`.
/// Clear previously specified packages with `:none:`.
#[clap(long, conflicts_with = "no_build")]
only_binary: Vec<PackageNameSpecifier>,
/// Validate the virtual environment after completing the installation, to detect packages with /// Validate the virtual environment after completing the installation, to detect packages with
/// missing dependencies or other issues. /// missing dependencies or other issues.
@ -492,22 +514,31 @@ struct PipInstallArgs {
/// When enabled, resolving will not run arbitrary code. The cached wheels of already-built /// When enabled, resolving will not run arbitrary code. The cached wheels of already-built
/// source distributions will be reused, but operations that require building distributions will /// source distributions will be reused, but operations that require building distributions will
/// exit with an error. /// exit with an error.
#[clap(long)] ///
/// Alias for `--only-binary :all:`.
#[clap(long, conflicts_with = "no_binary", conflicts_with = "only_binary")]
no_build: bool, no_build: bool,
/// Don't install pre-built wheels. /// Don't install pre-built wheels.
/// ///
/// When enabled, all installed packages will be installed from a source distribution. The resolver /// The given packages will be installed from a source distribution. The resolver
/// will still use pre-built wheels for metadata. /// will still use pre-built wheels for metadata.
#[clap(long)]
no_binary: bool,
/// Don't install pre-built wheels for a specific package.
/// ///
/// When enabled, the specified packages will be installed from a source distribution. The resolver /// Multiple packages may be provided. Disable binaries for all packages with `:all:`.
/// will still use pre-built wheels for metadata. /// Clear previously specified packages with `:none:`.
#[clap(long)] #[clap(long, conflicts_with = "no_build")]
no_binary_package: Vec<PackageName>, no_binary: Vec<PackageNameSpecifier>,
/// Only use pre-built wheels; don't build source distributions.
///
/// When enabled, resolving will not run code from the given packages. The cached wheels of already-built
/// source distributions will be reused, but operations that require building distributions will
/// exit with an error.
///
/// Multiple packages may be provided. Disable binaries for all packages with `:all:`.
/// Clear previously specified packages with `:none:`.
#[clap(long, conflicts_with = "no_build")]
only_binary: Vec<PackageNameSpecifier>,
/// Validate the virtual environment after completing the installation, to detect packages with /// Validate the virtual environment after completing the installation, to detect packages with
/// missing dependencies or other issues. /// missing dependencies or other issues.
@ -741,6 +772,7 @@ async fn run() -> Result<ExitStatus> {
ExtrasSpecification::Some(&args.extra) ExtrasSpecification::Some(&args.extra)
}; };
let upgrade = Upgrade::from_args(args.upgrade, args.upgrade_package); let upgrade = Upgrade::from_args(args.upgrade, args.upgrade_package);
let no_build = NoBuild::from_args(args.only_binary, args.no_build);
commands::pip_compile( commands::pip_compile(
&requirements, &requirements,
&constraints, &constraints,
@ -761,7 +793,7 @@ async fn run() -> Result<ExitStatus> {
} else { } else {
SetupPyStrategy::Pep517 SetupPyStrategy::Pep517
}, },
args.no_build, &no_build,
args.python_version, args.python_version,
args.exclude_newer, args.exclude_newer,
cache, cache,
@ -787,7 +819,8 @@ async fn run() -> Result<ExitStatus> {
.map(RequirementsSource::from_path) .map(RequirementsSource::from_path)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let reinstall = Reinstall::from_args(args.reinstall, args.reinstall_package); let reinstall = Reinstall::from_args(args.reinstall, args.reinstall_package);
let no_binary = NoBinary::from_args(args.no_binary, args.no_binary_package); let no_binary = NoBinary::from_args(args.no_binary);
let no_build = NoBuild::from_args(args.only_binary, args.no_build);
commands::pip_sync( commands::pip_sync(
&sources, &sources,
&reinstall, &reinstall,
@ -798,7 +831,7 @@ async fn run() -> Result<ExitStatus> {
} else { } else {
SetupPyStrategy::Pep517 SetupPyStrategy::Pep517
}, },
args.no_build, &no_build,
&no_binary, &no_binary,
args.strict, args.strict,
cache, cache,
@ -845,7 +878,8 @@ async fn run() -> Result<ExitStatus> {
ExtrasSpecification::Some(&args.extra) ExtrasSpecification::Some(&args.extra)
}; };
let reinstall = Reinstall::from_args(args.reinstall, args.reinstall_package); let reinstall = Reinstall::from_args(args.reinstall, args.reinstall_package);
let no_binary = NoBinary::from_args(args.no_binary, args.no_binary_package); let no_binary = NoBinary::from_args(args.no_binary);
let no_build = NoBuild::from_args(args.only_binary, args.no_build);
let dependency_mode = if args.no_deps { let dependency_mode = if args.no_deps {
DependencyMode::Direct DependencyMode::Direct
} else { } else {
@ -867,7 +901,7 @@ async fn run() -> Result<ExitStatus> {
} else { } else {
SetupPyStrategy::Pep517 SetupPyStrategy::Pep517
}, },
args.no_build, &no_build,
&no_binary, &no_binary,
args.strict, args.strict,
args.exclude_newer, args.exclude_newer,

View file

@ -668,7 +668,11 @@ fn install_no_binary() {
let context = TestContext::new("3.12"); let context = TestContext::new("3.12");
let mut command = command(&context); let mut command = command(&context);
command.arg("Flask").arg("--no-binary").arg("--strict"); command
.arg("jinja2")
.arg("--no-binary")
.arg(":all:")
.arg("--strict");
if cfg!(all(windows, debug_assertions)) { if cfg!(all(windows, debug_assertions)) {
// TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the
// default windows stack of 1MB // default windows stack of 1MB
@ -680,20 +684,15 @@ fn install_no_binary() {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
Resolved 7 packages in [TIME] Resolved 2 packages in [TIME]
Downloaded 7 packages in [TIME] Downloaded 2 packages in [TIME]
Installed 7 packages in [TIME] Installed 2 packages in [TIME]
+ blinker==1.7.0
+ click==8.1.7
+ flask==3.0.0
+ itsdangerous==2.1.2
+ jinja2==3.1.2 + jinja2==3.1.2
+ markupsafe==2.1.3 + markupsafe==2.1.3
+ werkzeug==3.0.1
"### "###
); );
context.assert_command("import flask").success(); context.assert_command("import jinja2").success();
} }
/// Install a package without using pre-built wheels for a subset of packages. /// Install a package without using pre-built wheels for a subset of packages.
@ -702,31 +701,78 @@ fn install_no_binary_subset() {
let context = TestContext::new("3.12"); let context = TestContext::new("3.12");
puffin_snapshot!(command(&context) puffin_snapshot!(command(&context)
.arg("Flask") .arg("jinja2")
.arg("--no-binary-package") .arg("--no-binary")
.arg("click") .arg("jinja2")
.arg("--no-binary-package") .arg("--no-binary")
.arg("flask") .arg("markupsafe")
.arg("--strict"), @r###" .arg("--strict"), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
Resolved 7 packages in [TIME] Resolved 2 packages in [TIME]
Downloaded 7 packages in [TIME] Downloaded 2 packages in [TIME]
Installed 7 packages in [TIME] Installed 2 packages in [TIME]
+ blinker==1.7.0
+ click==8.1.7
+ flask==3.0.0
+ itsdangerous==2.1.2
+ jinja2==3.1.2 + jinja2==3.1.2
+ markupsafe==2.1.3 + markupsafe==2.1.3
+ werkzeug==3.0.1
"### "###
); );
context.assert_command("import flask").success(); context.assert_command("import jinja2").success();
}
/// Install a package only using pre-built wheels.
#[test]
fn install_only_binary() {
let context = TestContext::new("3.12");
puffin_snapshot!(command(&context)
.arg("jinja2")
.arg("--only-binary")
.arg(":all:")
.arg("--strict"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Downloaded 2 packages in [TIME]
Installed 2 packages in [TIME]
+ jinja2==3.1.2
+ markupsafe==2.1.3
"###
);
context.assert_command("import jinja2").success();
}
/// Install a package only using pre-built wheels for a subset of packages.
#[test]
fn install_only_binary_subset() {
let context = TestContext::new("3.12");
puffin_snapshot!(command(&context)
.arg("jinja2")
.arg("--only-binary")
.arg("markupsafe")
.arg("--strict"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Downloaded 2 packages in [TIME]
Installed 2 packages in [TIME]
+ jinja2==3.1.2
+ markupsafe==2.1.3
"###
);
context.assert_command("import jinja2").success();
} }
/// Install a package without using pre-built wheels. /// Install a package without using pre-built wheels.
@ -736,7 +782,7 @@ fn reinstall_no_binary() {
// The first installation should use a pre-built wheel // The first installation should use a pre-built wheel
let mut command = command(&context); let mut command = command(&context);
command.arg("Flask").arg("--strict"); command.arg("jinja2").arg("--strict");
if cfg!(all(windows, debug_assertions)) { if cfg!(all(windows, debug_assertions)) {
// TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the
// default windows stack of 1MB // default windows stack of 1MB
@ -748,25 +794,24 @@ fn reinstall_no_binary() {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
Resolved 7 packages in [TIME] Resolved 2 packages in [TIME]
Downloaded 7 packages in [TIME] Downloaded 2 packages in [TIME]
Installed 7 packages in [TIME] Installed 2 packages in [TIME]
+ blinker==1.7.0
+ click==8.1.7
+ flask==3.0.0
+ itsdangerous==2.1.2
+ jinja2==3.1.2 + jinja2==3.1.2
+ markupsafe==2.1.3 + markupsafe==2.1.3
+ werkzeug==3.0.1
"### "###
); );
context.assert_command("import flask").success(); context.assert_command("import jinja2").success();
// Running installation again with `--no-binary` should be a no-op // Running installation again with `--no-binary` should be a no-op
// The first installation should use a pre-built wheel // The first installation should use a pre-built wheel
let mut command = crate::command(&context); let mut command = crate::command(&context);
command.arg("Flask").arg("--no-binary").arg("--strict"); command
.arg("jinja2")
.arg("--no-binary")
.arg(":all:")
.arg("--strict");
if cfg!(all(windows, debug_assertions)) { if cfg!(all(windows, debug_assertions)) {
// TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the
// default windows stack of 1MB // default windows stack of 1MB
@ -782,7 +827,7 @@ fn reinstall_no_binary() {
"### "###
); );
context.assert_command("import flask").success(); context.assert_command("import jinja2").success();
// With `--reinstall`, `--no-binary` should have an affect // With `--reinstall`, `--no-binary` should have an affect
let filters = if cfg!(windows) { let filters = if cfg!(windows) {
@ -797,10 +842,11 @@ fn reinstall_no_binary() {
}; };
let mut command = crate::command(&context); let mut command = crate::command(&context);
command command
.arg("Flask") .arg("jinja2")
.arg("--no-binary") .arg("--no-binary")
.arg(":all:")
.arg("--reinstall-package") .arg("--reinstall-package")
.arg("Flask") .arg("jinja2")
.arg("--strict"); .arg("--strict");
if cfg!(all(windows, debug_assertions)) { if cfg!(all(windows, debug_assertions)) {
// TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the
@ -813,14 +859,14 @@ fn reinstall_no_binary() {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
Resolved 7 packages in [TIME] Resolved 2 packages in [TIME]
Installed 1 package in [TIME] Installed 1 package in [TIME]
- flask==3.0.0 - jinja2==3.1.2
+ flask==3.0.0 + jinja2==3.1.2
"### "###
); );
context.assert_command("import flask").success(); context.assert_command("import jinja2").success();
} }
/// Install a package into a virtual environment, and ensuring that the executable permissions /// Install a package into a virtual environment, and ensuring that the executable permissions

View file

@ -821,6 +821,7 @@ fn install_no_binary() -> Result<()> {
command command
.arg("requirements.txt") .arg("requirements.txt")
.arg("--no-binary") .arg("--no-binary")
.arg(":all:")
.arg("--strict"); .arg("--strict");
if cfg!(all(windows, debug_assertions)) { if cfg!(all(windows, debug_assertions)) {
// TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the