1
1
use crate :: build_options:: CargoOptions ;
2
2
use crate :: target:: Arch ;
3
+ use crate :: BuildContext ;
3
4
use crate :: BuildOptions ;
4
5
use crate :: PlatformTag ;
5
6
use crate :: PythonInterpreter ;
6
7
use crate :: Target ;
7
8
use anyhow:: { anyhow, bail, Context , Result } ;
8
9
use cargo_options:: heading;
9
10
use pep508_rs:: { MarkerExpression , MarkerOperator , MarkerTree , MarkerValue } ;
11
+ use regex:: Regex ;
12
+ use std:: fs;
10
13
use std:: path:: Path ;
11
14
use std:: path:: PathBuf ;
12
15
use std:: process:: Command ;
13
16
use tempfile:: TempDir ;
17
+ use url:: Url ;
14
18
15
19
/// Install the crate as module in the current virtualenv
16
20
#[ derive( Debug , clap:: Parser ) ]
@@ -72,6 +76,143 @@ fn make_pip_command(python_path: &Path, pip_path: Option<&Path>) -> Command {
72
76
}
73
77
}
74
78
79
+ fn install_dependencies (
80
+ build_context : & BuildContext ,
81
+ extras : & [ String ] ,
82
+ interpreter : & PythonInterpreter ,
83
+ pip_path : Option < & Path > ,
84
+ ) -> Result < ( ) > {
85
+ if !build_context. metadata21 . requires_dist . is_empty ( ) {
86
+ let mut args = vec ! [ "install" . to_string( ) ] ;
87
+ args. extend ( build_context. metadata21 . requires_dist . iter ( ) . map ( |x| {
88
+ let mut pkg = x. clone ( ) ;
89
+ // Remove extra marker to make it installable with pip
90
+ // Keep in sync with `Metadata21::merge_pyproject_toml()`!
91
+ for extra in extras {
92
+ pkg. marker = pkg. marker . and_then ( |marker| -> Option < MarkerTree > {
93
+ match marker. clone ( ) {
94
+ MarkerTree :: Expression ( MarkerExpression {
95
+ l_value : MarkerValue :: Extra ,
96
+ operator : MarkerOperator :: Equal ,
97
+ r_value : MarkerValue :: QuotedString ( extra_value) ,
98
+ } ) if & extra_value == extra => None ,
99
+ MarkerTree :: And ( and) => match & * and {
100
+ [ existing, MarkerTree :: Expression ( MarkerExpression {
101
+ l_value : MarkerValue :: Extra ,
102
+ operator : MarkerOperator :: Equal ,
103
+ r_value : MarkerValue :: QuotedString ( extra_value) ,
104
+ } ) ] if extra_value == extra => Some ( existing. clone ( ) ) ,
105
+ _ => Some ( marker) ,
106
+ } ,
107
+ _ => Some ( marker) ,
108
+ }
109
+ } ) ;
110
+ }
111
+ pkg. to_string ( )
112
+ } ) ) ;
113
+ let status = make_pip_command ( & interpreter. executable , pip_path)
114
+ . args ( & args)
115
+ . status ( )
116
+ . context ( "Failed to run pip install" ) ?;
117
+ if !status. success ( ) {
118
+ bail ! ( r#"pip install finished with "{}""# , status)
119
+ }
120
+ }
121
+ Ok ( ( ) )
122
+ }
123
+
124
+ fn pip_install_wheel (
125
+ build_context : & BuildContext ,
126
+ python : & Path ,
127
+ venv_dir : & Path ,
128
+ pip_path : Option < & Path > ,
129
+ wheel_filename : & Path ,
130
+ ) -> Result < ( ) > {
131
+ let mut pip_cmd = make_pip_command ( python, pip_path) ;
132
+ let output = pip_cmd
133
+ . args ( [ "install" , "--no-deps" , "--force-reinstall" ] )
134
+ . arg ( dunce:: simplified ( wheel_filename) )
135
+ . output ( )
136
+ . context ( format ! (
137
+ "pip install failed (ran {:?} with {:?})" ,
138
+ pip_cmd. get_program( ) ,
139
+ & pip_cmd. get_args( ) . collect:: <Vec <_>>( ) ,
140
+ ) ) ?;
141
+ if !output. status . success ( ) {
142
+ bail ! (
143
+ "pip install in {} failed running {:?}: {}\n --- Stdout:\n {}\n --- Stderr:\n {}\n ---\n " ,
144
+ venv_dir. display( ) ,
145
+ & pip_cmd. get_args( ) . collect:: <Vec <_>>( ) ,
146
+ output. status,
147
+ String :: from_utf8_lossy( & output. stdout) . trim( ) ,
148
+ String :: from_utf8_lossy( & output. stderr) . trim( ) ,
149
+ ) ;
150
+ }
151
+ if !output. stderr . is_empty ( ) {
152
+ eprintln ! (
153
+ "⚠️ Warning: pip raised a warning running {:?}:\n {}" ,
154
+ & pip_cmd. get_args( ) . collect:: <Vec <_>>( ) ,
155
+ String :: from_utf8_lossy( & output. stderr) . trim( ) ,
156
+ ) ;
157
+ }
158
+ fix_direct_url ( build_context, python, pip_path) ?;
159
+ Ok ( ( ) )
160
+ }
161
+
162
+ /// Each editable-installed python package has a direct_url.json file that includes a file:// URL
163
+ /// indicating the location of the source code of that project. The maturin import hook uses this
164
+ /// URL to locate and rebuild editable-installed projects.
165
+ ///
166
+ /// When a maturin package is installed using `pip install -e`, pip takes care of writing the
167
+ /// correct URL, however when a maturin package is installed with `maturin develop`, the URL is
168
+ /// set to the path to the temporary wheel file created during installation.
169
+ fn fix_direct_url (
170
+ build_context : & BuildContext ,
171
+ python : & Path ,
172
+ pip_path : Option < & Path > ,
173
+ ) -> Result < ( ) > {
174
+ println ! ( "✏️ Setting installed package as editable" ) ;
175
+ let mut pip_cmd = make_pip_command ( python, pip_path) ;
176
+ let output = pip_cmd
177
+ . args ( [ "show" , "--files" ] )
178
+ . arg ( & build_context. metadata21 . name )
179
+ . output ( )
180
+ . context ( format ! (
181
+ "pip show failed (ran {:?} with {:?})" ,
182
+ pip_cmd. get_program( ) ,
183
+ & pip_cmd. get_args( ) . collect:: <Vec <_>>( ) ,
184
+ ) ) ?;
185
+ if let Some ( direct_url_path) = parse_direct_url_path ( & String :: from_utf8_lossy ( & output. stdout ) ) ?
186
+ {
187
+ let project_dir = build_context
188
+ . pyproject_toml_path
189
+ . parent ( )
190
+ . ok_or_else ( || anyhow ! ( "failed to get project directory" ) ) ?;
191
+ let uri = Url :: from_file_path ( project_dir)
192
+ . map_err ( |_| anyhow ! ( "failed to convert project directory to file URL" ) ) ?;
193
+ let content = format ! ( "{{\" dir_info\" : {{\" editable\" : true}}, \" url\" : \" {uri}\" }}" ) ;
194
+ fs:: write ( direct_url_path, content) ?;
195
+ }
196
+ Ok ( ( ) )
197
+ }
198
+
199
+ fn parse_direct_url_path ( pip_show_output : & str ) -> Result < Option < PathBuf > > {
200
+ if let Some ( Some ( location) ) = Regex :: new ( r"Location: ([^\r\n]*)" ) ?
201
+ . captures ( pip_show_output)
202
+ . map ( |c| c. get ( 1 ) )
203
+ {
204
+ if let Some ( Some ( direct_url_path) ) = Regex :: new ( r" (.*direct_url.json)" ) ?
205
+ . captures ( pip_show_output)
206
+ . map ( |c| c. get ( 1 ) )
207
+ {
208
+ return Ok ( Some (
209
+ PathBuf :: from ( location. as_str ( ) ) . join ( direct_url_path. as_str ( ) ) ,
210
+ ) ) ;
211
+ }
212
+ }
213
+ Ok ( None )
214
+ }
215
+
75
216
/// Installs a crate by compiling it and copying the shared library to site-packages.
76
217
/// Also adds the dist-info directory to make sure pip and other tools detect the library
77
218
///
@@ -137,74 +278,18 @@ pub fn develop(develop_options: DevelopOptions, venv_dir: &Path) -> Result<()> {
137
278
|| anyhow ! ( "Expected `python` to be a python interpreter inside a virtualenv ಠ_ಠ" ) ,
138
279
) ?;
139
280
140
- // Install dependencies
141
- if !build_context. metadata21 . requires_dist . is_empty ( ) {
142
- let mut args = vec ! [ "install" . to_string( ) ] ;
143
- args. extend ( build_context. metadata21 . requires_dist . iter ( ) . map ( |x| {
144
- let mut pkg = x. clone ( ) ;
145
- // Remove extra marker to make it installable with pip
146
- // Keep in sync with `Metadata21::merge_pyproject_toml()`!
147
- for extra in & extras {
148
- pkg. marker = pkg. marker . and_then ( |marker| -> Option < MarkerTree > {
149
- match marker. clone ( ) {
150
- MarkerTree :: Expression ( MarkerExpression {
151
- l_value : MarkerValue :: Extra ,
152
- operator : MarkerOperator :: Equal ,
153
- r_value : MarkerValue :: QuotedString ( extra_value) ,
154
- } ) if & extra_value == extra => None ,
155
- MarkerTree :: And ( and) => match & * and {
156
- [ existing, MarkerTree :: Expression ( MarkerExpression {
157
- l_value : MarkerValue :: Extra ,
158
- operator : MarkerOperator :: Equal ,
159
- r_value : MarkerValue :: QuotedString ( extra_value) ,
160
- } ) ] if extra_value == extra => Some ( existing. clone ( ) ) ,
161
- _ => Some ( marker) ,
162
- } ,
163
- _ => Some ( marker) ,
164
- }
165
- } ) ;
166
- }
167
- pkg. to_string ( )
168
- } ) ) ;
169
- let status = make_pip_command ( & interpreter. executable , pip_path. as_deref ( ) )
170
- . args ( & args)
171
- . status ( )
172
- . context ( "Failed to run pip install" ) ?;
173
- if !status. success ( ) {
174
- bail ! ( r#"pip install finished with "{}""# , status)
175
- }
176
- }
281
+ install_dependencies ( & build_context, & extras, & interpreter, pip_path. as_deref ( ) ) ?;
177
282
178
283
let wheels = build_context. build_wheels ( ) ?;
179
284
if !skip_install {
180
285
for ( filename, _supported_version) in wheels. iter ( ) {
181
- let mut pip_cmd = make_pip_command ( & python, pip_path. as_deref ( ) ) ;
182
- let output = pip_cmd
183
- . args ( [ "install" , "--no-deps" , "--force-reinstall" ] )
184
- . arg ( dunce:: simplified ( filename) )
185
- . output ( )
186
- . context ( format ! (
187
- "pip install failed (ran {:?} with {:?})" ,
188
- pip_cmd. get_program( ) ,
189
- & pip_cmd. get_args( ) . collect:: <Vec <_>>( ) ,
190
- ) ) ?;
191
- if !output. status . success ( ) {
192
- bail ! (
193
- "pip install in {} failed running {:?}: {}\n --- Stdout:\n {}\n --- Stderr:\n {}\n ---\n " ,
194
- venv_dir. display( ) ,
195
- & pip_cmd. get_args( ) . collect:: <Vec <_>>( ) ,
196
- output. status,
197
- String :: from_utf8_lossy( & output. stdout) . trim( ) ,
198
- String :: from_utf8_lossy( & output. stderr) . trim( ) ,
199
- ) ;
200
- }
201
- if !output. stderr . is_empty ( ) {
202
- eprintln ! (
203
- "⚠️ Warning: pip raised a warning running {:?}:\n {}" ,
204
- & pip_cmd. get_args( ) . collect:: <Vec <_>>( ) ,
205
- String :: from_utf8_lossy( & output. stderr) . trim( ) ,
206
- ) ;
207
- }
286
+ pip_install_wheel (
287
+ & build_context,
288
+ & python,
289
+ venv_dir,
290
+ pip_path. as_deref ( ) ,
291
+ filename,
292
+ ) ?;
208
293
eprintln ! (
209
294
"🛠 Installed {}-{}" ,
210
295
build_context. metadata21. name, build_context. metadata21. version
@@ -214,3 +299,79 @@ pub fn develop(develop_options: DevelopOptions, venv_dir: &Path) -> Result<()> {
214
299
215
300
Ok ( ( ) )
216
301
}
302
+
303
+ #[ cfg( test) ]
304
+ mod test {
305
+ use std:: path:: PathBuf ;
306
+
307
+ use super :: parse_direct_url_path;
308
+
309
+ #[ test]
310
+ #[ cfg( not( target_os = "windows" ) ) ]
311
+ fn test_parse_direct_url ( ) {
312
+ let example_with_direct_url = "\
313
+ Name: my-project
314
+ Version: 0.1.0
315
+ Location: /foo bar/venv/lib/pythonABC/site-packages
316
+ Editable project location: /tmp/temporary.whl
317
+ Files:
318
+ my_project-0.1.0+abc123de.dist-info/INSTALLER
319
+ my_project-0.1.0+abc123de.dist-info/METADATA
320
+ my_project-0.1.0+abc123de.dist-info/RECORD
321
+ my_project-0.1.0+abc123de.dist-info/REQUESTED
322
+ my_project-0.1.0+abc123de.dist-info/WHEEL
323
+ my_project-0.1.0+abc123de.dist-info/direct_url.json
324
+ my_project-0.1.0+abc123de.dist-info/entry_points.txt
325
+ my_project.pth
326
+ " ;
327
+ let expected_path = PathBuf :: from ( "/foo bar/venv/lib/pythonABC/site-packages/my_project-0.1.0+abc123de.dist-info/direct_url.json" ) ;
328
+ assert_eq ! (
329
+ parse_direct_url_path( example_with_direct_url) . unwrap( ) ,
330
+ Some ( expected_path)
331
+ ) ;
332
+
333
+ let example_without_direct_url = "\
334
+ Name: my-project
335
+ Version: 0.1.0
336
+ Location: /foo bar/venv/lib/pythonABC/site-packages
337
+ Files:
338
+ my_project-0.1.0+abc123de.dist-info/INSTALLER
339
+ my_project-0.1.0+abc123de.dist-info/METADATA
340
+ my_project-0.1.0+abc123de.dist-info/RECORD
341
+ my_project-0.1.0+abc123de.dist-info/REQUESTED
342
+ my_project-0.1.0+abc123de.dist-info/WHEEL
343
+ my_project-0.1.0+abc123de.dist-info/entry_points.txt
344
+ my_project.pth
345
+ " ;
346
+
347
+ assert_eq ! (
348
+ parse_direct_url_path( example_without_direct_url) . unwrap( ) ,
349
+ None
350
+ ) ;
351
+ }
352
+
353
+ #[ test]
354
+ #[ cfg( target_os = "windows" ) ]
355
+ fn test_parse_direct_url_windows ( ) {
356
+ let example_with_direct_url_windows = "\
357
+ Name: my-project\r
358
+ Version: 0.1.0\r
359
+ Location: C:\\ foo bar\\ venv\\ Lib\\ site-packages\r
360
+ Files:\r
361
+ my_project-0.1.0+abc123de.dist-info\\ INSTALLER\r
362
+ my_project-0.1.0+abc123de.dist-info\\ METADATA\r
363
+ my_project-0.1.0+abc123de.dist-info\\ RECORD\r
364
+ my_project-0.1.0+abc123de.dist-info\\ REQUESTED\r
365
+ my_project-0.1.0+abc123de.dist-info\\ WHEEL\r
366
+ my_project-0.1.0+abc123de.dist-info\\ direct_url.json\r
367
+ my_project-0.1.0+abc123de.dist-info\\ entry_points.txt\r
368
+ my_project.pth\r
369
+ " ;
370
+
371
+ let expected_path = PathBuf :: from ( "C:\\ foo bar\\ venv\\ Lib\\ site-packages\\ my_project-0.1.0+abc123de.dist-info\\ direct_url.json" ) ;
372
+ assert_eq ! (
373
+ parse_direct_url_path( example_with_direct_url_windows) . unwrap( ) ,
374
+ Some ( expected_path)
375
+ ) ;
376
+ }
377
+ }
0 commit comments