diff --git a/Cargo.lock b/Cargo.lock index 75262925bdbe..9f048263b96f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2980,6 +2980,7 @@ dependencies = [ "uv-client", "uv-fs", "uv-normalize", + "uv-types", "uv-warnings", ] diff --git a/crates/requirements-txt/Cargo.toml b/crates/requirements-txt/Cargo.toml index 02851c40a4cf..47eb4ae37e8e 100644 --- a/crates/requirements-txt/Cargo.toml +++ b/crates/requirements-txt/Cargo.toml @@ -17,6 +17,7 @@ pep508_rs = { workspace = true, features = ["rkyv", "serde", "non-pep508-extensi uv-client = { workspace = true } uv-fs = { workspace = true } uv-normalize = { workspace = true } +uv-types = { workspace = true } uv-warnings = { workspace = true } async-recursion = { workspace = true } diff --git a/crates/requirements-txt/src/lib.rs b/crates/requirements-txt/src/lib.rs index 0e44ef0727ad..df509a1114f0 100644 --- a/crates/requirements-txt/src/lib.rs +++ b/crates/requirements-txt/src/lib.rs @@ -38,6 +38,7 @@ use std::borrow::Cow; use std::fmt::{Display, Formatter}; use std::io; use std::path::{Path, PathBuf}; +use std::str::FromStr; use async_recursion::async_recursion; use serde::{Deserialize, Serialize}; @@ -54,6 +55,7 @@ use uv_client::BaseClient; use uv_client::BaseClientBuilder; use uv_fs::{normalize_url_path, Simplified}; use uv_normalize::ExtraName; +use uv_types::{NoBinary, NoBuild, PackageNameSpecifier}; use uv_warnings::warn_user; /// We emit one of those for each requirements.txt entry @@ -82,6 +84,10 @@ enum RequirementsTxtStatement { FindLinks(FindLink), /// `--no-index` NoIndex, + /// `--no-binary` + NoBinary(NoBinary), + /// `only-binary` + OnlyBinary(NoBuild), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -328,6 +334,10 @@ pub struct RequirementsTxt { pub find_links: Vec, /// Whether to ignore the index, specified with `--no-index`. pub no_index: bool, + /// Whether to disallow wheels, specified with `--no-binary`. + pub no_binary: NoBinary, + /// Whether to allow only wheels, specified with `--only-binary`. + pub only_binary: NoBuild, } impl RequirementsTxt { @@ -516,6 +526,12 @@ impl RequirementsTxt { RequirementsTxtStatement::NoIndex => { data.no_index = true; } + RequirementsTxtStatement::NoBinary(no_binary) => { + data.no_binary.extend(no_binary); + } + RequirementsTxtStatement::OnlyBinary(only_binary) => { + data.only_binary.extend(only_binary); + } } } Ok(data) @@ -531,6 +547,8 @@ impl RequirementsTxt { extra_index_urls, find_links, no_index, + no_binary, + only_binary, } = other; self.requirements.extend(requirements); self.constraints.extend(constraints); @@ -541,6 +559,8 @@ impl RequirementsTxt { self.extra_index_urls.extend(extra_index_urls); self.find_links.extend(find_links); self.no_index = self.no_index || no_index; + self.no_binary.extend(no_binary); + self.only_binary.extend(only_binary); } } @@ -622,6 +642,28 @@ fn parse_entry( } })?; RequirementsTxtStatement::FindLinks(path_or_url) + } else if s.eat_if("--no-binary") { + let given = parse_value(content, s, |c: char| !['\n', '\r'].contains(&c))?; + let specifier = PackageNameSpecifier::from_str(given).map_err(|err| { + RequirementsTxtParserError::NoBinary { + source: err, + specifier: given.to_string(), + start, + end: s.cursor(), + } + })?; + RequirementsTxtStatement::NoBinary(NoBinary::from_arg(specifier)) + } else if s.eat_if("--only-binary") { + let given = parse_value(content, s, |c: char| !['\n', '\r'].contains(&c))?; + let specifier = PackageNameSpecifier::from_str(given).map_err(|err| { + RequirementsTxtParserError::NoBinary { + source: err, + specifier: given.to_string(), + start, + end: s.cursor(), + } + })?; + RequirementsTxtStatement::OnlyBinary(NoBuild::from_arg(specifier)) } else if s.at(char::is_ascii_alphanumeric) || s.at(|char| matches!(char, '.' | '/' | '$')) { let (requirement, hashes) = parse_requirement_and_hashes(s, content, working_dir)?; RequirementsTxtStatement::RequirementEntry(RequirementEntry { @@ -867,6 +909,18 @@ pub enum RequirementsTxtParserError { InvalidEditablePath(String), UnsupportedUrl(String), MissingRequirementPrefix(String), + NoBinary { + source: uv_normalize::InvalidNameError, + specifier: String, + start: usize, + end: usize, + }, + OnlyBinary { + source: uv_normalize::InvalidNameError, + specifier: String, + start: usize, + end: usize, + }, UnnamedConstraint { start: usize, end: usize, @@ -918,6 +972,28 @@ impl RequirementsTxtParserError { }, Self::UnsupportedUrl(url) => Self::UnsupportedUrl(url), Self::MissingRequirementPrefix(given) => Self::MissingRequirementPrefix(given), + Self::NoBinary { + source, + specifier, + start, + end, + } => Self::NoBinary { + source, + specifier, + start: start + offset, + end: end + offset, + }, + Self::OnlyBinary { + source, + specifier, + start, + end, + } => Self::OnlyBinary { + source, + specifier, + start: start + offset, + end: end + offset, + }, Self::UnnamedConstraint { start, end } => Self::UnnamedConstraint { start: start + offset, end: end + offset, @@ -969,6 +1045,12 @@ impl Display for RequirementsTxtParserError { Self::MissingRequirementPrefix(given) => { write!(f, "Requirement `{given}` looks like a requirements file but was passed as a package name. Did you mean `-r {given}`?") } + Self::NoBinary { specifier, .. } => { + write!(f, "Invalid specifier for `--no-binary`: {specifier}") + } + Self::OnlyBinary { specifier, .. } => { + write!(f, "Invalid specifier for `--only-binary`: {specifier}") + } Self::UnnamedConstraint { .. } => { write!(f, "Unnamed requirements are not allowed as constraints") } @@ -1011,6 +1093,8 @@ impl std::error::Error for RequirementsTxtParserError { Self::InvalidEditablePath(_) => None, Self::UnsupportedUrl(_) => None, Self::MissingRequirementPrefix(_) => None, + Self::NoBinary { source, .. } => Some(source), + Self::OnlyBinary { source, .. } => Some(source), Self::UnnamedConstraint { .. } => None, Self::UnsupportedRequirement { source, .. } => Some(source), Self::Pep508 { source, .. } => Some(source), @@ -1055,6 +1139,20 @@ impl Display for RequirementsTxtFileError { self.file.user_display(), ) } + RequirementsTxtParserError::NoBinary { specifier, .. } => { + write!( + f, + "Invalid specifier for `--no-binary` in `{}`: {specifier}", + self.file.user_display(), + ) + } + RequirementsTxtParserError::OnlyBinary { specifier, .. } => { + write!( + f, + "Invalid specifier for `--only-binary` in `{}`: {specifier}", + self.file.user_display(), + ) + } RequirementsTxtParserError::UnnamedConstraint { .. } => { write!( f, @@ -1653,6 +1751,69 @@ mod test { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, + } + "###); + + Ok(()) + } + + #[tokio::test] + async fn nested_no_binary() -> Result<()> { + let temp_dir = assert_fs::TempDir::new()?; + + let requirements_txt = temp_dir.child("requirements.txt"); + requirements_txt.write_str(indoc! {" + flask + --no-binary :none: + -r child.txt + "})?; + + let child = temp_dir.child("child.txt"); + child.write_str(indoc! {" + --no-binary flask + "})?; + + let requirements = RequirementsTxt::parse( + requirements_txt.path(), + temp_dir.path(), + &BaseClientBuilder::new(), + ) + .await + .unwrap(); + insta::assert_debug_snapshot!(requirements, @r###" + RequirementsTxt { + requirements: [ + RequirementEntry { + requirement: Pep508( + Requirement { + name: PackageName( + "flask", + ), + extras: [], + version_or_url: None, + marker: None, + }, + ), + hashes: [], + editable: false, + }, + ], + constraints: [], + editables: [], + index_url: None, + extra_index_urls: [], + find_links: [], + no_index: false, + no_binary: Packages( + [ + PackageName( + "flask", + ), + ], + ), + only_binary: None, } "###); @@ -1689,37 +1850,39 @@ mod test { .unwrap(); insta::assert_debug_snapshot!(requirements, @r###" - RequirementsTxt { - requirements: [], - constraints: [], - editables: [ - EditableRequirement { - url: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/foo/bar", - query: None, - fragment: None, - }, - given: Some( - "/foo/bar", - ), + RequirementsTxt { + requirements: [], + constraints: [], + editables: [ + EditableRequirement { + url: VerbatimUrl { + url: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "/foo/bar", + query: None, + fragment: None, }, - extras: [], - path: "/foo/bar", + given: Some( + "/foo/bar", + ), }, - ], - index_url: None, - extra_index_urls: [], - find_links: [], - no_index: true, - } - "###); + extras: [], + path: "/foo/bar", + }, + ], + index_url: None, + extra_index_urls: [], + find_links: [], + no_index: true, + no_binary: None, + only_binary: None, + } + "###); Ok(()) } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-basic.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-basic.txt.snap index cba9974a9848..aeefa208a7e9 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-basic.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-basic.txt.snap @@ -161,4 +161,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-a.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-a.txt.snap index e6ab4fe5a38f..3bae2fc6fa87 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-a.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-a.txt.snap @@ -75,4 +75,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-b.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-b.txt.snap index 99d4e0cfb11a..8c311f4b9edc 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-b.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-b.txt.snap @@ -61,4 +61,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-editable.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-editable.txt.snap index 80ebe3f5841a..e8a8a5090a99 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-editable.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-editable.txt.snap @@ -66,4 +66,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-empty.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-empty.txt.snap index 077583693d2f..0a143a451c7e 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-empty.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-empty.txt.snap @@ -10,4 +10,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-for-poetry.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-for-poetry.txt.snap index b8d33f818416..513d0925987d 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-for-poetry.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-for-poetry.txt.snap @@ -108,4 +108,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-a.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-a.txt.snap index 54a870e38300..f14751af9039 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-a.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-a.txt.snap @@ -50,4 +50,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-b.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-b.txt.snap index bf1b07b155e7..07f69a01f553 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-b.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-b.txt.snap @@ -25,4 +25,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-poetry-with-hashes.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-poetry-with-hashes.txt.snap index 0aacf9a39848..318f3047d5ff 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-poetry-with-hashes.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-poetry-with-hashes.txt.snap @@ -296,4 +296,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-small.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-small.txt.snap index 00d71b0de0f6..717c063d4fd6 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-small.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-small.txt.snap @@ -61,4 +61,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-whitespace.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-whitespace.txt.snap index 80ebe3f5841a..e8a8a5090a99 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-whitespace.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-whitespace.txt.snap @@ -66,4 +66,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-basic.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-basic.txt.snap index cba9974a9848..aeefa208a7e9 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-basic.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-basic.txt.snap @@ -161,4 +161,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-a.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-a.txt.snap index e6ab4fe5a38f..3bae2fc6fa87 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-a.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-a.txt.snap @@ -75,4 +75,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-b.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-b.txt.snap index 99d4e0cfb11a..8c311f4b9edc 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-b.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-b.txt.snap @@ -61,4 +61,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-empty.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-empty.txt.snap index 077583693d2f..0a143a451c7e 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-empty.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-empty.txt.snap @@ -10,4 +10,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-for-poetry.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-for-poetry.txt.snap index b8d33f818416..513d0925987d 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-for-poetry.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-for-poetry.txt.snap @@ -108,4 +108,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-a.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-a.txt.snap index 54a870e38300..f14751af9039 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-a.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-a.txt.snap @@ -50,4 +50,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-b.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-b.txt.snap index bf1b07b155e7..07f69a01f553 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-b.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-b.txt.snap @@ -25,4 +25,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-poetry-with-hashes.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-poetry-with-hashes.txt.snap index 0aacf9a39848..318f3047d5ff 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-poetry-with-hashes.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-poetry-with-hashes.txt.snap @@ -296,4 +296,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-small.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-small.txt.snap index 00d71b0de0f6..717c063d4fd6 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-small.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-small.txt.snap @@ -61,4 +61,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap index ee62fe8aa5ec..1e71a4be417b 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap @@ -93,4 +93,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-whitespace.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-whitespace.txt.snap index 80ebe3f5841a..e8a8a5090a99 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-whitespace.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-whitespace.txt.snap @@ -66,4 +66,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap index 2d9c23e15fa5..d78405047fca 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap @@ -93,4 +93,6 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + no_binary: None, + only_binary: None, } diff --git a/crates/uv-requirements/src/specification.rs b/crates/uv-requirements/src/specification.rs index 14c779879208..eb73de073e6f 100644 --- a/crates/uv-requirements/src/specification.rs +++ b/crates/uv-requirements/src/specification.rs @@ -11,6 +11,7 @@ use requirements_txt::{EditableRequirement, FindLink, RequirementsTxt}; use uv_client::BaseClientBuilder; use uv_fs::Simplified; use uv_normalize::{ExtraName, PackageName}; +use uv_types::{NoBinary, NoBuild}; use crate::pyproject::{Pep621Metadata, PyProjectToml}; use crate::{ExtrasSpecification, RequirementsSource}; @@ -39,6 +40,10 @@ pub struct RequirementsSpecification { pub no_index: bool, /// The `--find-links` locations to use for fetching packages. pub find_links: Vec, + /// The `--no-binary` flags to enforce when selecting distributions. + pub no_binary: NoBinary, + /// The `--no-build` flags to enforce when selecting distributions. + pub no_build: NoBuild, } impl RequirementsSpecification { @@ -65,6 +70,8 @@ impl RequirementsSpecification { extra_index_urls: vec![], no_index: false, find_links: vec![], + no_binary: NoBinary::default(), + no_build: NoBuild::default(), } } RequirementsSource::Editable(name) => { @@ -82,6 +89,8 @@ impl RequirementsSpecification { extra_index_urls: vec![], no_index: false, find_links: vec![], + no_binary: NoBinary::default(), + no_build: NoBuild::default(), } } RequirementsSource::RequirementsTxt(path) => { @@ -114,6 +123,8 @@ impl RequirementsSpecification { FindLink::Path(path) => FlatIndexLocation::Path(path), }) .collect(), + no_binary: requirements_txt.no_binary, + no_build: requirements_txt.only_binary, } } RequirementsSource::PyprojectToml(path) => { @@ -151,6 +162,8 @@ impl RequirementsSpecification { extra_index_urls: vec![], no_index: false, find_links: vec![], + no_binary: NoBinary::default(), + no_build: NoBuild::default(), } } else { let path = fs_err::canonicalize(path)?; @@ -172,6 +185,8 @@ impl RequirementsSpecification { extra_index_urls: vec![], no_index: false, find_links: vec![], + no_binary: NoBinary::default(), + no_build: NoBuild::default(), } } } @@ -195,6 +210,8 @@ impl RequirementsSpecification { extra_index_urls: vec![], no_index: false, find_links: vec![], + no_binary: NoBinary::default(), + no_build: NoBuild::default(), } } }) @@ -240,6 +257,8 @@ impl RequirementsSpecification { spec.no_index |= source.no_index; spec.extra_index_urls.extend(source.extra_index_urls); spec.find_links.extend(source.find_links); + spec.no_binary.extend(source.no_binary); + spec.no_build.extend(source.no_build); } // Read all constraints, treating _everything_ as a constraint. @@ -273,6 +292,8 @@ impl RequirementsSpecification { spec.no_index |= source.no_index; spec.extra_index_urls.extend(source.extra_index_urls); spec.find_links.extend(source.find_links); + spec.no_binary.extend(source.no_binary); + spec.no_build.extend(source.no_build); } // Read all overrides, treating both requirements _and_ constraints as overrides. @@ -306,6 +327,8 @@ impl RequirementsSpecification { spec.no_index |= source.no_index; spec.extra_index_urls.extend(source.extra_index_urls); spec.find_links.extend(source.find_links); + spec.no_binary.extend(source.no_binary); + spec.no_build.extend(source.no_build); } Ok(spec) diff --git a/crates/uv-types/src/build_options.rs b/crates/uv-types/src/build_options.rs index b9b5b8481a42..81b646576b30 100644 --- a/crates/uv-types/src/build_options.rs +++ b/crates/uv-types/src/build_options.rs @@ -47,9 +47,10 @@ impl Display for BuildKind { } } -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub enum NoBinary { /// Allow installation of any wheel. + #[default] None, /// Do not allow installation from any wheels. @@ -60,7 +61,7 @@ pub enum NoBinary { } impl NoBinary { - /// Determine the binary installation strategy to use. + /// Determine the binary installation strategy to use for the given arguments. pub fn from_args(no_binary: Vec) -> Self { let combined = PackageNameSpecifiers::from_iter(no_binary.into_iter()); match combined { @@ -69,6 +70,54 @@ impl NoBinary { PackageNameSpecifiers::Packages(packages) => Self::Packages(packages), } } + + /// Determine the binary installation strategy to use for the given argument. + pub fn from_arg(no_binary: PackageNameSpecifier) -> Self { + Self::from_args(vec![no_binary]) + } + + /// Combine a set of [`NoBinary`] values. + #[must_use] + pub fn combine(self, other: Self) -> Self { + match (self, other) { + // If both are `None`, the result is `None`. + (Self::None, Self::None) => Self::None, + // If either is `All`, the result is `All`. + (Self::All, _) | (_, Self::All) => Self::All, + // If one is `None`, the result is the other. + (Self::Packages(a), Self::None) => Self::Packages(a), + (Self::None, Self::Packages(b)) => Self::Packages(b), + // If both are `Packages`, the result is the union of the two. + (Self::Packages(mut a), Self::Packages(b)) => { + a.extend(b); + Self::Packages(a) + } + } + } + + /// Extend a [`NoBinary`] value with another. + pub fn extend(&mut self, other: Self) { + match (&mut *self, other) { + // If either is `All`, the result is `All`. + (Self::All, _) | (_, Self::All) => *self = Self::All, + // If both are `None`, the result is `None`. + (Self::None, Self::None) => { + // Nothing to do. + } + // If one is `None`, the result is the other. + (Self::Packages(_), Self::None) => { + // Nothing to do. + } + (Self::None, Self::Packages(b)) => { + // Take ownership of `b`. + *self = Self::Packages(b); + } + // If both are `Packages`, the result is the union of the two. + (Self::Packages(a), Self::Packages(b)) => { + a.extend(b); + } + } + } } impl NoBinary { @@ -78,9 +127,10 @@ impl NoBinary { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub enum NoBuild { /// Allow building wheels from any source distribution. + #[default] None, /// Do not allow building wheels from any source distribution. @@ -91,7 +141,7 @@ pub enum NoBuild { } impl NoBuild { - /// Determine the build strategy to use. + /// Determine the build strategy to use for the given arguments. pub fn from_args(only_binary: Vec, no_build: bool) -> Self { if no_build { Self::All @@ -104,6 +154,54 @@ impl NoBuild { } } } + + /// Determine the build strategy to use for the given argument. + pub fn from_arg(no_build: PackageNameSpecifier) -> Self { + Self::from_args(vec![no_build], false) + } + + /// Combine a set of [`NoBuild`] values. + #[must_use] + pub fn combine(self, other: Self) -> Self { + match (self, other) { + // If both are `None`, the result is `None`. + (Self::None, Self::None) => Self::None, + // If either is `All`, the result is `All`. + (Self::All, _) | (_, Self::All) => Self::All, + // If one is `None`, the result is the other. + (Self::Packages(a), Self::None) => Self::Packages(a), + (Self::None, Self::Packages(b)) => Self::Packages(b), + // If both are `Packages`, the result is the union of the two. + (Self::Packages(mut a), Self::Packages(b)) => { + a.extend(b); + Self::Packages(a) + } + } + } + + /// Extend a [`NoBuild`] value with another. + pub fn extend(&mut self, other: Self) { + match (&mut *self, other) { + // If either is `All`, the result is `All`. + (Self::All, _) | (_, Self::All) => *self = Self::All, + // If both are `None`, the result is `None`. + (Self::None, Self::None) => { + // Nothing to do. + } + // If one is `None`, the result is the other. + (Self::Packages(_), Self::None) => { + // Nothing to do. + } + (Self::None, Self::Packages(b)) => { + // Take ownership of `b`. + *self = Self::Packages(b); + } + // If both are `Packages`, the result is the union of the two. + (Self::Packages(a), Self::Packages(b)) => { + a.extend(b); + } + } + } } impl NoBuild { diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index 5ae5000fe675..5a8e4a188e66 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -68,7 +68,7 @@ pub(crate) async fn pip_compile( config_settings: ConfigSettings, connectivity: Connectivity, no_build_isolation: bool, - no_build: &NoBuild, + no_build: NoBuild, python_version: Option, exclude_newer: Option>, annotation_style: AnnotationStyle, @@ -105,6 +105,8 @@ pub(crate) async fn pip_compile( extra_index_urls, no_index, find_links, + no_binary: _, + no_build: specified_no_build, } = RequirementsSpecification::from_sources( requirements, constraints, @@ -231,6 +233,9 @@ pub(crate) async fn pip_compile( BuildIsolation::Isolated }; + // Combine the `--no-build` flags. + let no_build = no_build.combine(specified_no_build); + let build_dispatch = BuildDispatch::new( &client, &cache, @@ -242,7 +247,7 @@ pub(crate) async fn pip_compile( setup_py, &config_settings, build_isolation, - no_build, + &no_build, &NoBinary::None, ) .with_options(OptionsBuilder::new().exclude_newer(exclude_newer).build()); diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index 6202ea71eed8..4594dde2d489 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -70,8 +70,8 @@ pub(crate) async fn pip_install( connectivity: Connectivity, config_settings: &ConfigSettings, no_build_isolation: bool, - no_build: &NoBuild, - no_binary: &NoBinary, + no_build: NoBuild, + no_binary: NoBinary, strict: bool, exclude_newer: Option>, python: Option, @@ -100,6 +100,8 @@ pub(crate) async fn pip_install( extra_index_urls, no_index, find_links, + no_binary: specified_no_binary, + no_build: specified_no_build, extras: _, } = read_requirements( requirements, @@ -213,6 +215,10 @@ pub(crate) async fn pip_install( BuildIsolation::Isolated }; + // Combine the `--no-binary` and `--no-build` flags. + let no_binary = no_binary.combine(specified_no_binary); + let no_build = no_build.combine(specified_no_build); + // Create a shared in-memory index. let index = InMemoryIndex::default(); @@ -231,8 +237,8 @@ pub(crate) async fn pip_install( setup_py, config_settings, build_isolation, - no_build, - no_binary, + &no_build, + &no_binary, ) .with_options(OptionsBuilder::new().exclude_newer(exclude_newer).build()); @@ -336,8 +342,8 @@ pub(crate) async fn pip_install( setup_py, config_settings, build_isolation, - no_build, - no_binary, + &no_build, + &no_binary, ) .with_options(OptionsBuilder::new().exclude_newer(exclude_newer).build()) }; @@ -348,7 +354,7 @@ pub(crate) async fn pip_install( editables, site_packages, reinstall, - no_binary, + &no_binary, link_mode, compile, &index_locations, diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index 83f74e0806b8..32a6dd7f3fdd 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -49,8 +49,8 @@ pub(crate) async fn pip_sync( connectivity: Connectivity, config_settings: &ConfigSettings, no_build_isolation: bool, - no_build: &NoBuild, - no_binary: &NoBinary, + no_build: NoBuild, + no_binary: NoBinary, strict: bool, python: Option, system: bool, @@ -78,6 +78,8 @@ pub(crate) async fn pip_sync( extra_index_urls, no_index, find_links, + no_binary: specified_no_binary, + no_build: specified_no_build, } = RequirementsSpecification::from_simple_sources(sources, &client_builder).await?; // Validate that the requirements are non-empty. @@ -165,6 +167,10 @@ pub(crate) async fn pip_sync( BuildIsolation::Isolated }; + // Combine the `--no-binary` and `--no-build` flags. + let no_binary = no_binary.combine(specified_no_binary); + let no_build = no_build.combine(specified_no_build); + // Prep the build context. let build_dispatch = BuildDispatch::new( &client, @@ -177,8 +183,8 @@ pub(crate) async fn pip_sync( setup_py, config_settings, build_isolation, - no_build, - no_binary, + &no_build, + &no_binary, ); // Convert from unnamed to named requirements. @@ -231,7 +237,7 @@ pub(crate) async fn pip_sync( .build( site_packages, reinstall, - no_binary, + &no_binary, &index_locations, &cache, &venv, @@ -267,8 +273,8 @@ pub(crate) async fn pip_sync( &client, venv.interpreter(), &flat_index, - no_binary, - no_build, + &no_binary, + &no_build, ) .with_reporter(FinderReporter::from(printer).with_length(remote.len() as u64)); let resolution = wheel_finder.resolve(&remote).await?; diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 977968f97350..c119912a4c46 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -1538,7 +1538,7 @@ async fn run() -> Result { Connectivity::Online }, args.no_build_isolation, - &no_build, + no_build, args.python_version, args.exclude_newer, args.annotation_style, @@ -1594,8 +1594,8 @@ async fn run() -> Result { }, &config_settings, args.no_build_isolation, - &no_build, - &no_binary, + no_build, + no_binary, args.strict, args.python, args.system, @@ -1690,8 +1690,8 @@ async fn run() -> Result { }, &config_settings, args.no_build_isolation, - &no_build, - &no_binary, + no_build, + no_binary, args.strict, args.exclude_newer, args.python, diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index aede6f7d9bdc..7ba2056dc146 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -1408,6 +1408,38 @@ fn reinstall_no_binary() { context.assert_command("import anyio").success(); } +/// Respect `--only-binary` flags in `requirements.txt` +#[test] +fn only_binary_requirements_txt() { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str(indoc! {r" + django_allauth==0.51.0 + --only-binary django_allauth + " + }) + .unwrap(); + + uv_snapshot!(command(&context) + .arg("-r") + .arg("requirements.txt") + .arg("--strict"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because django-allauth==0.51.0 is unusable because no wheels + are usable and building from source is disabled and you require + django-allauth==0.51.0, we can conclude that the requirements are + unsatisfiable. + "### + ); +} + /// Install a package into a virtual environment, and ensuring that the executable permissions /// are retained. ///