2424import time
2525import unicodedata
2626import urls
27+ import xattr
2728
2829CHUNK_SIZE = 256 * 1024
2930
3031ImportFolders = typing .Tuple [
3132 typing .Dict [str , machfs .Folder ], # Universal folders
3233 typing .Dict [str , machfs .Folder ], # Folders that need System 7
34+ typing .Dict [str , machfs .Folder ], # Folders that need Mac OS X
3335]
3436
3537def get_import_folders () -> ImportFolders :
3638 import_folders = {}
3739 import_folders7 = {}
40+ import_foldersX = {}
3841
39- manifest_folders , manifest_folders7 = import_manifests ()
42+ manifest_folders , manifest_folders7 , manifest_foldersX = import_manifests ()
4043 import_folders .update (manifest_folders )
4144 import_folders7 .update (manifest_folders7 )
45+ import_foldersX .update (manifest_foldersX )
4246
4347 zip_folders , zip_folder7 = import_zips ()
4448 import_folders .update (zip_folders )
4549 import_folders7 .update (zip_folder7 )
4650
47- return import_folders , import_folders7
51+ return import_folders , import_folders7 , import_foldersX
4852
4953
5054def import_manifests () -> ImportFolders :
5155 sys .stderr .write ("Importing other images\n " )
5256 import_folders = {}
5357 import_folders7 = {}
58+ import_foldersX = {}
5459 debug_filter = os .getenv ("DEBUG_LIBRARY_FILTER" )
5560
5661 for manifest_path in glob .iglob (os .path .join (paths .LIBRARY_DIR , "**" ,
@@ -76,15 +81,22 @@ def import_manifests() -> ImportFolders:
7681 "(build it with npm run build-xadmaster)\n " )
7782 continue
7883 folder = import_archive (manifest_json )
84+ elif src_ext in [".dmg" ]:
85+ if not os .path .exists (paths .HDIUTIL_PATH ):
86+ sys .stderr .write (" Skipping .dmg import, hdiutil not found\n " )
87+ continue
88+ folder = import_dmg (manifest_json )
7989 else :
8090 assert False , "Unexpected manifest URL extension: %s" % src_ext
8191
82- if manifest_json .get ("needs_system_7" ):
92+ if manifest_json .get ("needs_mac_os_x" ):
93+ import_foldersX [folder_path ] = folder
94+ elif manifest_json .get ("needs_system_7" ):
8395 import_folders7 [folder_path ] = folder
8496 else :
8597 import_folders [folder_path ] = folder
8698
87- return import_folders , import_folders7
99+ return import_folders , import_folders7 , import_foldersX
88100
89101
90102def import_disk_image (
@@ -319,6 +331,96 @@ def convert_date(date_str: str) -> int:
319331 file_or_folder .crdate = convert_date (entry ["XADCreationDate" ])
320332
321333
334+ def import_dmg (
335+ manifest_json : typing .Dict [str , typing .Any ]) -> machfs .Folder :
336+ src_url = manifest_json ["src_url" ]
337+ archive_path = urls .read_url_to_path (src_url )
338+ root_folder = machfs .Folder ()
339+
340+ def normalize (name : str ) -> str :
341+ # Normalizes accented characters to their combined form, since only
342+ # those have an equivalent in the MacRoman encoding that HFS ends up
343+ # using.
344+ return unicodedata .normalize ("NFC" , name )
345+
346+ with tempfile .TemporaryDirectory () as tmp_dir_path :
347+ hdiutil_code = subprocess .call ([
348+ paths .HDIUTIL_PATH , "attach" , archive_path , "-mountpoint" ,
349+ tmp_dir_path
350+ ],
351+ stdout = subprocess .DEVNULL )
352+ if hdiutil_code != 0 :
353+ assert False , "Could not mount .dmg: %s (cached at %s):" % (
354+ src_url , archive_path )
355+ try :
356+ # TODO: allow selection of a child folder
357+ root_dir_path = tmp_dir_path
358+
359+ if "src_folder" in manifest_json :
360+ src_folder_name = manifest_json ["src_folder" ]
361+ root_dir_path = os .path .join (root_dir_path , src_folder_name )
362+ update_folder_from_xattr (root_folder , root_dir_path )
363+ clear_folder_window_position (root_folder )
364+
365+ for dir_path , dir_names , file_names in os .walk (root_dir_path ):
366+ folder = root_folder
367+ dir_rel_path = os .path .relpath (dir_path , root_dir_path )
368+ if dir_rel_path != "." :
369+ folder_path_pieces = []
370+ for folder_name in dir_rel_path .split (os .path .sep ):
371+ folder_path_pieces .append (folder_name )
372+ folder_name = normalize (folder_name )
373+ if folder_name not in folder :
374+ new_folder = folder [folder_name ] = machfs .Folder ()
375+ update_folder_from_xattr (new_folder , os .path .join (root_dir_path ,
376+ * folder_path_pieces ))
377+ folder = folder [folder_name ]
378+ for file_name in file_names :
379+ file_path = os .path .join (dir_path , file_name )
380+ file = machfs .File ()
381+ with open (file_path , "rb" ) as f :
382+ file .data = f .read ()
383+ resource_fork_path = os .path .join (file_path , "..namedfork" ,
384+ "rsrc" )
385+ if os .path .exists (resource_fork_path ):
386+ with open (resource_fork_path , "rb" ) as f :
387+ file .rsrc = f .read ()
388+
389+ update_file_from_xattr (file , file_path )
390+
391+ folder [normalize (file_name )] = file
392+ finally :
393+ hdiutil_code = subprocess .call ([
394+ paths .HDIUTIL_PATH , "detach" , tmp_dir_path ])
395+ if hdiutil_code != 0 :
396+ assert False , "Could not unmount .dmg: %s (cached at %s):" % (
397+ src_url , archive_path )
398+
399+ return root_folder
400+
401+
402+ def update_file_from_xattr (file : machfs .File , file_path : str ) -> None :
403+ attr_name = "com.apple.FinderInfo"
404+ xattrs = xattr .listxattr (file_path )
405+ if attr_name not in xattrs :
406+ return
407+ finder_info = xattr .getxattr (file_path , attr_name )
408+ (file .type , file .creator , file .flags , file .y , file .x , _ , file .fndrInfo ) = struct .unpack (
409+ '>4s4sHhhH16s' , finder_info )
410+ if file .x == 0 and file .y == 0 :
411+ file .flags &= ~ machfs .main .FinderFlags .kHasBeenInited
412+
413+
414+ def update_folder_from_xattr (folder : machfs .Folder , folder_path : str ) -> None :
415+ attr_name = "com.apple.FinderInfo"
416+ xattrs = xattr .listxattr (folder_path )
417+ if attr_name not in xattrs :
418+ return
419+ # Get creator and type code
420+ finder_info = xattr .getxattr (folder_path , attr_name )
421+ folder .usrInfo = finder_info [0 :16 ]
422+ folder .fndrInfo = finder_info [16 :]
423+
322424SYSTEM7_ZIP_PATHS = {
323425 "Games/Bungie/Marathon Infinity" ,
324426 "Graphics/Adobe Photoshop 3.0" ,
@@ -555,8 +657,8 @@ def build_system_image(
555657 return write_image_def (image_data , disk .name , dest_dir )
556658
557659
558- def build_library_images (dest_dir : str ) -> typing .Tuple [ImageDef , ImageDef ]:
559- import_folders , import_folders7 = get_import_folders ()
660+ def build_library_images (dest_dir : str ) -> typing .Tuple [ImageDef , ImageDef , ImageDef ]:
661+ import_folders , import_folders7 , import_foldersX = get_import_folders ()
560662
561663 v = machfs .Volume ()
562664 with open (os .path .join (paths .IMAGES_DIR , "Infinite HD.dsk" ), "rb" ) as base :
@@ -593,7 +695,22 @@ def add_folders(folders: typing.Dict[str, machfs.Folder]) -> None:
593695 )
594696 image_def = write_image_def (image , "Infinite HD.dsk" , dest_dir )
595697
596- return image6_def , image_def
698+ # Not much point in including Classic software for the Mac OS X image, so
699+ # we start with a fresh volume.
700+ v = machfs .Volume ()
701+ with open (os .path .join (paths .IMAGES_DIR , "Infinite HD.dsk" ), "rb" ) as base :
702+ v .read (base .read ())
703+ v .name = "Infinite HD"
704+ add_folders (import_foldersX )
705+ imageX = v .write (
706+ size = 2000 * 1024 * 1024 ,
707+ align = 512 ,
708+ desktopdb = False ,
709+ bootable = False ,
710+ )
711+ imageX_def = write_image_def (imageX , "Infinite HDX.dsk" , dest_dir )
712+
713+ return image6_def , image_def , imageX_def
597714
598715
599716def build_passthrough_image (base_name : str , dest_dir : str , compressed : bool = False ) -> ImageDef :
@@ -738,12 +855,13 @@ def read_strings(name: str) -> str:
738855 continue
739856 images .append (build_system_image (disk , temp_dir ))
740857 if not system_filter :
741- infinite_hd6_image , infinite_hd_image = build_library_images (temp_dir )
858+ infinite_hd6_image , infinite_hd_image , infinite_hdX_image = build_library_images (temp_dir )
742859 images .append (infinite_hd6_image )
743860 images .append (infinite_hd_image )
861+ images .append (infinite_hdX_image )
744862 if not library_filter :
745863 build_desktop_db6 ([infinite_hd6_image ])
746- build_desktop_db ([infinite_hd_image ])
864+ build_desktop_db ([infinite_hd_image , infinite_hdX_image ])
747865
748866 images .append (
749867 build_passthrough_image ("Infinite HD (MFS).dsk" ,
0 commit comments