2
2
//! files
3
3
#![ deny( missing_docs) ]
4
4
5
- use anyhow:: Result ;
5
+ use anyhow:: { Context , Result } ;
6
6
use bytes:: buf:: Buf ;
7
7
use fs_err:: { self as fs} ;
8
- use futures:: future :: try_join_all ;
8
+ use futures:: { stream :: FuturesUnordered , StreamExt } ;
9
9
use fxhash:: FxHashMap ;
10
10
use indicatif:: { MultiProgress , ProgressBar , ProgressStyle } ;
11
11
use rattler_conda_types:: {
12
12
package:: { ArchiveType , IndexJson , PackageFile } ,
13
- ChannelInfo , PackageRecord , Platform , RepoData ,
13
+ ChannelInfo , PackageRecord , PatchInstructions , Platform , RepoData ,
14
14
} ;
15
15
use rattler_networking:: { Authentication , AuthenticationStorage } ;
16
- use rattler_package_streaming:: { read, seek} ;
16
+ use rattler_package_streaming:: {
17
+ read,
18
+ seek:: { self , stream_conda_content} ,
19
+ } ;
17
20
use std:: {
18
21
collections:: { HashMap , HashSet } ,
19
- io:: { Cursor , Read } ,
22
+ io:: { Cursor , Read , Seek } ,
20
23
path:: { Path , PathBuf } ,
21
24
str:: FromStr ,
22
25
sync:: Arc ,
@@ -71,6 +74,46 @@ pub fn package_record_from_index_json<T: Read>(
71
74
Ok ( package_record)
72
75
}
73
76
77
+ fn repodata_patch_from_conda_package_stream < ' a > (
78
+ package : impl Read + Seek + ' a ,
79
+ ) -> anyhow:: Result < rattler_conda_types:: RepoDataPatch > {
80
+ let mut subdirs = FxHashMap :: default ( ) ;
81
+
82
+ let mut content_reader = stream_conda_content ( package) ?;
83
+ let entries = content_reader. entries ( ) ?;
84
+ for entry in entries {
85
+ let mut entry = entry?;
86
+ if !entry. header ( ) . entry_type ( ) . is_file ( ) {
87
+ return Err ( anyhow:: anyhow!(
88
+ "Expected repodata patch package to be a file"
89
+ ) ) ;
90
+ }
91
+ let mut buf = Vec :: new ( ) ;
92
+ entry. read_to_end ( & mut buf) ?;
93
+ let path = entry. path ( ) ?;
94
+ let components = path. components ( ) . collect :: < Vec < _ > > ( ) ;
95
+ let subdir =
96
+ if components. len ( ) == 2 && components[ 1 ] . as_os_str ( ) == "patch_instructions.json" {
97
+ let subdir_str = components[ 0 ]
98
+ . as_os_str ( )
99
+ . to_str ( )
100
+ . context ( "Could not convert OsStr to str" ) ?;
101
+ let _ = Platform :: from_str ( subdir_str) ?;
102
+ subdir_str. to_string ( )
103
+ } else {
104
+ return Err ( anyhow:: anyhow!(
105
+ "Expected files of form <subdir>/patch_instructions.json, but found {}" ,
106
+ path. display( )
107
+ ) ) ;
108
+ } ;
109
+
110
+ let instructions: PatchInstructions = serde_json:: from_slice ( & buf) ?;
111
+ subdirs. insert ( subdir, instructions) ;
112
+ }
113
+
114
+ Ok ( rattler_conda_types:: RepoDataPatch { subdirs } )
115
+ }
116
+
74
117
/// Extract the package record from a `.tar.bz2` package file.
75
118
/// This function will look for the `info/index.json` file in the conda package
76
119
/// and extract the package record from it.
@@ -132,12 +175,17 @@ async fn index_subdir(
132
175
subdir : Platform ,
133
176
op : Operator ,
134
177
force : bool ,
178
+ repodata_patch : Option < PatchInstructions > ,
135
179
progress : Option < MultiProgress > ,
136
180
semaphore : Arc < Semaphore > ,
137
181
) -> Result < ( ) > {
182
+ let repodata_path = if repodata_patch. is_some ( ) {
183
+ format ! ( "{subdir}/repodata_from_packages.json" )
184
+ } else {
185
+ format ! ( "{subdir}/repodata.json" )
186
+ } ;
138
187
let mut registered_packages: FxHashMap < String , PackageRecord > = HashMap :: default ( ) ;
139
188
if !force {
140
- let repodata_path = format ! ( "{subdir}/repodata.json" ) ;
141
189
let repodata_bytes = op. read ( & repodata_path) . await ;
142
190
let repodata: RepoData = match repodata_bytes {
143
191
Ok ( bytes) => serde_json:: from_slice ( & bytes. to_vec ( ) ) ?,
@@ -210,7 +258,7 @@ async fn index_subdir(
210
258
. cloned ( )
211
259
. collect :: < Vec < _ > > ( ) ;
212
260
213
- tracing:: debug !(
261
+ tracing:: info !(
214
262
"Adding {} packages to subdir {}." ,
215
263
packages_to_add. len( ) ,
216
264
subdir
@@ -229,53 +277,79 @@ async fn index_subdir(
229
277
. progress_chars ( "##-" ) ;
230
278
pb. set_style ( sty) ;
231
279
232
- let tasks = packages_to_add
233
- . iter ( )
234
- . map ( |filename| {
235
- tokio:: spawn ( {
236
- let op = op. clone ( ) ;
237
- let filename = filename. clone ( ) ;
238
- let pb = pb. clone ( ) ;
239
- let semaphore = semaphore. clone ( ) ;
240
- {
241
- async move {
242
- let _permit = semaphore
243
- . acquire ( )
244
- . await
245
- . expect ( "Semaphore was unexpectedly closed" ) ;
246
- pb. set_message ( format ! (
247
- "Indexing {} {}" ,
248
- subdir. as_str( ) ,
249
- console:: style( filename. clone( ) ) . dim( )
250
- ) ) ;
251
- let file_path = format ! ( "{subdir}/{filename}" ) ;
252
- let buffer = op. read ( & file_path) . await ?;
253
- let reader = buffer. reader ( ) ;
254
- // We already know it's not None
255
- let archive_type = ArchiveType :: try_from ( & filename) . unwrap ( ) ;
256
- let record = match archive_type {
257
- ArchiveType :: TarBz2 => package_record_from_tar_bz2_reader ( reader) ,
258
- ArchiveType :: Conda => package_record_from_conda_reader ( reader) ,
259
- } ?;
260
- pb. inc ( 1 ) ;
261
- Ok :: < ( String , PackageRecord ) , std:: io:: Error > ( ( filename. clone ( ) , record) )
262
- }
280
+ let mut tasks = FuturesUnordered :: new ( ) ;
281
+ for filename in packages_to_add. iter ( ) {
282
+ let task = {
283
+ let op = op. clone ( ) ;
284
+ let filename = filename. clone ( ) ;
285
+ let pb = pb. clone ( ) ;
286
+ let semaphore = semaphore. clone ( ) ;
287
+ {
288
+ async move {
289
+ let _permit = semaphore
290
+ . acquire ( )
291
+ . await
292
+ . expect ( "Semaphore was unexpectedly closed" ) ;
293
+ pb. set_message ( format ! (
294
+ "Indexing {} {}" ,
295
+ subdir. as_str( ) ,
296
+ console:: style( filename. clone( ) ) . dim( )
297
+ ) ) ;
298
+ let file_path = format ! ( "{subdir}/{filename}" ) ;
299
+ let buffer = op. read ( & file_path) . await ?;
300
+ let reader = buffer. reader ( ) ;
301
+ // We already know it's not None
302
+ let archive_type = ArchiveType :: try_from ( & filename) . unwrap ( ) ;
303
+ let record = match archive_type {
304
+ ArchiveType :: TarBz2 => package_record_from_tar_bz2_reader ( reader) ,
305
+ ArchiveType :: Conda => package_record_from_conda_reader ( reader) ,
306
+ } ?;
307
+ pb. inc ( 1 ) ;
308
+ Ok :: < ( String , PackageRecord ) , std:: io:: Error > ( ( filename. clone ( ) , record) )
263
309
}
264
- } )
265
- } )
266
- . collect :: < Vec < _ > > ( ) ;
267
- let results = try_join_all ( tasks) . await ?;
268
-
269
- pb. finish_with_message ( format ! ( "Finished {}" , subdir. as_str( ) ) ) ;
310
+ }
311
+ } ;
312
+ tasks. push ( tokio:: spawn ( task) ) ;
313
+ }
314
+ let mut results = Vec :: new ( ) ;
315
+ while let Some ( join_result) = tasks. next ( ) . await {
316
+ match join_result {
317
+ Ok ( Ok ( result) ) => results. push ( result) ,
318
+ Ok ( Err ( e) ) => {
319
+ tasks. clear ( ) ;
320
+ tracing:: error!( "Failed to process package: {}" , e) ;
321
+ pb. abandon_with_message ( format ! (
322
+ "{} {}" ,
323
+ console:: style( "Failed to index" ) . red( ) ,
324
+ console:: style( subdir. as_str( ) ) . dim( )
325
+ ) ) ;
326
+ return Err ( e. into ( ) ) ;
327
+ }
328
+ Err ( join_err) => {
329
+ tasks. clear ( ) ;
330
+ tracing:: error!( "Task panicked: {}" , join_err) ;
331
+ pb. abandon_with_message ( format ! (
332
+ "{} {}" ,
333
+ console:: style( "Failed to index" ) . red( ) ,
334
+ console:: style( subdir. as_str( ) ) . dim( )
335
+ ) ) ;
336
+ return Err ( anyhow:: anyhow!( "Task panicked: {}" , join_err) ) ;
337
+ }
338
+ }
339
+ }
340
+ pb. finish_with_message ( format ! (
341
+ "{} {}" ,
342
+ console:: style( "Finished" ) . green( ) ,
343
+ subdir. as_str( )
344
+ ) ) ;
270
345
271
- tracing:: debug !(
346
+ tracing:: info !(
272
347
"Successfully added {} packages to subdir {}." ,
273
348
results. len( ) ,
274
349
subdir
275
350
) ;
276
351
277
- for result in results {
278
- let ( filename, record) = result?;
352
+ for ( filename, record) in results {
279
353
registered_packages. insert ( filename, record) ;
280
354
}
281
355
@@ -304,26 +378,46 @@ async fn index_subdir(
304
378
version : Some ( 2 ) ,
305
379
} ;
306
380
307
- let repodata_path = format ! ( "{subdir}/repodata.json" ) ;
381
+ tracing :: info !( "Writing repodata to {}" , repodata_path ) ;
308
382
let repodata_bytes = serde_json:: to_vec ( & repodata) ?;
309
383
op. write ( & repodata_path, repodata_bytes) . await ?;
384
+
385
+ if let Some ( instructions) = repodata_patch {
386
+ let patched_repodata_path = format ! ( "{subdir}/repodata.json" ) ;
387
+ tracing:: info!( "Writing patched repodata to {}" , patched_repodata_path) ;
388
+ let mut patched_repodata = repodata. clone ( ) ;
389
+ patched_repodata. apply_patches ( & instructions) ;
390
+ let patched_repodata_bytes = serde_json:: to_vec ( & patched_repodata) ?;
391
+ op. write ( & patched_repodata_path, patched_repodata_bytes)
392
+ . await ?;
393
+ }
310
394
// todo: also write repodata.json.bz2, repodata.json.zst, repodata.json.jlap and sharded repodata once available in rattler
311
395
// https://github.com/conda/rattler/issues/1096
312
396
313
397
Ok ( ( ) )
314
398
}
315
399
316
400
/// Create a new `repodata.json` for all packages in the channel at the given directory.
401
+ #[ allow( clippy:: too_many_arguments) ]
317
402
pub async fn index_fs (
318
403
channel : impl Into < PathBuf > ,
319
404
target_platform : Option < Platform > ,
405
+ repodata_patch : Option < String > ,
320
406
force : bool ,
321
407
max_parallel : usize ,
322
408
multi_progress : Option < MultiProgress > ,
323
409
) -> anyhow:: Result < ( ) > {
324
410
let mut config = FsConfig :: default ( ) ;
325
411
config. root = Some ( channel. into ( ) . canonicalize ( ) ?. to_string_lossy ( ) . to_string ( ) ) ;
326
- index ( target_platform, config, force, max_parallel, multi_progress) . await
412
+ index (
413
+ target_platform,
414
+ config,
415
+ repodata_patch,
416
+ force,
417
+ max_parallel,
418
+ multi_progress,
419
+ )
420
+ . await
327
421
}
328
422
329
423
/// Create a new `repodata.json` for all packages in the channel at the given S3 URL.
@@ -337,6 +431,7 @@ pub async fn index_s3(
337
431
secret_access_key : Option < String > ,
338
432
session_token : Option < String > ,
339
433
target_platform : Option < Platform > ,
434
+ repodata_patch : Option < String > ,
340
435
force : bool ,
341
436
max_parallel : usize ,
342
437
multi_progress : Option < MultiProgress > ,
@@ -376,6 +471,7 @@ pub async fn index_s3(
376
471
index (
377
472
target_platform,
378
473
s3_config,
474
+ repodata_patch,
379
475
force,
380
476
max_parallel,
381
477
multi_progress,
@@ -398,6 +494,7 @@ pub async fn index_s3(
398
494
pub async fn index < T : Configurator > (
399
495
target_platform : Option < Platform > ,
400
496
config : T ,
497
+ repodata_patch : Option < String > ,
401
498
force : bool ,
402
499
max_parallel : usize ,
403
500
multi_progress : Option < MultiProgress > ,
@@ -443,22 +540,57 @@ pub async fn index<T: Configurator>(
443
540
subdirs. insert ( Platform :: NoArch ) ;
444
541
}
445
542
543
+ let repodata_patch = if let Some ( path) = repodata_patch {
544
+ match ArchiveType :: try_from ( path. clone ( ) ) {
545
+ Some ( ArchiveType :: Conda ) => { }
546
+ Some ( ArchiveType :: TarBz2 ) | None => {
547
+ return Err ( anyhow:: anyhow!(
548
+ "Only .conda packages are supported for repodata patches. Got: {}" ,
549
+ path
550
+ ) )
551
+ }
552
+ }
553
+ let repodata_patch_path = format ! ( "noarch/{path}" ) ;
554
+ let repodata_patch_bytes = op. read ( & repodata_patch_path) . await ?. to_bytes ( ) ;
555
+ let reader = Cursor :: new ( repodata_patch_bytes) ;
556
+ let repodata_patch = repodata_patch_from_conda_package_stream ( reader) ?;
557
+ Some ( repodata_patch)
558
+ } else {
559
+ None
560
+ } ;
561
+
446
562
let semaphore = Semaphore :: new ( max_parallel) ;
447
563
let semaphore = Arc :: new ( semaphore) ;
448
564
449
- let tasks = subdirs
450
- . iter ( )
451
- . map ( |subdir| {
452
- tokio:: spawn ( index_subdir (
453
- * subdir,
454
- op. clone ( ) ,
455
- force,
456
- multi_progress. clone ( ) ,
457
- semaphore. clone ( ) ,
458
- ) )
459
- } )
460
- . collect :: < Vec < _ > > ( ) ;
461
- try_join_all ( tasks) . await ?;
565
+ let mut tasks = FuturesUnordered :: new ( ) ;
566
+ for subdir in subdirs. iter ( ) {
567
+ let task = index_subdir (
568
+ * subdir,
569
+ op. clone ( ) ,
570
+ force,
571
+ repodata_patch
572
+ . as_ref ( )
573
+ . and_then ( |p| p. subdirs . get ( & subdir. to_string ( ) ) . cloned ( ) ) ,
574
+ multi_progress. clone ( ) ,
575
+ semaphore. clone ( ) ,
576
+ ) ;
577
+ tasks. push ( tokio:: spawn ( task) ) ;
578
+ }
462
579
580
+ while let Some ( join_result) = tasks. next ( ) . await {
581
+ match join_result {
582
+ Ok ( Ok ( _) ) => { }
583
+ Ok ( Err ( e) ) => {
584
+ tracing:: error!( "Failed to process subdir: {}" , e) ;
585
+ tasks. clear ( ) ;
586
+ return Err ( e) ;
587
+ }
588
+ Err ( join_err) => {
589
+ tracing:: error!( "Task panicked: {}" , join_err) ;
590
+ tasks. clear ( ) ;
591
+ return Err ( anyhow:: anyhow!( "Task panicked: {}" , join_err) ) ;
592
+ }
593
+ }
594
+ }
463
595
Ok ( ( ) )
464
596
}
0 commit comments