Add a universal resolution mode to pip compile (#4505)

## Summary

This needs more tests and a few more changes, but checkpointing it for
now.
This commit is contained in:
Charlie Marsh 2024-06-25 17:28:50 -04:00 committed by GitHub
parent f2f48d339e
commit e1708689a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 186 additions and 27 deletions

View file

@ -568,6 +568,15 @@ pub struct PipCompileArgs {
#[arg(long)] #[arg(long)]
pub python_platform: Option<TargetTriple>, pub python_platform: Option<TargetTriple>,
/// Perform a universal resolution, attempting to generate a single `requirements.txt` output
/// file that is compatible with all operating systems, architectures and supported Python
/// versions.
#[arg(long, overrides_with("no_universal"))]
pub universal: bool,
#[arg(long, overrides_with("universal"), hide = true)]
pub no_universal: bool,
/// Specify a package to omit from the output resolution. Its dependencies will still be /// Specify a package to omit from the output resolution. Its dependencies will still be
/// included in the resolution. Equivalent to pip-compile's `--unsafe-package` option. /// included in the resolution. Equivalent to pip-compile's `--unsafe-package` option.
#[arg(long, alias = "unsafe-package")] #[arg(long, alias = "unsafe-package")]

View file

@ -1,5 +1,5 @@
use pep440_rs::VersionSpecifiers; use pep440_rs::{Operator, Version, VersionSpecifier, VersionSpecifiers};
use pep508_rs::StringVersion; use pep508_rs::{MarkerExpression, MarkerTree, MarkerValueVersion, StringVersion};
use uv_toolchain::{Interpreter, PythonVersion}; use uv_toolchain::{Interpreter, PythonVersion};
use crate::RequiresPython; use crate::RequiresPython;
@ -59,10 +59,32 @@ impl PythonRequirement {
self.target.as_ref() self.target.as_ref()
} }
/// Return the target version of Python as a "requires python" type, /// Return a [`MarkerTree`] representing the Python requirement.
/// if available. ///
pub(crate) fn requires_python(&self) -> Option<&RequiresPython> { /// See: [`RequiresPython::to_marker_tree`]
self.target().and_then(|target| target.as_requires_python()) pub fn to_marker_tree(&self) -> MarkerTree {
let version = match &self.target {
None => self.installed.version.clone(),
Some(PythonTarget::Version(version)) => version.version.clone(),
Some(PythonTarget::RequiresPython(requires_python)) => {
return requires_python.to_marker_tree()
}
};
let version_major_minor_only = Version::new(version.release().iter().take(2));
let expr_python_version = MarkerExpression::Version {
key: MarkerValueVersion::PythonVersion,
specifier: VersionSpecifier::from_version(Operator::Equal, version_major_minor_only)
.unwrap(),
};
let expr_python_full_version = MarkerExpression::Version {
key: MarkerValueVersion::PythonFullVersion,
specifier: VersionSpecifier::from_version(Operator::Equal, version).unwrap(),
};
MarkerTree::And(vec![
MarkerTree::Expression(expr_python_version),
MarkerTree::Expression(expr_python_full_version),
])
} }
} }

View file

@ -48,7 +48,6 @@ use crate::pubgrub::{
PubGrubPriorities, PubGrubPython, PubGrubSpecifier, PubGrubPriorities, PubGrubPython, PubGrubSpecifier,
}; };
use crate::python_requirement::PythonRequirement; use crate::python_requirement::PythonRequirement;
use crate::requires_python::RequiresPython;
use crate::resolution::ResolutionGraph; use crate::resolution::ResolutionGraph;
pub(crate) use crate::resolver::availability::{ pub(crate) use crate::resolver::availability::{
IncompletePackage, ResolverVersion, UnavailablePackage, UnavailableReason, UnavailableVersion, IncompletePackage, ResolverVersion, UnavailablePackage, UnavailableReason, UnavailableVersion,
@ -193,12 +192,7 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
let requires_python = if markers.is_some() { let requires_python = if markers.is_some() {
None None
} else { } else {
Some( Some(python_requirement.to_marker_tree())
python_requirement
.requires_python()
.map(RequiresPython::to_marker_tree)
.unwrap_or_else(|| MarkerTree::And(vec![])),
)
}; };
let state = ResolverState { let state = ResolverState {
index: index.clone(), index: index.clone(),

View file

@ -177,6 +177,7 @@ pub struct PipOptions {
pub config_settings: Option<ConfigSettings>, pub config_settings: Option<ConfigSettings>,
pub python_version: Option<PythonVersion>, pub python_version: Option<PythonVersion>,
pub python_platform: Option<TargetTriple>, pub python_platform: Option<TargetTriple>,
pub universal: Option<bool>,
pub exclude_newer: Option<ExcludeNewer>, pub exclude_newer: Option<ExcludeNewer>,
pub no_emit_package: Option<Vec<PackageName>>, pub no_emit_package: Option<Vec<PackageName>>,
pub emit_index_url: Option<bool>, pub emit_index_url: Option<bool>,

View file

@ -77,6 +77,7 @@ pub(crate) async fn pip_compile(
build_options: BuildOptions, build_options: BuildOptions,
python_version: Option<PythonVersion>, python_version: Option<PythonVersion>,
python_platform: Option<TargetTriple>, python_platform: Option<TargetTriple>,
universal: bool,
exclude_newer: Option<ExcludeNewer>, exclude_newer: Option<ExcludeNewer>,
annotation_style: AnnotationStyle, annotation_style: AnnotationStyle,
link_mode: LinkMode, link_mode: LinkMode,
@ -219,7 +220,13 @@ pub(crate) async fn pip_compile(
}; };
// Determine the environment for the resolution. // Determine the environment for the resolution.
let (tags, markers) = resolution_environment(python_version, python_platform, &interpreter)?; let (tags, markers) = if universal {
(None, None)
} else {
let (tags, markers) =
resolution_environment(python_version, python_platform, &interpreter)?;
(Some(tags), Some(markers))
};
// Generate, but don't enforce hashes for the requirements. // Generate, but don't enforce hashes for the requirements.
let hasher = if generate_hashes { let hasher = if generate_hashes {
@ -247,7 +254,7 @@ pub(crate) async fn pip_compile(
.index_urls(index_locations.index_urls()) .index_urls(index_locations.index_urls())
.index_strategy(index_strategy) .index_strategy(index_strategy)
.keyring(keyring_provider) .keyring(keyring_provider)
.markers(&markers) .markers(interpreter.markers())
.platform(interpreter.platform()) .platform(interpreter.platform())
.build(); .build();
@ -262,7 +269,7 @@ pub(crate) async fn pip_compile(
let flat_index = { let flat_index = {
let client = FlatIndexClient::new(&client, &cache); let client = FlatIndexClient::new(&client, &cache);
let entries = client.fetch(index_locations.flat_index()).await?; let entries = client.fetch(index_locations.flat_index()).await?;
FlatIndex::from_entries(entries, Some(&tags), &hasher, &build_options) FlatIndex::from_entries(entries, tags.as_deref(), &hasher, &build_options)
}; };
// Track in-flight downloads, builds, etc., across resolutions. // Track in-flight downloads, builds, etc., across resolutions.
@ -319,8 +326,8 @@ pub(crate) async fn pip_compile(
&hasher, &hasher,
&Reinstall::None, &Reinstall::None,
&upgrade, &upgrade,
Some(&tags), tags.as_deref(),
Some(&markers), markers.as_deref(),
python_requirement, python_requirement,
&client, &client,
&flat_index, &flat_index,
@ -368,7 +375,8 @@ pub(crate) async fn pip_compile(
} }
if include_marker_expression { if include_marker_expression {
let relevant_markers = resolution.marker_tree(&top_level_index, &markers)?; if let Some(markers) = markers.as_deref() {
let relevant_markers = resolution.marker_tree(&top_level_index, markers)?;
writeln!( writeln!(
writer, writer,
"{}", "{}",
@ -376,6 +384,7 @@ pub(crate) async fn pip_compile(
)?; )?;
writeln!(writer, "{}", format!("# {relevant_markers}").green())?; writeln!(writer, "{}", format!("# {relevant_markers}").green())?;
} }
}
let mut wrote_preamble = false; let mut wrote_preamble = false;
@ -439,11 +448,11 @@ pub(crate) async fn pip_compile(
"{}", "{}",
DisplayResolutionGraph::new( DisplayResolutionGraph::new(
&resolution, &resolution,
Some(&markers), markers.as_deref(),
&no_emit_packages, &no_emit_packages,
generate_hashes, generate_hashes,
include_extras, include_extras,
include_markers, include_markers || universal,
include_annotations, include_annotations,
include_index_annotation, include_index_annotation,
annotation_style, annotation_style,

View file

@ -282,6 +282,7 @@ async fn run() -> Result<ExitStatus> {
args.settings.build_options, args.settings.build_options,
args.settings.python_version, args.settings.python_version,
args.settings.python_platform, args.settings.python_platform,
args.settings.universal,
args.settings.exclude_newer, args.settings.exclude_newer,
args.settings.annotation_style, args.settings.annotation_style,
args.settings.link_mode, args.settings.link_mode,

View file

@ -525,6 +525,8 @@ impl PipCompileSettings {
only_binary, only_binary,
python_version, python_version,
python_platform, python_platform,
universal,
no_universal,
no_emit_package, no_emit_package,
emit_index_url, emit_index_url,
no_emit_index_url, no_emit_index_url,
@ -583,6 +585,7 @@ impl PipCompileSettings {
legacy_setup_py: flag(legacy_setup_py, no_legacy_setup_py), legacy_setup_py: flag(legacy_setup_py, no_legacy_setup_py),
python_version, python_version,
python_platform, python_platform,
universal: flag(universal, no_universal),
no_emit_package, no_emit_package,
emit_index_url: flag(emit_index_url, no_emit_index_url), emit_index_url: flag(emit_index_url, no_emit_index_url),
emit_find_links: flag(emit_find_links, no_emit_find_links), emit_find_links: flag(emit_find_links, no_emit_find_links),
@ -1499,6 +1502,7 @@ pub(crate) struct PipSettings {
pub(crate) config_setting: ConfigSettings, pub(crate) config_setting: ConfigSettings,
pub(crate) python_version: Option<PythonVersion>, pub(crate) python_version: Option<PythonVersion>,
pub(crate) python_platform: Option<TargetTriple>, pub(crate) python_platform: Option<TargetTriple>,
pub(crate) universal: bool,
pub(crate) exclude_newer: Option<ExcludeNewer>, pub(crate) exclude_newer: Option<ExcludeNewer>,
pub(crate) no_emit_package: Vec<PackageName>, pub(crate) no_emit_package: Vec<PackageName>,
pub(crate) emit_index_url: bool, pub(crate) emit_index_url: bool,
@ -1555,6 +1559,7 @@ impl PipSettings {
config_settings, config_settings,
python_version, python_version,
python_platform, python_platform,
universal,
exclude_newer, exclude_newer,
no_emit_package, no_emit_package,
emit_index_url, emit_index_url,
@ -1686,6 +1691,7 @@ impl PipSettings {
.unwrap_or_default(), .unwrap_or_default(),
python_version: args.python_version.combine(python_version), python_version: args.python_version.combine(python_version),
python_platform: args.python_platform.combine(python_platform), python_platform: args.python_platform.combine(python_platform),
universal: args.universal.combine(universal).unwrap_or_default(),
exclude_newer: args.exclude_newer.combine(exclude_newer), exclude_newer: args.exclude_newer.combine(exclude_newer),
no_emit_package: args no_emit_package: args
.no_emit_package .no_emit_package

View file

@ -6324,6 +6324,100 @@ fn no_strip_markers_transitive_marker() -> Result<()> {
Ok(()) Ok(())
} }
/// Perform a universal resolution with a package that has a marker.
#[test]
fn universal() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(indoc::indoc! {r"
trio ; python_version > '3.11'
trio ; sys_platform == 'win32'
"})?;
uv_snapshot!(context.compile()
.arg("requirements.in")
.arg("--universal"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --universal
attrs==23.2.0 ; python_version > '3.11' or sys_platform == 'win32'
# via
# outcome
# trio
cffi==1.16.0 ; implementation_name != 'pypy' and os_name == 'nt' and (python_version > '3.11' or sys_platform == 'win32')
# via trio
idna==3.6 ; python_version > '3.11' or sys_platform == 'win32'
# via trio
outcome==1.3.0.post0 ; python_version > '3.11' or sys_platform == 'win32'
# via trio
pycparser==2.21 ; implementation_name != 'pypy' and os_name == 'nt' and (python_version > '3.11' or sys_platform == 'win32')
# via cffi
sniffio==1.3.1 ; python_version > '3.11' or sys_platform == 'win32'
# via trio
sortedcontainers==2.4.0 ; python_version > '3.11' or sys_platform == 'win32'
# via trio
trio==0.25.0 ; python_version > '3.11' or sys_platform == 'win32'
# via -r requirements.in
----- stderr -----
Resolved 8 packages in [TIME]
"###
);
Ok(())
}
/// Perform a universal resolution with conflicting versions and markers.
#[test]
fn universal_conflicting() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(indoc::indoc! {r"
trio==0.25.0 ; sys_platform == 'darwin'
trio==0.10.0 ; sys_platform == 'win32'
"})?;
uv_snapshot!(context.compile()
.arg("requirements.in")
.arg("--universal"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --universal
async-generator==1.10 ; sys_platform == 'win32'
# via trio
attrs==23.2.0 ; sys_platform == 'darwin' or sys_platform == 'win32'
# via
# outcome
# trio
cffi==1.16.0 ; (implementation_name != 'pypy' and os_name == 'nt' and sys_platform == 'darwin') or (os_name == 'nt' and sys_platform == 'win32')
# via trio
idna==3.6 ; sys_platform == 'darwin' or sys_platform == 'win32'
# via trio
outcome==1.3.0.post0 ; sys_platform == 'darwin' or sys_platform == 'win32'
# via trio
pycparser==2.21 ; (implementation_name != 'pypy' and os_name == 'nt' and sys_platform == 'darwin') or (os_name == 'nt' and sys_platform == 'win32')
# via cffi
sniffio==1.3.1 ; sys_platform == 'darwin' or sys_platform == 'win32'
# via trio
sortedcontainers==2.4.0 ; sys_platform == 'darwin' or sys_platform == 'win32'
# via trio
trio==0.10.0 ; sys_platform == 'win32'
# via -r requirements.in
trio==0.25.0 ; sys_platform == 'darwin'
# via -r requirements.in
----- stderr -----
Resolved 10 packages in [TIME]
"###
);
Ok(())
}
/// Resolve a package from a `requirements.in` file, with a `constraints.txt` file pinning one of /// Resolve a package from a `requirements.in` file, with a `constraints.txt` file pinning one of
/// its transitive dependencies to a specific version. /// its transitive dependencies to a specific version.
#[test] #[test]

View file

@ -140,6 +140,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
), ),
python_version: None, python_version: None,
python_platform: None, python_platform: None,
universal: false,
exclude_newer: Some( exclude_newer: Some(
ExcludeNewer( ExcludeNewer(
2024-03-25T00:00:00Z, 2024-03-25T00:00:00Z,
@ -269,6 +270,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
), ),
python_version: None, python_version: None,
python_platform: None, python_platform: None,
universal: false,
exclude_newer: Some( exclude_newer: Some(
ExcludeNewer( ExcludeNewer(
2024-03-25T00:00:00Z, 2024-03-25T00:00:00Z,
@ -399,6 +401,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
), ),
python_version: None, python_version: None,
python_platform: None, python_platform: None,
universal: false,
exclude_newer: Some( exclude_newer: Some(
ExcludeNewer( ExcludeNewer(
2024-03-25T00:00:00Z, 2024-03-25T00:00:00Z,
@ -561,6 +564,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
), ),
python_version: None, python_version: None,
python_platform: None, python_platform: None,
universal: false,
exclude_newer: Some( exclude_newer: Some(
ExcludeNewer( ExcludeNewer(
2024-03-25T00:00:00Z, 2024-03-25T00:00:00Z,
@ -669,6 +673,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
), ),
python_version: None, python_version: None,
python_platform: None, python_platform: None,
universal: false,
exclude_newer: Some( exclude_newer: Some(
ExcludeNewer( ExcludeNewer(
2024-03-25T00:00:00Z, 2024-03-25T00:00:00Z,
@ -809,6 +814,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
), ),
python_version: None, python_version: None,
python_platform: None, python_platform: None,
universal: false,
exclude_newer: Some( exclude_newer: Some(
ExcludeNewer( ExcludeNewer(
2024-03-25T00:00:00Z, 2024-03-25T00:00:00Z,
@ -986,6 +992,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
), ),
python_version: None, python_version: None,
python_platform: None, python_platform: None,
universal: false,
exclude_newer: Some( exclude_newer: Some(
ExcludeNewer( ExcludeNewer(
2024-03-25T00:00:00Z, 2024-03-25T00:00:00Z,
@ -1162,6 +1169,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
), ),
python_version: None, python_version: None,
python_platform: None, python_platform: None,
universal: false,
exclude_newer: Some( exclude_newer: Some(
ExcludeNewer( ExcludeNewer(
2024-03-25T00:00:00Z, 2024-03-25T00:00:00Z,
@ -1311,6 +1319,7 @@ fn resolve_find_links() -> anyhow::Result<()> {
), ),
python_version: None, python_version: None,
python_platform: None, python_platform: None,
universal: false,
exclude_newer: Some( exclude_newer: Some(
ExcludeNewer( ExcludeNewer(
2024-03-25T00:00:00Z, 2024-03-25T00:00:00Z,
@ -1441,6 +1450,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
), ),
python_version: None, python_version: None,
python_platform: None, python_platform: None,
universal: false,
exclude_newer: Some( exclude_newer: Some(
ExcludeNewer( ExcludeNewer(
2024-03-25T00:00:00Z, 2024-03-25T00:00:00Z,
@ -1609,6 +1619,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
), ),
python_version: None, python_version: None,
python_platform: None, python_platform: None,
universal: false,
exclude_newer: Some( exclude_newer: Some(
ExcludeNewer( ExcludeNewer(
2024-03-25T00:00:00Z, 2024-03-25T00:00:00Z,
@ -1760,6 +1771,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
), ),
python_version: None, python_version: None,
python_platform: None, python_platform: None,
universal: false,
exclude_newer: Some( exclude_newer: Some(
ExcludeNewer( ExcludeNewer(
2024-03-25T00:00:00Z, 2024-03-25T00:00:00Z,
@ -1890,6 +1902,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
), ),
python_version: None, python_version: None,
python_platform: None, python_platform: None,
universal: false,
exclude_newer: Some( exclude_newer: Some(
ExcludeNewer( ExcludeNewer(
2024-03-25T00:00:00Z, 2024-03-25T00:00:00Z,
@ -2003,6 +2016,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
), ),
python_version: None, python_version: None,
python_platform: None, python_platform: None,
universal: false,
exclude_newer: Some( exclude_newer: Some(
ExcludeNewer( ExcludeNewer(
2024-03-25T00:00:00Z, 2024-03-25T00:00:00Z,
@ -2116,6 +2130,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
), ),
python_version: None, python_version: None,
python_platform: None, python_platform: None,
universal: false,
exclude_newer: Some( exclude_newer: Some(
ExcludeNewer( ExcludeNewer(
2024-03-25T00:00:00Z, 2024-03-25T00:00:00Z,
@ -2231,6 +2246,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
), ),
python_version: None, python_version: None,
python_platform: None, python_platform: None,
universal: false,
exclude_newer: Some( exclude_newer: Some(
ExcludeNewer( ExcludeNewer(
2024-03-25T00:00:00Z, 2024-03-25T00:00:00Z,
@ -2371,6 +2387,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> {
), ),
python_version: None, python_version: None,
python_platform: None, python_platform: None,
universal: false,
exclude_newer: Some( exclude_newer: Some(
ExcludeNewer( ExcludeNewer(
2024-03-25T00:00:00Z, 2024-03-25T00:00:00Z,

6
uv.schema.json generated
View file

@ -784,6 +784,12 @@
"null" "null"
] ]
}, },
"universal": {
"type": [
"boolean",
"null"
]
},
"upgrade": { "upgrade": {
"type": [ "type": [
"boolean", "boolean",