11use std:: borrow:: Cow ;
2- #[ cfg( not( windows) ) ]
3- use std:: fs;
42use std:: io:: Write as _;
5- #[ cfg( not( windows) ) ]
6- use std:: os:: unix:: fs:: PermissionsExt as _;
73use std:: path:: Path ;
84
95use anyhow:: { anyhow, bail, Context as _, Result } ;
106use clap:: { Arg , ArgAction , ArgMatches , Command } ;
117use indicatif:: ProgressStyle ;
12- use itertools:: Itertools as _;
138use log:: { debug, info, warn} ;
149use symbolic:: common:: ByteView ;
1510use zip:: write:: SimpleFileOptions ;
@@ -20,14 +15,13 @@ use crate::config::Config;
2015use crate :: utils:: args:: ArgExt as _;
2116use crate :: utils:: chunks:: { upload_chunks, Chunk } ;
2217use crate :: utils:: fs:: get_sha1_checksums;
23- #[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
2418use crate :: utils:: fs:: TempDir ;
2519use crate :: utils:: fs:: TempFile ;
2620#[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
2721use crate :: utils:: mobile_app:: {
2822 handle_asset_catalogs, ipa_to_xcarchive, is_apple_app, is_ipa_file,
2923} ;
30- use crate :: utils:: mobile_app:: { is_aab_file, is_apk_file, is_zip_file} ;
24+ use crate :: utils:: mobile_app:: { is_aab_file, is_apk_file, is_zip_file, normalize_directory } ;
3125use crate :: utils:: progress:: ProgressBar ;
3226use crate :: utils:: vcs;
3327
@@ -94,19 +88,14 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
9488 let byteview = ByteView :: open ( path) ?;
9589 debug ! ( "Loaded file with {} bytes" , byteview. len( ) ) ;
9690
97- #[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
98- if is_apple_app ( path) {
99- handle_asset_catalogs ( path) ;
100- }
101-
10291 validate_is_mobile_app ( path, & byteview) ?;
10392
10493 let normalized_zip = if path. is_file ( ) {
10594 debug ! ( "Normalizing file: {}" , path. display( ) ) ;
10695 handle_file ( path, & byteview) ?
10796 } else if path. is_dir ( ) {
10897 debug ! ( "Normalizing directory: {}" , path. display( ) ) ;
109- normalize_directory ( path) . with_context ( || {
98+ handle_directory ( path) . with_context ( || {
11099 format ! (
111100 "Failed to generate uploadable bundle for directory {}" ,
112101 path. display( )
@@ -196,9 +185,9 @@ fn handle_file(path: &Path, byteview: &ByteView) -> Result<TempFile> {
196185 #[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
197186 if is_zip_file ( byteview) && is_ipa_file ( byteview) ? {
198187 debug ! ( "Converting IPA file to XCArchive structure" ) ;
199- let temp_dir = TempDir :: create ( ) ?;
200- return ipa_to_xcarchive ( path, byteview, & temp_dir )
201- . and_then ( |path| normalize_directory ( & path) )
188+ let archive_temp_dir = TempDir :: create ( ) ?;
189+ return ipa_to_xcarchive ( path, byteview, & archive_temp_dir )
190+ . and_then ( |path| handle_directory ( & path) )
202191 . with_context ( || format ! ( "Failed to process IPA file {}" , path. display( ) ) ) ;
203192 }
204193
@@ -285,65 +274,13 @@ fn normalize_file(path: &Path, bytes: &[u8]) -> Result<TempFile> {
285274 Ok ( temp_file)
286275}
287276
288- // For XCArchive directories, we'll zip the entire directory
289- fn normalize_directory ( path : & Path ) -> Result < TempFile > {
290- debug ! ( "Creating normalized zip for directory: {}" , path. display( ) ) ;
291-
292- let temp_file = TempFile :: create ( ) ?;
293- let mut zip = ZipWriter :: new ( temp_file. open ( ) ?) ;
294-
295- let mut file_count = 0 ;
296-
297- // Collect and sort entries for deterministic ordering
298- // This is important to ensure stable sha1 checksums for the zip file as
299- // an optimization is used to avoid re-uploading the same chunks if they're already on the server.
300- let entries = walkdir:: WalkDir :: new ( path)
301- . follow_links ( true )
302- . into_iter ( )
303- . filter_map ( Result :: ok)
304- . filter ( |entry| entry. path ( ) . is_file ( ) )
305- . map ( |entry| {
306- let entry_path = entry. into_path ( ) ;
307- let relative_path = entry_path
308- . strip_prefix ( path. parent ( ) . ok_or_else ( || {
309- anyhow ! (
310- "Cannot determine parent directory for path: {}" ,
311- path. display( )
312- )
313- } ) ?) ?
314- . to_owned ( ) ;
315- Ok ( ( entry_path, relative_path) )
316- } )
317- . collect :: < Result < Vec < _ > > > ( ) ?
318- . into_iter ( )
319- . sorted_by ( |( _, a) , ( _, b) | a. cmp ( b) ) ;
320-
321- // Need to set the last modified time to a fixed value to ensure consistent checksums
322- // This is important as an optimization to avoid re-uploading the same chunks if they're already on the server
323- // but the last modified time being different will cause checksums to be different.
324- let options = SimpleFileOptions :: default ( )
325- . compression_method ( zip:: CompressionMethod :: Stored )
326- . last_modified_time ( DateTime :: default ( ) ) ;
327-
328- for ( entry_path, relative_path) in entries {
329- debug ! ( "Adding file to zip: {}" , relative_path. display( ) ) ;
330-
331- #[ cfg( not( windows) ) ]
332- // On Unix, we need to preserve the file permissions.
333- let options = options. unix_permissions ( fs:: metadata ( & entry_path) ?. permissions ( ) . mode ( ) ) ;
334-
335- zip. start_file ( relative_path. to_string_lossy ( ) , options) ?;
336- let file_byteview = ByteView :: open ( & entry_path) ?;
337- zip. write_all ( file_byteview. as_slice ( ) ) ?;
338- file_count += 1 ;
277+ fn handle_directory ( path : & Path ) -> Result < TempFile > {
278+ let temp_dir = TempDir :: create ( ) ?;
279+ #[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
280+ if is_apple_app ( path) {
281+ handle_asset_catalogs ( path, temp_dir. path ( ) ) ;
339282 }
340-
341- zip. finish ( ) ?;
342- debug ! (
343- "Successfully created normalized zip for directory with {} files" ,
344- file_count
345- ) ;
346- Ok ( temp_file)
283+ normalize_directory ( path, temp_dir. path ( ) )
347284}
348285
349286/// Returns artifact id if upload was successful.
@@ -456,12 +393,76 @@ mod tests {
456393 fs:: create_dir_all ( test_dir. join ( "Products" ) ) ?;
457394 fs:: write ( test_dir. join ( "Products" ) . join ( "app.txt" ) , "test content" ) ?;
458395
459- let result_zip = normalize_directory ( & test_dir) ?;
396+ let result_zip = normalize_directory ( & test_dir, temp_dir . path ( ) ) ?;
460397 let zip_file = fs:: File :: open ( result_zip. path ( ) ) ?;
461398 let mut archive = ZipArchive :: new ( zip_file) ?;
462399 let file = archive. by_index ( 0 ) ?;
463400 let file_path = file. name ( ) ;
464401 assert_eq ! ( file_path, "MyApp.xcarchive/Products/app.txt" ) ;
465402 Ok ( ( ) )
466403 }
404+
405+ #[ test]
406+ #[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
407+ fn test_xcarchive_upload_includes_parsed_assets ( ) -> Result < ( ) > {
408+ // Test that XCArchive uploads include parsed asset catalogs
409+ let xcarchive_path = Path :: new ( "tests/integration/_fixtures/mobile_app/archive.xcarchive" ) ;
410+
411+ // Process the XCArchive directory
412+ let result = handle_directory ( xcarchive_path) ?;
413+
414+ // Verify the resulting zip contains parsed assets
415+ let zip_file = fs:: File :: open ( result. path ( ) ) ?;
416+ let mut archive = ZipArchive :: new ( zip_file) ?;
417+
418+ let mut has_parsed_assets = false ;
419+ for i in 0 ..archive. len ( ) {
420+ let file = archive. by_index ( i) ?;
421+ let file_name = file
422+ . enclosed_name ( )
423+ . ok_or ( anyhow ! ( "Failed to get file name" ) ) ?;
424+ if file_name. to_string_lossy ( ) . contains ( "ParsedAssets" ) {
425+ has_parsed_assets = true ;
426+ break ;
427+ }
428+ }
429+
430+ assert ! (
431+ has_parsed_assets,
432+ "XCArchive upload should include parsed asset catalogs"
433+ ) ;
434+ Ok ( ( ) )
435+ }
436+
437+ #[ test]
438+ #[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
439+ fn test_ipa_upload_includes_parsed_assets ( ) -> Result < ( ) > {
440+ // Test that IPA uploads handle missing asset catalogs gracefully
441+ let ipa_path = Path :: new ( "tests/integration/_fixtures/mobile_app/ipa_with_asset.ipa" ) ;
442+ let byteview = ByteView :: open ( ipa_path) ?;
443+
444+ // Process the IPA file - this should work even without asset catalogs
445+ let result = handle_file ( ipa_path, & byteview) ?;
446+
447+ let zip_file = fs:: File :: open ( result. path ( ) ) ?;
448+ let mut archive = ZipArchive :: new ( zip_file) ?;
449+
450+ let mut has_parsed_assets = false ;
451+ for i in 0 ..archive. len ( ) {
452+ let file = archive. by_index ( i) ?;
453+ let file_name = file
454+ . enclosed_name ( )
455+ . ok_or ( anyhow ! ( "Failed to get file name" ) ) ?;
456+ if file_name. to_string_lossy ( ) . contains ( "ParsedAssets" ) {
457+ has_parsed_assets = true ;
458+ break ;
459+ }
460+ }
461+
462+ assert ! (
463+ has_parsed_assets,
464+ "XCArchive upload should include parsed asset catalogs"
465+ ) ;
466+ Ok ( ( ) )
467+ }
467468}
0 commit comments