mirror of
				https://github.com/astral-sh/uv.git
				synced 2025-10-31 03:55:33 +00:00 
			
		
		
		
	Reject already-installed wheels that don't match the target platform (#15484)
## Summary We've received several requests to validate that installed wheels match the current Python platform. This isn't _super_ common, since it requires that your platform changes in some meaningful way (e.g., you switch from x86 to ARM), though in practice, it sounds like it _can_ happen in HPC environments. This seems like a good thing to do regardless, so we now validate that the tags (as recoded in `WHEEL`) are consistent with the current platform during installs. Closes https://github.com/astral-sh/uv/issues/15035.
This commit is contained in:
		
							parent
							
								
									563adb8904
								
							
						
					
					
						commit
						be4d5b72aa
					
				
					 25 changed files with 751 additions and 63 deletions
				
			
		
							
								
								
									
										1
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							|  | @ -5628,6 +5628,7 @@ dependencies = [ | ||||||
|  "uv-distribution-filename", |  "uv-distribution-filename", | ||||||
|  "uv-fs", |  "uv-fs", | ||||||
|  "uv-git-types", |  "uv-git-types", | ||||||
|  |  "uv-install-wheel", | ||||||
|  "uv-normalize", |  "uv-normalize", | ||||||
|  "uv-pep440", |  "uv-pep440", | ||||||
|  "uv-pep508", |  "uv-pep508", | ||||||
|  |  | ||||||
							
								
								
									
										487
									
								
								crates/uv-distribution-filename/src/expanded_tags.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										487
									
								
								crates/uv-distribution-filename/src/expanded_tags.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,487 @@ | ||||||
|  | use std::str::FromStr; | ||||||
|  | 
 | ||||||
|  | use memchr::memchr; | ||||||
|  | use thiserror::Error; | ||||||
|  | 
 | ||||||
|  | use uv_platform_tags::{ | ||||||
|  |     AbiTag, LanguageTag, ParseAbiTagError, ParseLanguageTagError, ParsePlatformTagError, | ||||||
|  |     PlatformTag, Tags, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | use crate::splitter::MemchrSplitter; | ||||||
|  | use crate::wheel_tag::{WheelTag, WheelTagLarge, WheelTagSmall}; | ||||||
|  | 
 | ||||||
|  | /// The expanded wheel tags as stored in a `WHEEL` file.
 | ||||||
|  | ///
 | ||||||
|  | /// For example, if a wheel filename included `py2.py3-none-any`, the `WHEEL` file would include:
 | ||||||
|  | /// ```
 | ||||||
|  | /// Tag: py2-none-any
 | ||||||
|  | /// Tag: py3-none-any
 | ||||||
|  | /// ```
 | ||||||
|  | ///
 | ||||||
|  | /// This type stores those expanded tags.
 | ||||||
|  | #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] | ||||||
|  | pub struct ExpandedTags(smallvec::SmallVec<[WheelTag; 1]>); | ||||||
|  | 
 | ||||||
|  | impl ExpandedTags { | ||||||
|  |     /// Parse a list of expanded wheel tags (e.g., `py3-none-any`).
 | ||||||
|  |     pub fn parse<'a>(tags: impl IntoIterator<Item = &'a str>) -> Result<Self, ExpandedTagError> { | ||||||
|  |         let tags = tags | ||||||
|  |             .into_iter() | ||||||
|  |             .map(parse_expanded_tag) | ||||||
|  |             .collect::<Result<_, _>>()?; | ||||||
|  |         Ok(Self(tags)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Returns `true` if the wheel is compatible with the given tags.
 | ||||||
|  |     pub fn is_compatible(&self, compatible_tags: &Tags) -> bool { | ||||||
|  |         self.0.iter().any(|tag| { | ||||||
|  |             compatible_tags.is_compatible(tag.python_tags(), tag.abi_tags(), tag.platform_tags()) | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Error, Debug)] | ||||||
|  | pub enum ExpandedTagError { | ||||||
|  |     #[error("The wheel tag \"{0}\" is missing a language tag")] | ||||||
|  |     MissingLanguageTag(String), | ||||||
|  |     #[error("The wheel tag \"{0}\" is missing an ABI tag")] | ||||||
|  |     MissingAbiTag(String), | ||||||
|  |     #[error("The wheel tag \"{0}\" is missing a platform tag")] | ||||||
|  |     MissingPlatformTag(String), | ||||||
|  |     #[error("The wheel tag \"{0}\" contains too many segments")] | ||||||
|  |     ExtraSegment(String), | ||||||
|  |     #[error("The wheel tag \"{0}\" contains an invalid language tag")] | ||||||
|  |     InvalidLanguageTag(String, #[source] ParseLanguageTagError), | ||||||
|  |     #[error("The wheel tag \"{0}\" contains an invalid ABI tag")] | ||||||
|  |     InvalidAbiTag(String, #[source] ParseAbiTagError), | ||||||
|  |     #[error("The wheel tag \"{0}\" contains an invalid platform tag")] | ||||||
|  |     InvalidPlatformTag(String, #[source] ParsePlatformTagError), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Parse an expanded (i.e., simplified) wheel tag, e.g. `py3-none-any`.
 | ||||||
|  | ///
 | ||||||
|  | /// Unlike parsing tags in a wheel filename, each tag in this case is expected to contain exactly
 | ||||||
|  | /// three segments separated by `-`: a language tag, an ABI tag, and a platform tag; however,
 | ||||||
|  | /// empirically, some build backends do emit multipart tags (like `cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64`),
 | ||||||
|  | /// so we allow those too.
 | ||||||
|  | fn parse_expanded_tag(tag: &str) -> Result<WheelTag, ExpandedTagError> { | ||||||
|  |     let mut splitter = memchr::Memchr::new(b'-', tag.as_bytes()); | ||||||
|  |     if tag.is_empty() { | ||||||
|  |         return Err(ExpandedTagError::MissingLanguageTag(tag.to_string())); | ||||||
|  |     } | ||||||
|  |     let Some(python_tag_index) = splitter.next() else { | ||||||
|  |         return Err(ExpandedTagError::MissingAbiTag(tag.to_string())); | ||||||
|  |     }; | ||||||
|  |     let Some(abi_tag_index) = splitter.next() else { | ||||||
|  |         return Err(ExpandedTagError::MissingPlatformTag(tag.to_string())); | ||||||
|  |     }; | ||||||
|  |     if splitter.next().is_some() { | ||||||
|  |         return Err(ExpandedTagError::ExtraSegment(tag.to_string())); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let python_tag = &tag[..python_tag_index]; | ||||||
|  |     let abi_tag = &tag[python_tag_index + 1..abi_tag_index]; | ||||||
|  |     let platform_tag = &tag[abi_tag_index + 1..]; | ||||||
|  | 
 | ||||||
|  |     let is_small = memchr(b'.', tag.as_bytes()).is_none(); | ||||||
|  | 
 | ||||||
|  |     if let Some(small) = is_small | ||||||
|  |         .then(|| { | ||||||
|  |             Some(WheelTagSmall { | ||||||
|  |                 python_tag: LanguageTag::from_str(python_tag).ok()?, | ||||||
|  |                 abi_tag: AbiTag::from_str(abi_tag).ok()?, | ||||||
|  |                 platform_tag: PlatformTag::from_str(platform_tag).ok()?, | ||||||
|  |             }) | ||||||
|  |         }) | ||||||
|  |         .flatten() | ||||||
|  |     { | ||||||
|  |         Ok(WheelTag::Small { small }) | ||||||
|  |     } else { | ||||||
|  |         Ok(WheelTag::Large { | ||||||
|  |             large: Box::new(WheelTagLarge { | ||||||
|  |                 build_tag: None, | ||||||
|  |                 python_tag: MemchrSplitter::split(python_tag, b'.') | ||||||
|  |                     .map(LanguageTag::from_str) | ||||||
|  |                     .filter_map(Result::ok) | ||||||
|  |                     .collect(), | ||||||
|  |                 abi_tag: MemchrSplitter::split(abi_tag, b'.') | ||||||
|  |                     .map(AbiTag::from_str) | ||||||
|  |                     .filter_map(Result::ok) | ||||||
|  |                     .collect(), | ||||||
|  |                 platform_tag: MemchrSplitter::split(platform_tag, b'.') | ||||||
|  |                     .map(PlatformTag::from_str) | ||||||
|  |                     .filter_map(Result::ok) | ||||||
|  |                     .collect(), | ||||||
|  |                 repr: tag.into(), | ||||||
|  |             }), | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  | 
 | ||||||
|  |     use super::*; | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_parse_simple_expanded_tag() { | ||||||
|  |         let tags = ExpandedTags::parse(vec!["py3-none-any"]).unwrap(); | ||||||
|  | 
 | ||||||
|  |         insta::assert_debug_snapshot!(tags, @r" | ||||||
|  |         ExpandedTags( | ||||||
|  |             [ | ||||||
|  |                 Small { | ||||||
|  |                     small: WheelTagSmall { | ||||||
|  |                         python_tag: Python { | ||||||
|  |                             major: 3, | ||||||
|  |                             minor: None, | ||||||
|  |                         }, | ||||||
|  |                         abi_tag: None, | ||||||
|  |                         platform_tag: Any, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|  |         ) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_parse_multiple_expanded_tags() { | ||||||
|  |         let tags = ExpandedTags::parse(vec![ | ||||||
|  |             "py2-none-any", | ||||||
|  |             "py3-none-any", | ||||||
|  |             "cp39-cp39-linux_x86_64", | ||||||
|  |         ]) | ||||||
|  |         .unwrap(); | ||||||
|  | 
 | ||||||
|  |         insta::assert_debug_snapshot!(tags, @r" | ||||||
|  |         ExpandedTags( | ||||||
|  |             [ | ||||||
|  |                 Small { | ||||||
|  |                     small: WheelTagSmall { | ||||||
|  |                         python_tag: Python { | ||||||
|  |                             major: 2, | ||||||
|  |                             minor: None, | ||||||
|  |                         }, | ||||||
|  |                         abi_tag: None, | ||||||
|  |                         platform_tag: Any, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 Small { | ||||||
|  |                     small: WheelTagSmall { | ||||||
|  |                         python_tag: Python { | ||||||
|  |                             major: 3, | ||||||
|  |                             minor: None, | ||||||
|  |                         }, | ||||||
|  |                         abi_tag: None, | ||||||
|  |                         platform_tag: Any, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 Small { | ||||||
|  |                     small: WheelTagSmall { | ||||||
|  |                         python_tag: CPython { | ||||||
|  |                             python_version: ( | ||||||
|  |                                 3, | ||||||
|  |                                 9, | ||||||
|  |                             ), | ||||||
|  |                         }, | ||||||
|  |                         abi_tag: CPython { | ||||||
|  |                             gil_disabled: false, | ||||||
|  |                             python_version: ( | ||||||
|  |                                 3, | ||||||
|  |                                 9, | ||||||
|  |                             ), | ||||||
|  |                         }, | ||||||
|  |                         platform_tag: Linux { | ||||||
|  |                             arch: X86_64, | ||||||
|  |                         }, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|  |         ) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_parse_complex_platform_tag() { | ||||||
|  |         let tags = ExpandedTags::parse(vec![ | ||||||
|  |             "cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64", | ||||||
|  |         ]) | ||||||
|  |         .unwrap(); | ||||||
|  | 
 | ||||||
|  |         insta::assert_debug_snapshot!(tags, @r#" | ||||||
|  |         ExpandedTags( | ||||||
|  |             [ | ||||||
|  |                 Large { | ||||||
|  |                     large: WheelTagLarge { | ||||||
|  |                         build_tag: None, | ||||||
|  |                         python_tag: [ | ||||||
|  |                             CPython { | ||||||
|  |                                 python_version: ( | ||||||
|  |                                     3, | ||||||
|  |                                     12, | ||||||
|  |                                 ), | ||||||
|  |                             }, | ||||||
|  |                         ], | ||||||
|  |                         abi_tag: [ | ||||||
|  |                             CPython { | ||||||
|  |                                 gil_disabled: false, | ||||||
|  |                                 python_version: ( | ||||||
|  |                                     3, | ||||||
|  |                                     12, | ||||||
|  |                                 ), | ||||||
|  |                             }, | ||||||
|  |                         ], | ||||||
|  |                         platform_tag: [ | ||||||
|  |                             Manylinux { | ||||||
|  |                                 major: 2, | ||||||
|  |                                 minor: 17, | ||||||
|  |                                 arch: X86_64, | ||||||
|  |                             }, | ||||||
|  |                             Manylinux2014 { | ||||||
|  |                                 arch: X86_64, | ||||||
|  |                             }, | ||||||
|  |                         ], | ||||||
|  |                         repr: "cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64", | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|  |         ) | ||||||
|  |         "#);
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_parse_unknown_expanded_tag() { | ||||||
|  |         let tags = ExpandedTags::parse(vec!["py3-foo-any"]).unwrap(); | ||||||
|  | 
 | ||||||
|  |         insta::assert_debug_snapshot!(tags, @r#" | ||||||
|  |         ExpandedTags( | ||||||
|  |             [ | ||||||
|  |                 Large { | ||||||
|  |                     large: WheelTagLarge { | ||||||
|  |                         build_tag: None, | ||||||
|  |                         python_tag: [ | ||||||
|  |                             Python { | ||||||
|  |                                 major: 3, | ||||||
|  |                                 minor: None, | ||||||
|  |                             }, | ||||||
|  |                         ], | ||||||
|  |                         abi_tag: [], | ||||||
|  |                         platform_tag: [ | ||||||
|  |                             Any, | ||||||
|  |                         ], | ||||||
|  |                         repr: "py3-foo-any", | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|  |         ) | ||||||
|  |         "#);
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_parse_expanded_tag_with_dots() { | ||||||
|  |         let tags = ExpandedTags::parse(vec!["py2.py3-none-any"]).unwrap(); | ||||||
|  | 
 | ||||||
|  |         insta::assert_debug_snapshot!(tags, @r#" | ||||||
|  |         ExpandedTags( | ||||||
|  |             [ | ||||||
|  |                 Large { | ||||||
|  |                     large: WheelTagLarge { | ||||||
|  |                         build_tag: None, | ||||||
|  |                         python_tag: [ | ||||||
|  |                             Python { | ||||||
|  |                                 major: 2, | ||||||
|  |                                 minor: None, | ||||||
|  |                             }, | ||||||
|  |                             Python { | ||||||
|  |                                 major: 3, | ||||||
|  |                                 minor: None, | ||||||
|  |                             }, | ||||||
|  |                         ], | ||||||
|  |                         abi_tag: [ | ||||||
|  |                             None, | ||||||
|  |                         ], | ||||||
|  |                         platform_tag: [ | ||||||
|  |                             Any, | ||||||
|  |                         ], | ||||||
|  |                         repr: "py2.py3-none-any", | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|  |         ) | ||||||
|  |         "#);
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_error_missing_language_tag() { | ||||||
|  |         let err = ExpandedTags::parse(vec![""]).unwrap_err(); | ||||||
|  |         insta::assert_debug_snapshot!(err, @r#" | ||||||
|  |         MissingLanguageTag( | ||||||
|  |             "", | ||||||
|  |         ) | ||||||
|  |         "#);
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_error_missing_abi_tag() { | ||||||
|  |         let err = ExpandedTags::parse(vec!["py3"]).unwrap_err(); | ||||||
|  |         insta::assert_debug_snapshot!(err, @r#" | ||||||
|  |         MissingAbiTag( | ||||||
|  |             "py3", | ||||||
|  |         ) | ||||||
|  |         "#);
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_error_missing_platform_tag() { | ||||||
|  |         let err = ExpandedTags::parse(vec!["py3-none"]).unwrap_err(); | ||||||
|  |         insta::assert_debug_snapshot!(err, @r#" | ||||||
|  |         MissingPlatformTag( | ||||||
|  |             "py3-none", | ||||||
|  |         ) | ||||||
|  |         "#);
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_error_extra_segment() { | ||||||
|  |         let err = ExpandedTags::parse(vec!["py3-none-any-extra"]).unwrap_err(); | ||||||
|  |         insta::assert_debug_snapshot!(err, @r#" | ||||||
|  |         ExtraSegment( | ||||||
|  |             "py3-none-any-extra", | ||||||
|  |         ) | ||||||
|  |         "#);
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_parse_expanded_tag_single_segment() { | ||||||
|  |         let result = parse_expanded_tag("py3-none-any"); | ||||||
|  |         assert!(result.is_ok()); | ||||||
|  |         let tag = result.unwrap(); | ||||||
|  | 
 | ||||||
|  |         insta::assert_debug_snapshot!(tag, @r" | ||||||
|  |         Small { | ||||||
|  |             small: WheelTagSmall { | ||||||
|  |                 python_tag: Python { | ||||||
|  |                     major: 3, | ||||||
|  |                     minor: None, | ||||||
|  |                 }, | ||||||
|  |                 abi_tag: None, | ||||||
|  |                 platform_tag: Any, | ||||||
|  |             }, | ||||||
|  |         } | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_parse_expanded_tag_multi_segment() { | ||||||
|  |         let result = parse_expanded_tag("cp39.cp310-cp39.cp310-linux_x86_64.linux_i686"); | ||||||
|  |         assert!(result.is_ok()); | ||||||
|  |         let tag = result.unwrap(); | ||||||
|  | 
 | ||||||
|  |         insta::assert_debug_snapshot!(tag, @r#" | ||||||
|  |         Large { | ||||||
|  |             large: WheelTagLarge { | ||||||
|  |                 build_tag: None, | ||||||
|  |                 python_tag: [ | ||||||
|  |                     CPython { | ||||||
|  |                         python_version: ( | ||||||
|  |                             3, | ||||||
|  |                             9, | ||||||
|  |                         ), | ||||||
|  |                     }, | ||||||
|  |                     CPython { | ||||||
|  |                         python_version: ( | ||||||
|  |                             3, | ||||||
|  |                             10, | ||||||
|  |                         ), | ||||||
|  |                     }, | ||||||
|  |                 ], | ||||||
|  |                 abi_tag: [ | ||||||
|  |                     CPython { | ||||||
|  |                         gil_disabled: false, | ||||||
|  |                         python_version: ( | ||||||
|  |                             3, | ||||||
|  |                             9, | ||||||
|  |                         ), | ||||||
|  |                     }, | ||||||
|  |                     CPython { | ||||||
|  |                         gil_disabled: false, | ||||||
|  |                         python_version: ( | ||||||
|  |                             3, | ||||||
|  |                             10, | ||||||
|  |                         ), | ||||||
|  |                     }, | ||||||
|  |                 ], | ||||||
|  |                 platform_tag: [ | ||||||
|  |                     Linux { | ||||||
|  |                         arch: X86_64, | ||||||
|  |                     }, | ||||||
|  |                     Linux { | ||||||
|  |                         arch: X86, | ||||||
|  |                     }, | ||||||
|  |                 ], | ||||||
|  |                 repr: "cp39.cp310-cp39.cp310-linux_x86_64.linux_i686", | ||||||
|  |             }, | ||||||
|  |         } | ||||||
|  |         "#);
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_parse_expanded_tag_empty() { | ||||||
|  |         let result = parse_expanded_tag(""); | ||||||
|  |         assert!(result.is_err()); | ||||||
|  | 
 | ||||||
|  |         insta::assert_debug_snapshot!(result.unwrap_err(), @r#" | ||||||
|  |         MissingLanguageTag( | ||||||
|  |             "", | ||||||
|  |         ) | ||||||
|  |         "#);
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_parse_expanded_tag_one_segment() { | ||||||
|  |         let result = parse_expanded_tag("python"); | ||||||
|  |         assert!(result.is_err()); | ||||||
|  | 
 | ||||||
|  |         insta::assert_debug_snapshot!(result.unwrap_err(), @r#" | ||||||
|  |         MissingAbiTag( | ||||||
|  |             "python", | ||||||
|  |         ) | ||||||
|  |         "#);
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_parse_expanded_tag_two_segments() { | ||||||
|  |         let result = parse_expanded_tag("py3-none"); | ||||||
|  |         assert!(result.is_err()); | ||||||
|  | 
 | ||||||
|  |         insta::assert_debug_snapshot!(result.unwrap_err(), @r#" | ||||||
|  |         MissingPlatformTag( | ||||||
|  |             "py3-none", | ||||||
|  |         ) | ||||||
|  |         "#);
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_parse_expanded_tag_four_segments() { | ||||||
|  |         let result = parse_expanded_tag("py3-none-any-extra"); | ||||||
|  |         assert!(result.is_err()); | ||||||
|  | 
 | ||||||
|  |         insta::assert_debug_snapshot!(result.unwrap_err(), @r#" | ||||||
|  |         ExtraSegment( | ||||||
|  |             "py3-none-any-extra", | ||||||
|  |         ) | ||||||
|  |         "#);
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_expanded_tags_ordering() { | ||||||
|  |         let tags1 = ExpandedTags::parse(vec!["py3-none-any"]).unwrap(); | ||||||
|  |         let tags2 = ExpandedTags::parse(vec!["py3-none-any"]).unwrap(); | ||||||
|  |         let tags3 = ExpandedTags::parse(vec!["py2-none-any"]).unwrap(); | ||||||
|  | 
 | ||||||
|  |         assert_eq!(tags1, tags2); | ||||||
|  |         assert_ne!(tags1, tags3); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,16 +1,19 @@ | ||||||
| use std::fmt::{Display, Formatter}; | use std::fmt::{Display, Formatter}; | ||||||
| use std::str::FromStr; | use std::str::FromStr; | ||||||
|  | 
 | ||||||
| use uv_normalize::PackageName; | use uv_normalize::PackageName; | ||||||
| use uv_pep440::Version; | use uv_pep440::Version; | ||||||
| 
 | 
 | ||||||
| pub use build_tag::{BuildTag, BuildTagError}; | pub use build_tag::{BuildTag, BuildTagError}; | ||||||
| pub use egg::{EggInfoFilename, EggInfoFilenameError}; | pub use egg::{EggInfoFilename, EggInfoFilenameError}; | ||||||
|  | pub use expanded_tags::{ExpandedTagError, ExpandedTags}; | ||||||
| pub use extension::{DistExtension, ExtensionError, SourceDistExtension}; | pub use extension::{DistExtension, ExtensionError, SourceDistExtension}; | ||||||
| pub use source_dist::{SourceDistFilename, SourceDistFilenameError}; | pub use source_dist::{SourceDistFilename, SourceDistFilenameError}; | ||||||
| pub use wheel::{WheelFilename, WheelFilenameError}; | pub use wheel::{WheelFilename, WheelFilenameError}; | ||||||
| 
 | 
 | ||||||
| mod build_tag; | mod build_tag; | ||||||
| mod egg; | mod egg; | ||||||
|  | mod expanded_tags; | ||||||
| mod extension; | mod extension; | ||||||
| mod source_dist; | mod source_dist; | ||||||
| mod splitter; | mod splitter; | ||||||
|  |  | ||||||
|  | @ -25,6 +25,7 @@ uv-git-types = { workspace = true } | ||||||
| uv-normalize = { workspace = true } | uv-normalize = { workspace = true } | ||||||
| uv-pep440 = { workspace = true } | uv-pep440 = { workspace = true } | ||||||
| uv-pep508 = { workspace = true } | uv-pep508 = { workspace = true } | ||||||
|  | uv-install-wheel = { workspace = true } | ||||||
| uv-platform-tags = { workspace = true } | uv-platform-tags = { workspace = true } | ||||||
| uv-pypi-types = { workspace = true } | uv-pypi-types = { workspace = true } | ||||||
| uv-redacted = { workspace = true } | uv-redacted = { workspace = true } | ||||||
|  |  | ||||||
|  | @ -9,8 +9,9 @@ use tracing::warn; | ||||||
| use url::Url; | use url::Url; | ||||||
| 
 | 
 | ||||||
| use uv_cache_info::CacheInfo; | use uv_cache_info::CacheInfo; | ||||||
| use uv_distribution_filename::EggInfoFilename; | use uv_distribution_filename::{EggInfoFilename, ExpandedTags}; | ||||||
| use uv_fs::Simplified; | use uv_fs::Simplified; | ||||||
|  | use uv_install_wheel::WheelFile; | ||||||
| use uv_normalize::PackageName; | use uv_normalize::PackageName; | ||||||
| use uv_pep440::Version; | use uv_pep440::Version; | ||||||
| use uv_pypi_types::{DirectUrl, MetadataError}; | use uv_pypi_types::{DirectUrl, MetadataError}; | ||||||
|  | @ -40,6 +41,12 @@ pub enum InstalledDistError { | ||||||
|     #[error(transparent)] |     #[error(transparent)] | ||||||
|     PackageNameParse(#[from] uv_normalize::InvalidNameError), |     PackageNameParse(#[from] uv_normalize::InvalidNameError), | ||||||
| 
 | 
 | ||||||
|  |     #[error(transparent)] | ||||||
|  |     WheelFileParse(#[from] uv_install_wheel::Error), | ||||||
|  | 
 | ||||||
|  |     #[error(transparent)] | ||||||
|  |     ExpandedTagParse(#[from] uv_distribution_filename::ExpandedTagError), | ||||||
|  | 
 | ||||||
|     #[error("Invalid .egg-link path: `{}`", _0.user_display())] |     #[error("Invalid .egg-link path: `{}`", _0.user_display())] | ||||||
|     InvalidEggLinkPath(PathBuf), |     InvalidEggLinkPath(PathBuf), | ||||||
| 
 | 
 | ||||||
|  | @ -414,6 +421,30 @@ impl InstalledDist { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /// Return the supported wheel tags for the distribution from the `WHEEL` file, if available.
 | ||||||
|  |     pub fn read_tags(&self) -> Result<Option<ExpandedTags>, InstalledDistError> { | ||||||
|  |         // TODO(charlie): Cache this result.
 | ||||||
|  |         let path = match self { | ||||||
|  |             Self::Registry(InstalledRegistryDist { path, .. }) => path, | ||||||
|  |             Self::Url(InstalledDirectUrlDist { path, .. }) => path, | ||||||
|  |             Self::EggInfoFile(_) => return Ok(None), | ||||||
|  |             Self::EggInfoDirectory(_) => return Ok(None), | ||||||
|  |             Self::LegacyEditable(_) => return Ok(None), | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         // Read the `WHEEL` file.
 | ||||||
|  |         let contents = fs_err::read_to_string(path.join("WHEEL"))?; | ||||||
|  |         let wheel_file = WheelFile::parse(&contents)?; | ||||||
|  |         let Some(tags) = wheel_file.tags() else { | ||||||
|  |             return Ok(None); | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         // Parse the tags.
 | ||||||
|  |         let tags = ExpandedTags::parse(tags.iter().map(String::as_str))?; | ||||||
|  | 
 | ||||||
|  |         Ok(Some(tags)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /// Return true if the distribution is editable.
 |     /// Return true if the distribution is editable.
 | ||||||
|     pub fn is_editable(&self) -> bool { |     pub fn is_editable(&self) -> bool { | ||||||
|         matches!( |         matches!( | ||||||
|  |  | ||||||
|  | @ -1,20 +1,23 @@ | ||||||
| use crate::Error; | use std::ffi::{OsStr, OsString}; | ||||||
|  | use std::path::{Path, PathBuf}; | ||||||
|  | use std::sync::{Arc, Mutex}; | ||||||
|  | use std::time::SystemTime; | ||||||
|  | 
 | ||||||
| use fs_err as fs; | use fs_err as fs; | ||||||
| use fs_err::DirEntry; | use fs_err::DirEntry; | ||||||
| use reflink_copy as reflink; | use reflink_copy as reflink; | ||||||
| use rustc_hash::FxHashMap; | use rustc_hash::FxHashMap; | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
| use std::ffi::{OsStr, OsString}; |  | ||||||
| use std::path::{Path, PathBuf}; |  | ||||||
| use std::sync::{Arc, Mutex}; |  | ||||||
| use std::time::SystemTime; |  | ||||||
| use tempfile::tempdir_in; | use tempfile::tempdir_in; | ||||||
| use tracing::{debug, instrument, trace}; | use tracing::{debug, instrument, trace}; | ||||||
|  | use walkdir::WalkDir; | ||||||
|  | 
 | ||||||
| use uv_distribution_filename::WheelFilename; | use uv_distribution_filename::WheelFilename; | ||||||
| use uv_fs::Simplified; | use uv_fs::Simplified; | ||||||
| use uv_preview::{Preview, PreviewFeatures}; | use uv_preview::{Preview, PreviewFeatures}; | ||||||
| use uv_warnings::{warn_user, warn_user_once}; | use uv_warnings::{warn_user, warn_user_once}; | ||||||
| use walkdir::WalkDir; | 
 | ||||||
|  | use crate::Error; | ||||||
| 
 | 
 | ||||||
| #[allow(clippy::struct_field_names)] | #[allow(clippy::struct_field_names)] | ||||||
| #[derive(Debug, Default)] | #[derive(Debug, Default)] | ||||||
|  |  | ||||||
|  | @ -949,12 +949,9 @@ mod test { | ||||||
|     use assert_fs::prelude::*; |     use assert_fs::prelude::*; | ||||||
|     use indoc::{formatdoc, indoc}; |     use indoc::{formatdoc, indoc}; | ||||||
| 
 | 
 | ||||||
|     use crate::Error; |  | ||||||
|     use crate::wheel::format_shebang; |  | ||||||
| 
 |  | ||||||
|     use super::{ |     use super::{ | ||||||
|         RecordEntry, Script, WheelFile, get_script_executable, parse_email_message_file, |         Error, RecordEntry, Script, WheelFile, format_shebang, get_script_executable, | ||||||
|         read_record_file, write_installer_metadata, |         parse_email_message_file, read_record_file, write_installer_metadata, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     #[test] |     #[test] | ||||||
|  |  | ||||||
|  | @ -125,6 +125,7 @@ impl<'a> Planner<'a> { | ||||||
|                             dist.name(), |                             dist.name(), | ||||||
|                             installed, |                             installed, | ||||||
|                             &source, |                             &source, | ||||||
|  |                             tags, | ||||||
|                             config_settings, |                             config_settings, | ||||||
|                             config_settings_package, |                             config_settings_package, | ||||||
|                             extra_build_requires, |                             extra_build_requires, | ||||||
|  |  | ||||||
|  | @ -14,6 +14,7 @@ use uv_distribution_types::{ | ||||||
| }; | }; | ||||||
| use uv_git_types::GitOid; | use uv_git_types::GitOid; | ||||||
| use uv_normalize::PackageName; | use uv_normalize::PackageName; | ||||||
|  | use uv_platform_tags::Tags; | ||||||
| use uv_pypi_types::{DirInfo, DirectUrl, VcsInfo, VcsKind}; | use uv_pypi_types::{DirInfo, DirectUrl, VcsInfo, VcsKind}; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Copy, Clone)] | #[derive(Debug, Copy, Clone)] | ||||||
|  | @ -32,6 +33,7 @@ impl RequirementSatisfaction { | ||||||
|         name: &PackageName, |         name: &PackageName, | ||||||
|         distribution: &InstalledDist, |         distribution: &InstalledDist, | ||||||
|         source: &RequirementSource, |         source: &RequirementSource, | ||||||
|  |         tags: &Tags, | ||||||
|         config_settings: &ConfigSettings, |         config_settings: &ConfigSettings, | ||||||
|         config_settings_package: &PackageConfigSettings, |         config_settings_package: &PackageConfigSettings, | ||||||
|         extra_build_requires: &ExtraBuildRequires, |         extra_build_requires: &ExtraBuildRequires, | ||||||
|  | @ -55,7 +57,7 @@ impl RequirementSatisfaction { | ||||||
|             ); |             ); | ||||||
|             dist_build_info != &build_info |             dist_build_info != &build_info | ||||||
|         }) { |         }) { | ||||||
|             debug!("Build info mismatch for {name}: {distribution:?}"); |             debug!("Build info mismatch for {name}: {distribution}"); | ||||||
|             return Self::OutOfDate; |             return Self::OutOfDate; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -63,10 +65,9 @@ impl RequirementSatisfaction { | ||||||
|         match source { |         match source { | ||||||
|             // If the requirement comes from a registry, check by name.
 |             // If the requirement comes from a registry, check by name.
 | ||||||
|             RequirementSource::Registry { specifier, .. } => { |             RequirementSource::Registry { specifier, .. } => { | ||||||
|                 if specifier.contains(distribution.version()) { |                 if !specifier.contains(distribution.version()) { | ||||||
|                     return Self::Satisfied; |                     return Self::Mismatch; | ||||||
|                 } |                 } | ||||||
|                 Self::Mismatch |  | ||||||
|             } |             } | ||||||
|             RequirementSource::Url { |             RequirementSource::Url { | ||||||
|                 // We use the location since `direct_url.json` also stores this URL, e.g.
 |                 // We use the location since `direct_url.json` also stores this URL, e.g.
 | ||||||
|  | @ -130,9 +131,6 @@ impl RequirementSatisfaction { | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
|                 // Otherwise, assume the requirement is up-to-date.
 |  | ||||||
|                 Self::Satisfied |  | ||||||
|             } |             } | ||||||
|             RequirementSource::Git { |             RequirementSource::Git { | ||||||
|                 url: _, |                 url: _, | ||||||
|  | @ -188,8 +186,6 @@ impl RequirementSatisfaction { | ||||||
|                     ); |                     ); | ||||||
|                     return Self::OutOfDate; |                     return Self::OutOfDate; | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
|                 Self::Satisfied |  | ||||||
|             } |             } | ||||||
|             RequirementSource::Path { |             RequirementSource::Path { | ||||||
|                 install_path: requested_path, |                 install_path: requested_path, | ||||||
|  | @ -244,8 +240,6 @@ impl RequirementSatisfaction { | ||||||
|                         return Self::CacheInvalid; |                         return Self::CacheInvalid; | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
|                 Self::Satisfied |  | ||||||
|             } |             } | ||||||
|             RequirementSource::Directory { |             RequirementSource::Directory { | ||||||
|                 install_path: requested_path, |                 install_path: requested_path, | ||||||
|  | @ -314,9 +308,26 @@ impl RequirementSatisfaction { | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 Self::Satisfied |                 // If the distribution isn't compatible with the current platform, it is a mismatch.
 | ||||||
|  |                 if let Ok(Some(wheel_tags)) = distribution.read_tags() { | ||||||
|  |                     if !wheel_tags.is_compatible(tags) { | ||||||
|  |                         debug!("Platform tags mismatch for {name}: {distribution}"); | ||||||
|  |                         return Self::Mismatch; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         // If the distribution isn't compatible with the current platform, it is a mismatch.
 | ||||||
|  |         if let Ok(Some(wheel_tags)) = distribution.read_tags() { | ||||||
|  |             if !wheel_tags.is_compatible(tags) { | ||||||
|  |                 debug!("Platform tags mismatch for {name}: {distribution}"); | ||||||
|  |                 return Self::Mismatch; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Otherwise, assume the requirement is up-to-date.
 | ||||||
|  |         Self::Satisfied | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -16,6 +16,7 @@ use uv_fs::Simplified; | ||||||
| use uv_normalize::PackageName; | use uv_normalize::PackageName; | ||||||
| use uv_pep440::{Version, VersionSpecifiers}; | use uv_pep440::{Version, VersionSpecifiers}; | ||||||
| use uv_pep508::VersionOrUrl; | use uv_pep508::VersionOrUrl; | ||||||
|  | use uv_platform_tags::Tags; | ||||||
| use uv_pypi_types::{ResolverMarkerEnvironment, VerbatimParsedUrl}; | use uv_pypi_types::{ResolverMarkerEnvironment, VerbatimParsedUrl}; | ||||||
| use uv_python::{Interpreter, PythonEnvironment}; | use uv_python::{Interpreter, PythonEnvironment}; | ||||||
| use uv_redacted::DisplaySafeUrl; | use uv_redacted::DisplaySafeUrl; | ||||||
|  | @ -194,6 +195,7 @@ impl SitePackages { | ||||||
|     pub fn diagnostics( |     pub fn diagnostics( | ||||||
|         &self, |         &self, | ||||||
|         markers: &ResolverMarkerEnvironment, |         markers: &ResolverMarkerEnvironment, | ||||||
|  |         tags: &Tags, | ||||||
|     ) -> Result<Vec<SitePackagesDiagnostic>> { |     ) -> Result<Vec<SitePackagesDiagnostic>> { | ||||||
|         let mut diagnostics = Vec::new(); |         let mut diagnostics = Vec::new(); | ||||||
| 
 | 
 | ||||||
|  | @ -242,6 +244,25 @@ impl SitePackages { | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|  |                 // Verify that the package is compatible with the current tags.
 | ||||||
|  |                 match distribution.read_tags() { | ||||||
|  |                     Ok(Some(wheel_tags)) => { | ||||||
|  |                         if !wheel_tags.is_compatible(tags) { | ||||||
|  |                             // TODO(charlie): Show the expanded tag hint, that explains _why_ it doesn't match.
 | ||||||
|  |                             diagnostics.push(SitePackagesDiagnostic::IncompatiblePlatform { | ||||||
|  |                                 package: package.clone(), | ||||||
|  |                             }); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     Ok(None) => {} | ||||||
|  |                     Err(_) => { | ||||||
|  |                         diagnostics.push(SitePackagesDiagnostic::TagsUnavailable { | ||||||
|  |                             package: package.clone(), | ||||||
|  |                             path: distribution.install_path().to_owned(), | ||||||
|  |                         }); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|                 // Verify that the dependencies are installed.
 |                 // Verify that the dependencies are installed.
 | ||||||
|                 for dependency in &metadata.requires_dist { |                 for dependency in &metadata.requires_dist { | ||||||
|                     if !dependency.evaluate_markers(markers, &[]) { |                     if !dependency.evaluate_markers(markers, &[]) { | ||||||
|  | @ -259,12 +280,10 @@ impl SitePackages { | ||||||
|                         } |                         } | ||||||
|                         [installed] => { |                         [installed] => { | ||||||
|                             match &dependency.version_or_url { |                             match &dependency.version_or_url { | ||||||
|                                 None | Some(uv_pep508::VersionOrUrl::Url(_)) => { |                                 None | Some(VersionOrUrl::Url(_)) => { | ||||||
|                                     // Nothing to do (accept any installed version).
 |                                     // Nothing to do (accept any installed version).
 | ||||||
|                                 } |                                 } | ||||||
|                                 Some(uv_pep508::VersionOrUrl::VersionSpecifier( |                                 Some(VersionOrUrl::VersionSpecifier(version_specifier)) => { | ||||||
|                                     version_specifier, |  | ||||||
|                                 )) => { |  | ||||||
|                                     // The installed version doesn't satisfy the requirement.
 |                                     // The installed version doesn't satisfy the requirement.
 | ||||||
|                                     if !version_specifier.contains(installed.version()) { |                                     if !version_specifier.contains(installed.version()) { | ||||||
|                                         diagnostics.push( |                                         diagnostics.push( | ||||||
|  | @ -296,6 +315,7 @@ impl SitePackages { | ||||||
|         constraints: &[NameRequirementSpecification], |         constraints: &[NameRequirementSpecification], | ||||||
|         overrides: &[UnresolvedRequirementSpecification], |         overrides: &[UnresolvedRequirementSpecification], | ||||||
|         markers: &ResolverMarkerEnvironment, |         markers: &ResolverMarkerEnvironment, | ||||||
|  |         tags: &Tags, | ||||||
|         config_settings: &ConfigSettings, |         config_settings: &ConfigSettings, | ||||||
|         config_settings_package: &PackageConfigSettings, |         config_settings_package: &PackageConfigSettings, | ||||||
|         extra_build_requires: &ExtraBuildRequires, |         extra_build_requires: &ExtraBuildRequires, | ||||||
|  | @ -385,6 +405,7 @@ impl SitePackages { | ||||||
|             constraints.iter().map(|constraint| &constraint.requirement), |             constraints.iter().map(|constraint| &constraint.requirement), | ||||||
|             overrides.iter().map(Cow::as_ref), |             overrides.iter().map(Cow::as_ref), | ||||||
|             markers, |             markers, | ||||||
|  |             tags, | ||||||
|             config_settings, |             config_settings, | ||||||
|             config_settings_package, |             config_settings_package, | ||||||
|             extra_build_requires, |             extra_build_requires, | ||||||
|  | @ -399,6 +420,7 @@ impl SitePackages { | ||||||
|         constraints: impl Iterator<Item = &'a Requirement>, |         constraints: impl Iterator<Item = &'a Requirement>, | ||||||
|         overrides: impl Iterator<Item = &'a Requirement>, |         overrides: impl Iterator<Item = &'a Requirement>, | ||||||
|         markers: &ResolverMarkerEnvironment, |         markers: &ResolverMarkerEnvironment, | ||||||
|  |         tags: &Tags, | ||||||
|         config_settings: &ConfigSettings, |         config_settings: &ConfigSettings, | ||||||
|         config_settings_package: &PackageConfigSettings, |         config_settings_package: &PackageConfigSettings, | ||||||
|         extra_build_requires: &ExtraBuildRequires, |         extra_build_requires: &ExtraBuildRequires, | ||||||
|  | @ -460,6 +482,7 @@ impl SitePackages { | ||||||
|                             name, |                             name, | ||||||
|                             distribution, |                             distribution, | ||||||
|                             &requirement.source, |                             &requirement.source, | ||||||
|  |                             tags, | ||||||
|                             config_settings, |                             config_settings, | ||||||
|                             config_settings_package, |                             config_settings_package, | ||||||
|                             extra_build_requires, |                             extra_build_requires, | ||||||
|  | @ -481,6 +504,7 @@ impl SitePackages { | ||||||
|                                 name, |                                 name, | ||||||
|                                 distribution, |                                 distribution, | ||||||
|                                 &constraint.source, |                                 &constraint.source, | ||||||
|  |                                 tags, | ||||||
|                                 config_settings, |                                 config_settings, | ||||||
|                                 config_settings_package, |                                 config_settings_package, | ||||||
|                                 extra_build_requires, |                                 extra_build_requires, | ||||||
|  | @ -566,6 +590,12 @@ pub enum SitePackagesDiagnostic { | ||||||
|         /// The path to the package.
 |         /// The path to the package.
 | ||||||
|         path: PathBuf, |         path: PathBuf, | ||||||
|     }, |     }, | ||||||
|  |     TagsUnavailable { | ||||||
|  |         /// The package that is missing tags.
 | ||||||
|  |         package: PackageName, | ||||||
|  |         /// The path to the package.
 | ||||||
|  |         path: PathBuf, | ||||||
|  |     }, | ||||||
|     IncompatiblePythonVersion { |     IncompatiblePythonVersion { | ||||||
|         /// The package that requires a different version of Python.
 |         /// The package that requires a different version of Python.
 | ||||||
|         package: PackageName, |         package: PackageName, | ||||||
|  | @ -574,6 +604,10 @@ pub enum SitePackagesDiagnostic { | ||||||
|         /// The version of Python that is required.
 |         /// The version of Python that is required.
 | ||||||
|         requires_python: VersionSpecifiers, |         requires_python: VersionSpecifiers, | ||||||
|     }, |     }, | ||||||
|  |     IncompatiblePlatform { | ||||||
|  |         /// The package that was built for a different platform.
 | ||||||
|  |         package: PackageName, | ||||||
|  |     }, | ||||||
|     MissingDependency { |     MissingDependency { | ||||||
|         /// The package that is missing a dependency.
 |         /// The package that is missing a dependency.
 | ||||||
|         package: PackageName, |         package: PackageName, | ||||||
|  | @ -604,6 +638,10 @@ impl Diagnostic for SitePackagesDiagnostic { | ||||||
|                 "The package `{package}` is broken or incomplete (unable to read `METADATA`). Consider recreating the virtualenv, or removing the package directory at: {}.", |                 "The package `{package}` is broken or incomplete (unable to read `METADATA`). Consider recreating the virtualenv, or removing the package directory at: {}.", | ||||||
|                 path.display(), |                 path.display(), | ||||||
|             ), |             ), | ||||||
|  |             Self::TagsUnavailable { package, path } => format!( | ||||||
|  |                 "The package `{package}` is broken or incomplete (unable to read `WHEEL` file). Consider recreating the virtualenv, or removing the package directory at: {}.", | ||||||
|  |                 path.display(), | ||||||
|  |             ), | ||||||
|             Self::IncompatiblePythonVersion { |             Self::IncompatiblePythonVersion { | ||||||
|                 package, |                 package, | ||||||
|                 version, |                 version, | ||||||
|  | @ -611,6 +649,9 @@ impl Diagnostic for SitePackagesDiagnostic { | ||||||
|             } => format!( |             } => format!( | ||||||
|                 "The package `{package}` requires Python {requires_python}, but `{version}` is installed" |                 "The package `{package}` requires Python {requires_python}, but `{version}` is installed" | ||||||
|             ), |             ), | ||||||
|  |             Self::IncompatiblePlatform { package } => { | ||||||
|  |                 format!("The package `{package}` was built for a different platform") | ||||||
|  |             } | ||||||
|             Self::MissingDependency { |             Self::MissingDependency { | ||||||
|                 package, |                 package, | ||||||
|                 requirement, |                 requirement, | ||||||
|  | @ -640,7 +681,9 @@ impl Diagnostic for SitePackagesDiagnostic { | ||||||
|     fn includes(&self, name: &PackageName) -> bool { |     fn includes(&self, name: &PackageName) -> bool { | ||||||
|         match self { |         match self { | ||||||
|             Self::MetadataUnavailable { package, .. } => name == package, |             Self::MetadataUnavailable { package, .. } => name == package, | ||||||
|  |             Self::TagsUnavailable { package, .. } => name == package, | ||||||
|             Self::IncompatiblePythonVersion { package, .. } => name == package, |             Self::IncompatiblePythonVersion { package, .. } => name == package, | ||||||
|  |             Self::IncompatiblePlatform { package } => name == package, | ||||||
|             Self::MissingDependency { package, .. } => name == package, |             Self::MissingDependency { package, .. } => name == package, | ||||||
|             Self::IncompatibleDependency { |             Self::IncompatibleDependency { | ||||||
|                 package, |                 package, | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ use uv_distribution_types::{CompatibleDist, IncompatibleDist, IncompatibleSource | ||||||
| use uv_distribution_types::{DistributionMetadata, IncompatibleWheel, Name, PrioritizedDist}; | use uv_distribution_types::{DistributionMetadata, IncompatibleWheel, Name, PrioritizedDist}; | ||||||
| use uv_normalize::PackageName; | use uv_normalize::PackageName; | ||||||
| use uv_pep440::Version; | use uv_pep440::Version; | ||||||
|  | use uv_platform_tags::Tags; | ||||||
| use uv_types::InstalledPackagesProvider; | use uv_types::InstalledPackagesProvider; | ||||||
| 
 | 
 | ||||||
| use crate::preferences::{Entry, PreferenceSource, Preferences}; | use crate::preferences::{Entry, PreferenceSource, Preferences}; | ||||||
|  | @ -84,6 +85,7 @@ impl CandidateSelector { | ||||||
|         exclusions: &'a Exclusions, |         exclusions: &'a Exclusions, | ||||||
|         index: Option<&'a IndexUrl>, |         index: Option<&'a IndexUrl>, | ||||||
|         env: &ResolverEnvironment, |         env: &ResolverEnvironment, | ||||||
|  |         tags: Option<&'a Tags>, | ||||||
|     ) -> Option<Candidate<'a>> { |     ) -> Option<Candidate<'a>> { | ||||||
|         let reinstall = exclusions.reinstall(package_name); |         let reinstall = exclusions.reinstall(package_name); | ||||||
|         let upgrade = exclusions.upgrade(package_name); |         let upgrade = exclusions.upgrade(package_name); | ||||||
|  | @ -106,6 +108,7 @@ impl CandidateSelector { | ||||||
|             reinstall, |             reinstall, | ||||||
|             index, |             index, | ||||||
|             env, |             env, | ||||||
|  |             tags, | ||||||
|         ) { |         ) { | ||||||
|             trace!("Using preference {} {}", preferred.name, preferred.version); |             trace!("Using preference {} {}", preferred.name, preferred.version); | ||||||
|             return Some(preferred); |             return Some(preferred); | ||||||
|  | @ -116,7 +119,7 @@ impl CandidateSelector { | ||||||
|         let installed = if reinstall { |         let installed = if reinstall { | ||||||
|             None |             None | ||||||
|         } else { |         } else { | ||||||
|             Self::get_installed(package_name, range, installed_packages) |             Self::get_installed(package_name, range, installed_packages, tags) | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         // If we're not upgrading, we should prefer the already-installed distribution.
 |         // If we're not upgrading, we should prefer the already-installed distribution.
 | ||||||
|  | @ -176,6 +179,7 @@ impl CandidateSelector { | ||||||
|         reinstall: bool, |         reinstall: bool, | ||||||
|         index: Option<&'a IndexUrl>, |         index: Option<&'a IndexUrl>, | ||||||
|         env: &ResolverEnvironment, |         env: &ResolverEnvironment, | ||||||
|  |         tags: Option<&'a Tags>, | ||||||
|     ) -> Option<Candidate<'a>> { |     ) -> Option<Candidate<'a>> { | ||||||
|         let preferences = preferences.get(package_name); |         let preferences = preferences.get(package_name); | ||||||
| 
 | 
 | ||||||
|  | @ -231,6 +235,7 @@ impl CandidateSelector { | ||||||
|             installed_packages, |             installed_packages, | ||||||
|             reinstall, |             reinstall, | ||||||
|             env, |             env, | ||||||
|  |             tags, | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -244,6 +249,7 @@ impl CandidateSelector { | ||||||
|         installed_packages: &'a InstalledPackages, |         installed_packages: &'a InstalledPackages, | ||||||
|         reinstall: bool, |         reinstall: bool, | ||||||
|         env: &ResolverEnvironment, |         env: &ResolverEnvironment, | ||||||
|  |         tags: Option<&Tags>, | ||||||
|     ) -> Option<Candidate<'a>> { |     ) -> Option<Candidate<'a>> { | ||||||
|         for (version, source) in preferences { |         for (version, source) in preferences { | ||||||
|             // Respect the version range for this requirement.
 |             // Respect the version range for this requirement.
 | ||||||
|  | @ -263,6 +269,17 @@ impl CandidateSelector { | ||||||
|                                 "Found installed version of {dist} that satisfies preference in {range}" |                                 "Found installed version of {dist} that satisfies preference in {range}" | ||||||
|                             ); |                             ); | ||||||
| 
 | 
 | ||||||
|  |                             // Verify that the installed distribution is compatible with the environment.
 | ||||||
|  |                             if tags.is_some_and(|tags| { | ||||||
|  |                                 let Ok(Some(wheel_tags)) = dist.read_tags() else { | ||||||
|  |                                     return false; | ||||||
|  |                                 }; | ||||||
|  |                                 !wheel_tags.is_compatible(tags) | ||||||
|  |                             }) { | ||||||
|  |                                 debug!("Platform tags mismatch for installed {dist}"); | ||||||
|  |                                 continue; | ||||||
|  |                             } | ||||||
|  | 
 | ||||||
|                             return Some(Candidate { |                             return Some(Candidate { | ||||||
|                                 name: package_name, |                                 name: package_name, | ||||||
|                                 version, |                                 version, | ||||||
|  | @ -351,6 +368,7 @@ impl CandidateSelector { | ||||||
|         package_name: &'a PackageName, |         package_name: &'a PackageName, | ||||||
|         range: &Range<Version>, |         range: &Range<Version>, | ||||||
|         installed_packages: &'a InstalledPackages, |         installed_packages: &'a InstalledPackages, | ||||||
|  |         tags: Option<&'a Tags>, | ||||||
|     ) -> Option<Candidate<'a>> { |     ) -> Option<Candidate<'a>> { | ||||||
|         let installed_dists = installed_packages.get_packages(package_name); |         let installed_dists = installed_packages.get_packages(package_name); | ||||||
|         match installed_dists.as_slice() { |         match installed_dists.as_slice() { | ||||||
|  | @ -363,7 +381,17 @@ impl CandidateSelector { | ||||||
|                     return None; |                     return None; | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 debug!("Found installed version of {dist} that satisfies {range}"); |                 // Verify that the installed distribution is compatible with the environment.
 | ||||||
|  |                 if tags.is_some_and(|tags| { | ||||||
|  |                     let Ok(Some(wheel_tags)) = dist.read_tags() else { | ||||||
|  |                         return false; | ||||||
|  |                     }; | ||||||
|  |                     !wheel_tags.is_compatible(tags) | ||||||
|  |                 }) { | ||||||
|  |                     debug!("Platform tags mismatch for installed {dist}"); | ||||||
|  |                     return None; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|                 return Some(Candidate { |                 return Some(Candidate { | ||||||
|                     name: package_name, |                     name: package_name, | ||||||
|                     version, |                     version, | ||||||
|  |  | ||||||
|  | @ -5270,18 +5270,14 @@ impl WheelTagHint { | ||||||
|     fn python_tags<'a>( |     fn python_tags<'a>( | ||||||
|         filenames: impl Iterator<Item = &'a WheelFilename> + 'a, |         filenames: impl Iterator<Item = &'a WheelFilename> + 'a, | ||||||
|     ) -> impl Iterator<Item = LanguageTag> + 'a { |     ) -> impl Iterator<Item = LanguageTag> + 'a { | ||||||
|         filenames |         filenames.flat_map(WheelFilename::python_tags).copied() | ||||||
|             .flat_map(uv_distribution_filename::WheelFilename::python_tags) |  | ||||||
|             .copied() |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Returns an iterator over the compatible Python tags of the available wheels.
 |     /// Returns an iterator over the compatible Python tags of the available wheels.
 | ||||||
|     fn abi_tags<'a>( |     fn abi_tags<'a>( | ||||||
|         filenames: impl Iterator<Item = &'a WheelFilename> + 'a, |         filenames: impl Iterator<Item = &'a WheelFilename> + 'a, | ||||||
|     ) -> impl Iterator<Item = AbiTag> + 'a { |     ) -> impl Iterator<Item = AbiTag> + 'a { | ||||||
|         filenames |         filenames.flat_map(WheelFilename::abi_tags).copied() | ||||||
|             .flat_map(uv_distribution_filename::WheelFilename::abi_tags) |  | ||||||
|             .copied() |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Returns the set of platform tags for the distribution that are ABI-compatible with the given
 |     /// Returns the set of platform tags for the distribution that are ABI-compatible with the given
 | ||||||
|  |  | ||||||
|  | @ -1269,6 +1269,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag | ||||||
|             &self.exclusions, |             &self.exclusions, | ||||||
|             index, |             index, | ||||||
|             env, |             env, | ||||||
|  |             self.tags.as_ref(), | ||||||
|         ) else { |         ) else { | ||||||
|             // Short circuit: we couldn't find _any_ versions for a package.
 |             // Short circuit: we couldn't find _any_ versions for a package.
 | ||||||
|             return Ok(None); |             return Ok(None); | ||||||
|  | @ -1485,6 +1486,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag | ||||||
|             &self.exclusions, |             &self.exclusions, | ||||||
|             index, |             index, | ||||||
|             env, |             env, | ||||||
|  |             self.tags.as_ref(), | ||||||
|         ) else { |         ) else { | ||||||
|             return Ok(None); |             return Ok(None); | ||||||
|         }; |         }; | ||||||
|  | @ -2447,6 +2449,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag | ||||||
|                     &self.exclusions, |                     &self.exclusions, | ||||||
|                     None, |                     None, | ||||||
|                     &env, |                     &env, | ||||||
|  |                     self.tags.as_ref(), | ||||||
|                 ) else { |                 ) else { | ||||||
|                     return Ok(None); |                     return Ok(None); | ||||||
|                 }; |                 }; | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ use uv_python::{ | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| use crate::commands::pip::operations::report_target_environment; | use crate::commands::pip::operations::report_target_environment; | ||||||
| use crate::commands::pip::resolution_markers; | use crate::commands::pip::{resolution_markers, resolution_tags}; | ||||||
| use crate::commands::{ExitStatus, elapsed}; | use crate::commands::{ExitStatus, elapsed}; | ||||||
| use crate::printer::Printer; | use crate::printer::Printer; | ||||||
| 
 | 
 | ||||||
|  | @ -57,12 +57,15 @@ pub(crate) fn pip_check( | ||||||
|         .dimmed() |         .dimmed() | ||||||
|     )?; |     )?; | ||||||
| 
 | 
 | ||||||
|     // Determine the markers to use for resolution.
 |     // Determine the markers and tags to use for resolution.
 | ||||||
|     let markers = resolution_markers(python_version, python_platform, environment.interpreter()); |     let markers = resolution_markers(python_version, python_platform, environment.interpreter()); | ||||||
|  |     let tags = resolution_tags(python_version, python_platform, environment.interpreter())?; | ||||||
| 
 | 
 | ||||||
|     // Run the diagnostics.
 |     // Run the diagnostics.
 | ||||||
|     let diagnostics: Vec<SitePackagesDiagnostic> = |     let diagnostics: Vec<SitePackagesDiagnostic> = site_packages | ||||||
|         site_packages.diagnostics(&markers)?.into_iter().collect(); |         .diagnostics(&markers, &tags)? | ||||||
|  |         .into_iter() | ||||||
|  |         .collect(); | ||||||
| 
 | 
 | ||||||
|     if diagnostics.is_empty() { |     if diagnostics.is_empty() { | ||||||
|         writeln!( |         writeln!( | ||||||
|  |  | ||||||
|  | @ -87,11 +87,12 @@ pub(crate) fn pip_freeze( | ||||||
| 
 | 
 | ||||||
|     // Validate that the environment is consistent.
 |     // Validate that the environment is consistent.
 | ||||||
|     if strict { |     if strict { | ||||||
|         // Determine the markers to use for resolution.
 |         // Determine the markers and tags to use for resolution.
 | ||||||
|         let markers = environment.interpreter().resolver_marker_environment(); |         let markers = environment.interpreter().resolver_marker_environment(); | ||||||
|  |         let tags = environment.interpreter().tags()?; | ||||||
| 
 | 
 | ||||||
|         for entry in site_packages { |         for entry in site_packages { | ||||||
|             for diagnostic in entry.diagnostics(&markers)? { |             for diagnostic in entry.diagnostics(&markers, tags)? { | ||||||
|                 writeln!( |                 writeln!( | ||||||
|                     printer.stderr(), |                     printer.stderr(), | ||||||
|                     "{}{} {}", |                     "{}{} {}", | ||||||
|  |  | ||||||
|  | @ -265,13 +265,18 @@ pub(crate) async fn pip_install( | ||||||
|         }) |         }) | ||||||
|         .ok(); |         .ok(); | ||||||
| 
 | 
 | ||||||
|     // Determine the markers to use for the resolution.
 |     // Determine the markers and tags to use for the resolution.
 | ||||||
|     let interpreter = environment.interpreter(); |     let interpreter = environment.interpreter(); | ||||||
|     let marker_env = resolution_markers( |     let marker_env = resolution_markers( | ||||||
|         python_version.as_ref(), |         python_version.as_ref(), | ||||||
|         python_platform.as_ref(), |         python_platform.as_ref(), | ||||||
|         interpreter, |         interpreter, | ||||||
|     ); |     ); | ||||||
|  |     let tags = resolution_tags( | ||||||
|  |         python_version.as_ref(), | ||||||
|  |         python_platform.as_ref(), | ||||||
|  |         interpreter, | ||||||
|  |     )?; | ||||||
| 
 | 
 | ||||||
|     // Determine the set of installed packages.
 |     // Determine the set of installed packages.
 | ||||||
|     let site_packages = SitePackages::from_environment(&environment)?; |     let site_packages = SitePackages::from_environment(&environment)?; | ||||||
|  | @ -291,6 +296,7 @@ pub(crate) async fn pip_install( | ||||||
|             &constraints, |             &constraints, | ||||||
|             &overrides, |             &overrides, | ||||||
|             &marker_env, |             &marker_env, | ||||||
|  |             &tags, | ||||||
|             config_settings, |             config_settings, | ||||||
|             config_settings_package, |             config_settings_package, | ||||||
|             &extra_build_requires, |             &extra_build_requires, | ||||||
|  | @ -329,13 +335,6 @@ pub(crate) async fn pip_install( | ||||||
|         PythonRequirement::from_interpreter(interpreter) |         PythonRequirement::from_interpreter(interpreter) | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     // Determine the tags to use for the resolution.
 |  | ||||||
|     let tags = resolution_tags( |  | ||||||
|         python_version.as_ref(), |  | ||||||
|         python_platform.as_ref(), |  | ||||||
|         interpreter, |  | ||||||
|     )?; |  | ||||||
| 
 |  | ||||||
|     // Collect the set of required hashes.
 |     // Collect the set of required hashes.
 | ||||||
|     let hasher = if let Some(hash_checking) = hash_checking { |     let hasher = if let Some(hash_checking) = hash_checking { | ||||||
|         HashStrategy::from_requirements( |         HashStrategy::from_requirements( | ||||||
|  | @ -636,7 +635,7 @@ pub(crate) async fn pip_install( | ||||||
| 
 | 
 | ||||||
|     // Notify the user of any environment diagnostics.
 |     // Notify the user of any environment diagnostics.
 | ||||||
|     if strict && !dry_run.enabled() { |     if strict && !dry_run.enabled() { | ||||||
|         operations::diagnose_environment(&resolution, &environment, &marker_env, printer)?; |         operations::diagnose_environment(&resolution, &environment, &marker_env, &tags, printer)?; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Ok(ExitStatus::Success) |     Ok(ExitStatus::Success) | ||||||
|  |  | ||||||
|  | @ -272,10 +272,11 @@ pub(crate) async fn pip_list( | ||||||
| 
 | 
 | ||||||
|     // Validate that the environment is consistent.
 |     // Validate that the environment is consistent.
 | ||||||
|     if strict { |     if strict { | ||||||
|         // Determine the markers to use for resolution.
 |         // Determine the markers and tags to use for resolution.
 | ||||||
|         let markers = environment.interpreter().resolver_marker_environment(); |         let markers = environment.interpreter().resolver_marker_environment(); | ||||||
|  |         let tags = environment.interpreter().tags()?; | ||||||
| 
 | 
 | ||||||
|         for diagnostic in site_packages.diagnostics(&markers)? { |         for diagnostic in site_packages.diagnostics(&markers, tags)? { | ||||||
|             writeln!( |             writeln!( | ||||||
|                 printer.stderr(), |                 printer.stderr(), | ||||||
|                 "{}{} {}", |                 "{}{} {}", | ||||||
|  |  | ||||||
|  | @ -975,10 +975,11 @@ pub(crate) fn diagnose_environment( | ||||||
|     resolution: &Resolution, |     resolution: &Resolution, | ||||||
|     venv: &PythonEnvironment, |     venv: &PythonEnvironment, | ||||||
|     markers: &ResolverMarkerEnvironment, |     markers: &ResolverMarkerEnvironment, | ||||||
|  |     tags: &Tags, | ||||||
|     printer: Printer, |     printer: Printer, | ||||||
| ) -> Result<(), Error> { | ) -> Result<(), Error> { | ||||||
|     let site_packages = SitePackages::from_environment(venv)?; |     let site_packages = SitePackages::from_environment(venv)?; | ||||||
|     for diagnostic in site_packages.diagnostics(markers)? { |     for diagnostic in site_packages.diagnostics(markers, tags)? { | ||||||
|         // Only surface diagnostics that are "relevant" to the current resolution.
 |         // Only surface diagnostics that are "relevant" to the current resolution.
 | ||||||
|         if resolution |         if resolution | ||||||
|             .distributions() |             .distributions() | ||||||
|  |  | ||||||
|  | @ -57,8 +57,9 @@ pub(crate) fn pip_show( | ||||||
|     // Build the installed index.
 |     // Build the installed index.
 | ||||||
|     let site_packages = SitePackages::from_environment(&environment)?; |     let site_packages = SitePackages::from_environment(&environment)?; | ||||||
| 
 | 
 | ||||||
|     // Determine the markers to use for resolution.
 |     // Determine the markers and tags to use for resolution.
 | ||||||
|     let markers = environment.interpreter().resolver_marker_environment(); |     let markers = environment.interpreter().resolver_marker_environment(); | ||||||
|  |     let tags = environment.interpreter().tags()?; | ||||||
| 
 | 
 | ||||||
|     // Sort and deduplicate the packages, which are keyed by name.
 |     // Sort and deduplicate the packages, which are keyed by name.
 | ||||||
|     packages.sort_unstable(); |     packages.sort_unstable(); | ||||||
|  | @ -201,7 +202,7 @@ pub(crate) fn pip_show( | ||||||
| 
 | 
 | ||||||
|     // Validate that the environment is consistent.
 |     // Validate that the environment is consistent.
 | ||||||
|     if strict { |     if strict { | ||||||
|         for diagnostic in site_packages.diagnostics(&markers)? { |         for diagnostic in site_packages.diagnostics(&markers, tags)? { | ||||||
|             writeln!( |             writeln!( | ||||||
|                 printer.stderr(), |                 printer.stderr(), | ||||||
|                 "{}{} {}", |                 "{}{} {}", | ||||||
|  |  | ||||||
|  | @ -566,7 +566,7 @@ pub(crate) async fn pip_sync( | ||||||
| 
 | 
 | ||||||
|     // Notify the user of any environment diagnostics.
 |     // Notify the user of any environment diagnostics.
 | ||||||
|     if strict && !dry_run.enabled() { |     if strict && !dry_run.enabled() { | ||||||
|         operations::diagnose_environment(&resolution, &environment, &marker_env, printer)?; |         operations::diagnose_environment(&resolution, &environment, &marker_env, &tags, printer)?; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Ok(ExitStatus::Success) |     Ok(ExitStatus::Success) | ||||||
|  |  | ||||||
|  | @ -80,8 +80,9 @@ pub(crate) async fn pip_tree( | ||||||
|         packages |         packages | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     // Determine the markers to use for the resolution.
 |     // Determine the markers and tags to use for the resolution.
 | ||||||
|     let markers = environment.interpreter().resolver_marker_environment(); |     let markers = environment.interpreter().resolver_marker_environment(); | ||||||
|  |     let tags = environment.interpreter().tags()?; | ||||||
| 
 | 
 | ||||||
|     // Determine the latest version for each package.
 |     // Determine the latest version for each package.
 | ||||||
|     let latest = if outdated && !packages.is_empty() { |     let latest = if outdated && !packages.is_empty() { | ||||||
|  | @ -178,7 +179,7 @@ pub(crate) async fn pip_tree( | ||||||
| 
 | 
 | ||||||
|     // Validate that the environment is consistent.
 |     // Validate that the environment is consistent.
 | ||||||
|     if strict { |     if strict { | ||||||
|         for diagnostic in site_packages.diagnostics(&markers)? { |         for diagnostic in site_packages.diagnostics(&markers, tags)? { | ||||||
|             writeln!( |             writeln!( | ||||||
|                 printer.stderr(), |                 printer.stderr(), | ||||||
|                 "{}{} {}", |                 "{}{} {}", | ||||||
|  |  | ||||||
|  | @ -2266,9 +2266,10 @@ pub(crate) async fn update_environment( | ||||||
|         .. |         .. | ||||||
|     } = spec; |     } = spec; | ||||||
| 
 | 
 | ||||||
|     // Determine markers to use for resolution.
 |     // Determine markers and tags to use for resolution.
 | ||||||
|     let interpreter = venv.interpreter(); |     let interpreter = venv.interpreter(); | ||||||
|     let marker_env = venv.interpreter().resolver_marker_environment(); |     let marker_env = venv.interpreter().resolver_marker_environment(); | ||||||
|  |     let tags = venv.interpreter().tags()?; | ||||||
| 
 | 
 | ||||||
|     // Check if the current environment satisfies the requirements
 |     // Check if the current environment satisfies the requirements
 | ||||||
|     let site_packages = SitePackages::from_environment(&venv)?; |     let site_packages = SitePackages::from_environment(&venv)?; | ||||||
|  | @ -2282,6 +2283,7 @@ pub(crate) async fn update_environment( | ||||||
|             &constraints, |             &constraints, | ||||||
|             &overrides, |             &overrides, | ||||||
|             &marker_env, |             &marker_env, | ||||||
|  |             tags, | ||||||
|             config_setting, |             config_setting, | ||||||
|             config_settings_package, |             config_settings_package, | ||||||
|             &extra_build_requires, |             &extra_build_requires, | ||||||
|  |  | ||||||
|  | @ -1354,6 +1354,12 @@ fn can_skip_ephemeral( | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     // Determine the markers and tags to use for resolution.
 | ||||||
|  |     let markers = interpreter.resolver_marker_environment(); | ||||||
|  |     let Ok(tags) = interpreter.tags() else { | ||||||
|  |         return false; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|     // Lower the extra build dependencies, if any.
 |     // Lower the extra build dependencies, if any.
 | ||||||
|     let extra_build_requires = |     let extra_build_requires = | ||||||
|         LoweredExtraBuildDependencies::from_non_lowered(extra_build_dependencies.clone()) |         LoweredExtraBuildDependencies::from_non_lowered(extra_build_dependencies.clone()) | ||||||
|  | @ -1363,7 +1369,8 @@ fn can_skip_ephemeral( | ||||||
|         &spec.requirements, |         &spec.requirements, | ||||||
|         &spec.constraints, |         &spec.constraints, | ||||||
|         &spec.overrides, |         &spec.overrides, | ||||||
|         &interpreter.resolver_marker_environment(), |         &markers, | ||||||
|  |         tags, | ||||||
|         config_setting, |         config_setting, | ||||||
|         config_settings_package, |         config_settings_package, | ||||||
|         &extra_build_requires, |         &extra_build_requires, | ||||||
|  |  | ||||||
|  | @ -965,6 +965,10 @@ async fn get_or_create_environment( | ||||||
|                     ) |                     ) | ||||||
|                     .into_inner(); |                     .into_inner(); | ||||||
| 
 | 
 | ||||||
|  |                     // Determine the markers and tags to use for the resolution.
 | ||||||
|  |                     let markers = interpreter.resolver_marker_environment(); | ||||||
|  |                     let tags = interpreter.tags()?; | ||||||
|  | 
 | ||||||
|                     // Check if the installed packages meet the requirements.
 |                     // Check if the installed packages meet the requirements.
 | ||||||
|                     let site_packages = SitePackages::from_environment(&environment)?; |                     let site_packages = SitePackages::from_environment(&environment)?; | ||||||
|                     if matches!( |                     if matches!( | ||||||
|  | @ -972,7 +976,8 @@ async fn get_or_create_environment( | ||||||
|                             requirements.iter(), |                             requirements.iter(), | ||||||
|                             constraints.iter(), |                             constraints.iter(), | ||||||
|                             overrides.iter(), |                             overrides.iter(), | ||||||
|                             &interpreter.resolver_marker_environment(), |                             &markers, | ||||||
|  |                             tags, | ||||||
|                             config_setting, |                             config_setting, | ||||||
|                             config_settings_package, |                             config_settings_package, | ||||||
|                             &extra_build_requires, |                             &extra_build_requires, | ||||||
|  |  | ||||||
|  | @ -8745,7 +8745,7 @@ fn no_extension() { | ||||||
| 
 | 
 | ||||||
| /// Regression test for: <https://github.com/astral-sh/uv/pull/6646>
 | /// Regression test for: <https://github.com/astral-sh/uv/pull/6646>
 | ||||||
| #[test] | #[test] | ||||||
| fn switch_platform() -> Result<()> { | fn switch_python_version() -> Result<()> { | ||||||
|     let context = TestContext::new("3.12"); |     let context = TestContext::new("3.12"); | ||||||
| 
 | 
 | ||||||
|     let requirements_txt = context.temp_dir.child("requirements.txt"); |     let requirements_txt = context.temp_dir.child("requirements.txt"); | ||||||
|  | @ -12718,3 +12718,65 @@ fn transitive_dependency_config_settings_invalidation() -> Result<()> { | ||||||
| 
 | 
 | ||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn switch_platform() { | ||||||
|  |     let context = TestContext::new("3.12"); | ||||||
|  | 
 | ||||||
|  |     uv_snapshot!(context.pip_install() | ||||||
|  |         .arg("cffi") | ||||||
|  |         .arg("--python-platform") | ||||||
|  |         .arg("windows"), @r" | ||||||
|  |     success: true | ||||||
|  |     exit_code: 0 | ||||||
|  |     ----- stdout ----- | ||||||
|  | 
 | ||||||
|  |     ----- stderr ----- | ||||||
|  |     Resolved 2 packages in [TIME] | ||||||
|  |     Prepared 2 packages in [TIME] | ||||||
|  |     Installed 2 packages in [TIME] | ||||||
|  |      + cffi==1.16.0 | ||||||
|  |      + pycparser==2.21 | ||||||
|  |     " | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     uv_snapshot!(context.pip_check().arg("--python-platform").arg("windows"), @r" | ||||||
|  |     success: true | ||||||
|  |     exit_code: 0 | ||||||
|  |     ----- stdout ----- | ||||||
|  | 
 | ||||||
|  |     ----- stderr ----- | ||||||
|  |     Checked 2 packages in [TIME] | ||||||
|  |     All installed packages are compatible | ||||||
|  |     " | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     uv_snapshot!(context.pip_check().arg("--python-platform").arg("linux"), @r" | ||||||
|  |     success: false | ||||||
|  |     exit_code: 1 | ||||||
|  |     ----- stdout ----- | ||||||
|  | 
 | ||||||
|  |     ----- stderr ----- | ||||||
|  |     Checked 2 packages in [TIME] | ||||||
|  |     Found 1 incompatibility | ||||||
|  |     The package `cffi` was built for a different platform | ||||||
|  |     " | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     uv_snapshot!(context.pip_install() | ||||||
|  |         .arg("cffi") | ||||||
|  |         .arg("--python-platform") | ||||||
|  |         .arg("linux"), @r" | ||||||
|  |     success: true | ||||||
|  |     exit_code: 0 | ||||||
|  |     ----- stdout ----- | ||||||
|  | 
 | ||||||
|  |     ----- stderr ----- | ||||||
|  |     Resolved 2 packages in [TIME] | ||||||
|  |     Prepared 1 package in [TIME] | ||||||
|  |     Uninstalled 1 package in [TIME] | ||||||
|  |     Installed 1 package in [TIME] | ||||||
|  |      ~ cffi==1.16.0 | ||||||
|  |     " | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Charlie Marsh
						Charlie Marsh