@@ -4,7 +4,7 @@ use std::fs;
4
4
use std:: io:: Write as _;
5
5
#[ cfg( not( windows) ) ]
6
6
use std:: os:: unix:: fs:: PermissionsExt as _;
7
- use std:: path:: Path ;
7
+ use std:: path:: { Path , PathBuf } ;
8
8
9
9
use anyhow:: { anyhow, bail, Context as _, Result } ;
10
10
use clap:: { Arg , ArgAction , ArgMatches , Command } ;
@@ -13,6 +13,7 @@ use itertools::Itertools as _;
13
13
use log:: { debug, info, warn} ;
14
14
use sha1_smol:: Digest ;
15
15
use symbolic:: common:: ByteView ;
16
+ use walkdir:: WalkDir ;
16
17
use zip:: write:: SimpleFileOptions ;
17
18
use zip:: { DateTime , ZipWriter } ;
18
19
@@ -21,7 +22,6 @@ use crate::config::Config;
21
22
use crate :: utils:: args:: ArgExt as _;
22
23
use crate :: utils:: chunks:: { upload_chunks, Chunk , ASSEMBLE_POLL_INTERVAL } ;
23
24
use crate :: utils:: fs:: get_sha1_checksums;
24
- #[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
25
25
use crate :: utils:: fs:: TempDir ;
26
26
use crate :: utils:: fs:: TempFile ;
27
27
#[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
@@ -95,19 +95,14 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
95
95
let byteview = ByteView :: open ( path) ?;
96
96
debug ! ( "Loaded file with {} bytes" , byteview. len( ) ) ;
97
97
98
- #[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
99
- if is_apple_app ( path) {
100
- handle_asset_catalogs ( path) ;
101
- }
102
-
103
98
validate_is_mobile_app ( path, & byteview) ?;
104
99
105
100
let normalized_zip = if path. is_file ( ) {
106
101
debug ! ( "Normalizing file: {}" , path. display( ) ) ;
107
102
handle_file ( path, & byteview) ?
108
103
} else if path. is_dir ( ) {
109
104
debug ! ( "Normalizing directory: {}" , path. display( ) ) ;
110
- normalize_directory ( path) . with_context ( || {
105
+ handle_directory ( path) . with_context ( || {
111
106
format ! (
112
107
"Failed to generate uploadable bundle for directory {}" ,
113
108
path. display( )
@@ -187,9 +182,9 @@ fn handle_file(path: &Path, byteview: &ByteView) -> Result<TempFile> {
187
182
#[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
188
183
if is_zip_file ( byteview) && is_ipa_file ( byteview) ? {
189
184
debug ! ( "Converting IPA file to XCArchive structure" ) ;
190
- let temp_dir = TempDir :: create ( ) ?;
191
- return ipa_to_xcarchive ( path, byteview, & temp_dir )
192
- . and_then ( |path| normalize_directory ( & path) )
185
+ let archive_temp_dir = TempDir :: create ( ) ?;
186
+ return ipa_to_xcarchive ( path, byteview, & archive_temp_dir )
187
+ . and_then ( |path| handle_directory ( & path) )
193
188
. with_context ( || format ! ( "Failed to process IPA file {}" , path. display( ) ) ) ;
194
189
}
195
190
@@ -276,38 +271,45 @@ fn normalize_file(path: &Path, bytes: &[u8]) -> Result<TempFile> {
276
271
Ok ( temp_file)
277
272
}
278
273
274
+ fn sort_entries ( path : & Path ) -> Result < std:: vec:: IntoIter < ( PathBuf , PathBuf ) > > {
275
+ Ok ( WalkDir :: new ( path)
276
+ . follow_links ( true )
277
+ . into_iter ( )
278
+ . filter_map ( Result :: ok)
279
+ . filter ( |entry| entry. path ( ) . is_file ( ) )
280
+ . map ( |entry| {
281
+ let entry_path = entry. into_path ( ) ;
282
+ let relative_path = entry_path. strip_prefix ( path) ?. to_owned ( ) ;
283
+ Ok ( ( entry_path, relative_path) )
284
+ } )
285
+ . collect :: < Result < Vec < _ > > > ( ) ?
286
+ . into_iter ( )
287
+ . sorted_by ( |( _, a) , ( _, b) | a. cmp ( b) ) )
288
+ }
289
+
290
+ fn handle_directory ( path : & Path ) -> Result < TempFile > {
291
+ let temp_dir = TempDir :: create ( ) ?;
292
+ #[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
293
+ if is_apple_app ( path) {
294
+ handle_asset_catalogs ( path, temp_dir. path ( ) ) ;
295
+ }
296
+ normalize_directory ( path, temp_dir. path ( ) )
297
+ }
298
+
279
299
// For XCArchive directories, we'll zip the entire directory
280
- fn normalize_directory ( path : & Path ) -> Result < TempFile > {
300
+ fn normalize_directory ( path : & Path , parsed_assets_path : & Path ) -> Result < TempFile > {
281
301
debug ! ( "Creating normalized zip for directory: {}" , path. display( ) ) ;
282
302
283
303
let temp_file = TempFile :: create ( ) ?;
284
304
let mut zip = ZipWriter :: new ( temp_file. open ( ) ?) ;
285
305
286
306
let mut file_count = 0 ;
307
+ let directory_name = path. file_name ( ) . expect ( "Failed to get basename" ) ;
287
308
288
309
// Collect and sort entries for deterministic ordering
289
310
// This is important to ensure stable sha1 checksums for the zip file as
290
311
// an optimization is used to avoid re-uploading the same chunks if they're already on the server.
291
- let entries = walkdir:: WalkDir :: new ( path)
292
- . follow_links ( true )
293
- . into_iter ( )
294
- . filter_map ( Result :: ok)
295
- . filter ( |entry| entry. path ( ) . is_file ( ) )
296
- . map ( |entry| {
297
- let entry_path = entry. into_path ( ) ;
298
- let relative_path = entry_path
299
- . strip_prefix ( path. parent ( ) . ok_or_else ( || {
300
- anyhow ! (
301
- "Cannot determine parent directory for path: {}" ,
302
- path. display( )
303
- )
304
- } ) ?) ?
305
- . to_owned ( ) ;
306
- Ok ( ( entry_path, relative_path) )
307
- } )
308
- . collect :: < Result < Vec < _ > > > ( ) ?
309
- . into_iter ( )
310
- . sorted_by ( |( _, a) , ( _, b) | a. cmp ( b) ) ;
312
+ let entries = sort_entries ( path) ?;
311
313
312
314
// Need to set the last modified time to a fixed value to ensure consistent checksums
313
315
// This is important as an optimization to avoid re-uploading the same chunks if they're already on the server
@@ -317,18 +319,47 @@ fn normalize_directory(path: &Path) -> Result<TempFile> {
317
319
. last_modified_time ( DateTime :: default ( ) ) ;
318
320
319
321
for ( entry_path, relative_path) in entries {
320
- debug ! ( "Adding file to zip: {}" , relative_path. display( ) ) ;
322
+ let zip_path = format ! (
323
+ "{}/{}" ,
324
+ directory_name. to_string_lossy( ) ,
325
+ relative_path. to_string_lossy( )
326
+ ) ;
327
+ debug ! ( "Adding file to zip: {}" , zip_path) ;
321
328
322
329
#[ cfg( not( windows) ) ]
323
330
// On Unix, we need to preserve the file permissions.
324
331
let options = options. unix_permissions ( fs:: metadata ( & entry_path) ?. permissions ( ) . mode ( ) ) ;
325
332
326
- zip. start_file ( relative_path . to_string_lossy ( ) , options) ?;
333
+ zip. start_file ( zip_path , options) ?;
327
334
let file_byteview = ByteView :: open ( & entry_path) ?;
328
335
zip. write_all ( file_byteview. as_slice ( ) ) ?;
329
336
file_count += 1 ;
330
337
}
331
338
339
+ // Add parsed assets to the zip in a "ParsedAssets" directory
340
+ if parsed_assets_path. exists ( ) {
341
+ debug ! (
342
+ "Adding parsed assets from: {}" ,
343
+ parsed_assets_path. display( )
344
+ ) ;
345
+
346
+ let parsed_assets_entries = sort_entries ( parsed_assets_path) ?;
347
+
348
+ for ( entry_path, relative_path) in parsed_assets_entries {
349
+ let zip_path = format ! (
350
+ "{}/ParsedAssets/{}" ,
351
+ directory_name. to_string_lossy( ) ,
352
+ relative_path. to_string_lossy( )
353
+ ) ;
354
+ debug ! ( "Adding parsed asset to zip: {}" , zip_path) ;
355
+
356
+ zip. start_file ( zip_path, options) ?;
357
+ let file_byteview = ByteView :: open ( & entry_path) ?;
358
+ zip. write_all ( file_byteview. as_slice ( ) ) ?;
359
+ file_count += 1 ;
360
+ }
361
+ }
362
+
332
363
zip. finish ( ) ?;
333
364
debug ! (
334
365
"Successfully created normalized zip for directory with {} files" ,
@@ -470,12 +501,66 @@ mod tests {
470
501
fs:: create_dir_all ( test_dir. join ( "Products" ) ) ?;
471
502
fs:: write ( test_dir. join ( "Products" ) . join ( "app.txt" ) , "test content" ) ?;
472
503
473
- let result_zip = normalize_directory ( & test_dir) ?;
504
+ let result_zip = normalize_directory ( & test_dir, temp_dir . path ( ) ) ?;
474
505
let zip_file = fs:: File :: open ( result_zip. path ( ) ) ?;
475
506
let mut archive = ZipArchive :: new ( zip_file) ?;
476
507
let file = archive. by_index ( 0 ) ?;
477
508
let file_path = file. name ( ) ;
478
509
assert_eq ! ( file_path, "MyApp.xcarchive/Products/app.txt" ) ;
479
510
Ok ( ( ) )
480
511
}
512
+
513
+ #[ test]
514
+ #[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
515
+ fn test_xcarchive_upload_includes_parsed_assets ( ) -> Result < ( ) > {
516
+ // Test that XCArchive uploads include parsed asset catalogs
517
+ let xcarchive_path = Path :: new ( "tests/integration/_fixtures/mobile_app/archive.xcarchive" ) ;
518
+
519
+ // Process the XCArchive directory
520
+ let result = handle_directory ( xcarchive_path) ?;
521
+
522
+ // Verify the resulting zip contains parsed assets
523
+ let zip_file = fs:: File :: open ( result. path ( ) ) ?;
524
+ let mut archive = ZipArchive :: new ( zip_file) ?;
525
+
526
+ let mut has_parsed_assets = false ;
527
+ for i in 0 ..archive. len ( ) {
528
+ let file = archive. by_index ( i) ?;
529
+ let file_name = file. enclosed_name ( ) . ok_or ( anyhow ! ( "Failed to get file name" ) ) ?;
530
+ if file_name. to_string_lossy ( ) . contains ( "ParsedAssets" ) {
531
+ has_parsed_assets = true ;
532
+ break ;
533
+ }
534
+ }
535
+
536
+ assert ! ( has_parsed_assets, "XCArchive upload should include parsed asset catalogs" ) ;
537
+ Ok ( ( ) )
538
+ }
539
+
540
+ #[ test]
541
+ #[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
542
+ fn test_ipa_upload_includes_parsed_assets ( ) -> Result < ( ) > {
543
+ // Test that IPA uploads handle missing asset catalogs gracefully
544
+ let ipa_path = Path :: new ( "tests/integration/_fixtures/mobile_app/ipa_with_asset.ipa" ) ;
545
+ let byteview = ByteView :: open ( ipa_path) ?;
546
+
547
+ // Process the IPA file - this should work even without asset catalogs
548
+ let result = handle_file ( ipa_path, & byteview) ?;
549
+
550
+ let zip_file = fs:: File :: open ( result. path ( ) ) ?;
551
+ let mut archive = ZipArchive :: new ( zip_file) ?;
552
+
553
+ let mut has_parsed_assets = false ;
554
+ for i in 0 ..archive. len ( ) {
555
+ let file = archive. by_index ( i) ?;
556
+ let file_name = file. enclosed_name ( ) . ok_or ( anyhow ! ( "Failed to get file name" ) ) ?;
557
+ if file_name. to_string_lossy ( ) . contains ( "ParsedAssets" ) {
558
+ has_parsed_assets = true ;
559
+ break ;
560
+ }
561
+ }
562
+
563
+ assert ! ( has_parsed_assets, "XCArchive upload should include parsed asset catalogs" ) ;
564
+ Ok ( ( ) )
565
+ }
481
566
}
0 commit comments