1- use std:: collections:: { HashMap , HashSet } ;
21use std:: path:: Path ;
32use std:: str:: FromStr ;
43
54use anyhow:: Result ;
6- use futures:: future:: Either ;
7- use futures:: { StreamExt , TryFutureExt } ;
8- use pep440_rs:: Version ;
9- use pep508_rs:: { Requirement , VersionOrUrl } ;
105use tracing:: debug;
116
12- use puffin_client:: { File , PypiClientBuilder , SimpleJson } ;
137use puffin_interpreter:: PythonExecutable ;
14- use puffin_package:: metadata:: Metadata21 ;
15- use puffin_package:: package_name:: PackageName ;
16- use puffin_package:: wheel:: WheelFilename ;
178use puffin_platform:: Platform ;
9+ use puffin_resolve:: resolve;
1810
1911use crate :: commands:: ExitStatus ;
2012
21- #[ derive( Debug ) ]
22- enum Request {
23- Package ( Requirement ) ,
24- Version ( Requirement , File ) ,
25- }
26-
27- #[ derive( Debug ) ]
28- enum Response {
29- Package ( SimpleJson , Requirement ) ,
30- Version ( Metadata21 , Requirement ) ,
31- }
32-
3313pub ( crate ) async fn install ( src : & Path , cache : Option < & Path > ) -> Result < ExitStatus > {
3414 // Read the `requirements.txt` from disk.
3515 let requirements_txt = std:: fs:: read_to_string ( src) ?;
@@ -45,134 +25,17 @@ pub(crate) async fn install(src: &Path, cache: Option<&Path>) -> Result<ExitStat
4525 python. executable( ) . display( )
4626 ) ;
4727
48- // Determine the compatible platform tags.
49- let tags = platform. compatible_tags ( python. version ( ) ) ?;
50-
51- // Instantiate a client.
52- let pypi_client = {
53- let mut pypi_client = PypiClientBuilder :: default ( ) ;
54- if let Some ( cache) = cache {
55- pypi_client = pypi_client. cache ( cache) ;
56- }
57- pypi_client. build ( )
58- } ;
59-
60- // A channel to fetch package metadata (e.g., given `flask`, fetch all versions) and version
61- // metadata (e.g., given `flask==1.0.0`, fetch the metadata for that version).
62- let ( package_sink, package_stream) = futures:: channel:: mpsc:: unbounded ( ) ;
63-
64- // Initialize the package stream.
65- let mut package_stream = package_stream
66- . map ( |request : Request | match request {
67- Request :: Package ( requirement) => Either :: Left (
68- pypi_client
69- . simple ( requirement. name . clone ( ) )
70- . map_ok ( move |metadata| Response :: Package ( metadata, requirement) ) ,
71- ) ,
72- Request :: Version ( requirement, file) => Either :: Right (
73- pypi_client
74- . file ( file)
75- . map_ok ( move |metadata| Response :: Version ( metadata, requirement) ) ,
76- ) ,
77- } )
78- . buffer_unordered ( 32 )
79- . ready_chunks ( 32 ) ;
80-
81- // Push all the requirements into the package sink.
82- let mut in_flight: HashSet < PackageName > = HashSet :: with_capacity ( requirements. len ( ) ) ;
83- for requirement in & * requirements {
84- debug ! ( "--> adding root dependency: {}" , requirement) ;
85- package_sink. unbounded_send ( Request :: Package ( requirement. clone ( ) ) ) ?;
86- in_flight. insert ( PackageName :: normalize ( & requirement. name ) ) ;
87- }
88-
89- // Resolve the requirements.
90- let mut resolution: HashMap < PackageName , Version > = HashMap :: with_capacity ( requirements. len ( ) ) ;
91-
92- while let Some ( chunk) = package_stream. next ( ) . await {
93- for result in chunk {
94- let result: Response = result?;
95- match result {
96- Response :: Package ( metadata, requirement) => {
97- // TODO(charlie): Support URLs. Right now, we treat a URL as an unpinned dependency.
98- let specifiers =
99- requirement
100- . version_or_url
101- . as_ref ( )
102- . and_then ( |version_or_url| match version_or_url {
103- VersionOrUrl :: VersionSpecifier ( specifiers) => Some ( specifiers) ,
104- VersionOrUrl :: Url ( _) => None ,
105- } ) ;
106-
107- // Pick a version that satisfies the requirement.
108- let Some ( file) = metadata. files . iter ( ) . rev ( ) . find ( |file| {
109- // We only support wheels for now.
110- let Ok ( name) = WheelFilename :: from_str ( file. filename . as_str ( ) ) else {
111- return false ;
112- } ;
113-
114- let Ok ( version) = Version :: from_str ( & name. version ) else {
115- return false ;
116- } ;
117-
118- if !name. is_compatible ( & tags) {
119- return false ;
120- }
121-
122- specifiers
123- . iter ( )
124- . all ( |specifier| specifier. contains ( & version) )
125- } ) else {
126- continue ;
127- } ;
128-
129- package_sink. unbounded_send ( Request :: Version ( requirement, file. clone ( ) ) ) ?;
130- }
131- Response :: Version ( metadata, requirement) => {
132- debug ! (
133- "--> selected version {} for {}" ,
134- metadata. version, requirement
135- ) ;
136-
137- // Add to the resolved set.
138- let normalized_name = PackageName :: normalize ( & requirement. name ) ;
139- in_flight. remove ( & normalized_name) ;
140- resolution. insert ( normalized_name, metadata. version ) ;
141-
142- // Enqueue its dependencies.
143- for dependency in metadata. requires_dist {
144- if !dependency. evaluate_markers (
145- python. markers ( ) ,
146- requirement. extras . clone ( ) . unwrap_or_default ( ) ,
147- ) {
148- debug ! ( "--> ignoring {dependency} due to environment mismatch" ) ;
149- continue ;
150- }
151-
152- let normalized_name = PackageName :: normalize ( & dependency. name ) ;
153-
154- if resolution. contains_key ( & normalized_name) {
155- continue ;
156- }
157-
158- if !in_flight. insert ( normalized_name) {
159- continue ;
160- }
161-
162- debug ! ( "--> adding transitive dependency: {}" , dependency) ;
163-
164- package_sink. unbounded_send ( Request :: Package ( dependency) ) ?;
165- }
166- }
167- }
168- }
169-
170- if in_flight. is_empty ( ) {
171- break ;
172- }
173- }
174-
175- for ( name, version) in resolution {
28+ // Resolve the dependencies.
29+ let resolution = resolve (
30+ & requirements,
31+ python. version ( ) ,
32+ python. markers ( ) ,
33+ & platform,
34+ cache,
35+ )
36+ . await ?;
37+
38+ for ( name, version) in resolution. iter ( ) {
17639 #[ allow( clippy:: print_stdout) ]
17740 {
17841 println ! ( "{name}=={version}" ) ;
0 commit comments