#!/usr/bin/python # # A short utility script to run the UNIX 'file' command to determine # the compiled architecture of a given .app package # # David Friedlander, ADNET Systems, 3 Jan 2020 import sys, os, re, signal use_usr_bin_file_instead = 0; try: import magic #Instead of a shell call out to 'file', install and use 'libmagic' except: print "Unable to import 'magic' module. Please do 'port install py27-magic' or 'conda install python-magic'" print "Will try using /usr/bin/file instead..." # Sigh; would rather do it elegantly and internal to python, but /usr/bin/file ought to work. use_usr_bin_file_instead = 1; all_filenames = [] arch_short = "" overview = "" verbose = False not_64bit_apps = [] known_match = {} # need to define hash for i386, x86_64, ppc, ppc7400, etc (with long strings) # because a single architecture binary does not have the short term there! arch_types = { # Very odd that different generations of binaries have diff word orderings 'Mach-O 64-bit x86_64 executable': 'x86_64', 'Mach-O 64-bit executable x86_64': 'x86_64', 'Mach-O executable i386': 'i386', 'Mach-O i386 executable': 'i386', 'Mach-O executable ppc': 'ppc', 'Mach-O ppc executable': 'ppc', 'Mach-O executable ppc64': 'ppc64', 'ppc': 'ppc', #yeah, this is weird (e.g., old Amadeus Pro) 'POSIX shell script, ASCII text executable': 'shell script', 'Tenex C shell script, ASCII text executable': 'tcsh shell script', } def signal_handler(sig, frame): # https://stackoverflow.com/questions/1112343/how-do-i-capture-sigint-in-python print 'You pressed Ctrl+C!' sys.exit(0) def extract_executable_type(mymagic): mymagic = re.sub('^\[', '', mymagic) # remove leading left '[' (only should be on 1st element) mymagic = re.sub('\]$', '', mymagic) # remove trailing right ']' (only should be on last element) mymagic = re.sub('\, flags:.*', '', mymagic) # remove everything incl and after ", flags:" #print "L50 [%s]" % mymagic if re.search(':', mymagic): #universal binaries prepend the short name plus a colon (e.g., "i386:") arch_details_list = re.split(':', mymagic) #now split short/long on the ':' try: (arch_short, arch_long) = arch_details_list except: arch_long = arch_details_list[0] arch_short = arch_details_list[0] else: arch_long = mymagic try: arch_short = arch_types[mymagic] except: arch_short = mymagic #arch_short = arch_types[mymagic] #print "L68 SHORT --[%s]-- \nLONG --[%s]--\n" % (arch_short, arch_long) # testingGICV return (arch_short, arch_long) def find_magic_info_for(app_path): app_path = app_path.rstrip('/') # strip right trailing slash, if present app_name = os.path.basename(app_path) app_name = re.sub('\.app$', '', app_name) app_path_escaped = re.sub(r'\+', '\\+', app_path) found = "" magic_result = "" # Special case: if re.search('(Libre|Open)Office', app_name): app_name = 'soffice' is_executable = os.access(app_path, os.X_OK) # Are we looking at a single binary or a .app hierarchy? if not os.path.isdir(app_path) and is_executable: #print "L89 %s is not a directory or application!" % app_path if use_usr_bin_file_instead == 1: magic_result = os.popen('/usr/bin/file \"%s\"' % app_path).read().rstrip() # names like "C++" need to be escaped, or else the regex will fall over! magic_result = re.sub(app_path_escaped + '\:\s+', '', magic_result) #strip 'app_name:' #magic_result2 = magic.from_file(app_path) #testing else: magic_result = magic.from_file(app_path) else: # Treat it like a double-clickable application. #print "Internal binary for this app is : " + app_path + "/Contents/MacOS/" + app_name internal_app_name = app_path + "/Contents/MacOS/" + app_name # names like "C++" need to be escaped, or else the regex will fall over! internal_app_name = re.sub(r'\+', '\\+', internal_app_name) if os.path.isdir(internal_app_name): # This test is to handle a weird special case where some quite old # files are named .app but are plain files (e.g., IDL's "mergecom.app") if use_usr_bin_file_instead == 1: magic_result = os.popen('/usr/bin/file \"%s\"' % internal_app_name).read().rstrip() #magic_result2 = magic.from_file(internal_app_name) #testing else: magic_result = magic.from_file(internal_app_name) else: #We are dealing with a non-executable. Let's move on... sys.stdout.write(".") sys.stdout.flush() return if verbose: print "--------------------------------------" print "MAGIC RESULT [" + magic_result + "]\n" #print "MAGIC RESULT (/usr/bin/file) [" + magic_result + "]" #print "MAGIC RESULT2 (pure python) [" + magic_result2 + "]" #print "--------------------------------------" # Do we have multiple architectures ('uiversal binary') ? if re.search('architectures', magic_result): magic_details = re.split('architectures: ', magic_result) overview = magic_details[0] + 'architectures' + ':' #print "L121 Full magic details are $$$ %s $$$\n\n" % magic_details[1] archs_found = re.split(r'\]\s\[', magic_details[1]) #info about each architecture found #print "L123 overview ---[%s]---" % overview for k in archs_found: #print "L125 arch found is *** %s ***" % k # When /usr/bin/file finds universal binary executables, it prints: # /tmp/foo: Mach-O universal binary with 2 architectures: [ppc:Mach-O executable ppc] [i386] # /tmp/foo (for architecture ppc): Mach-O executable ppc # /tmp/foo (for architecture i386): Mach-O executable i386 # We want to ignore those extra two lines. if use_usr_bin_file_instead and re.search('\(for architecture \w*\)\:', k) : k = re.sub('\n' + app_path_escaped + ' \(for architecture \w*\)\:\s+.*', '', k) (arch_short, arch_long) = extract_executable_type(k) found = found + " " + arch_short else: (arch_short, arch_long) = extract_executable_type(magic_result) found = arch_short #For entries of the form: # [i386:Mach-O i386 executable, flags: if verbose: print app_path print "\t==> Application name [%s]" % app_name #print overview if re.search('(ppc|i386)', found) and not re.search('x86_64', found): #print "L110 Found Short Architecture is [%s]" % found # 'found' has a leading space sometimes. Let's not print it. not_64bit_apps.append(app_path + " [" + found.lstrip(' ') + "]") #print "L157: %s " % magic_result #print "L158 %s \nshort: %s \nlong: %s" % (app_path,arch_short,arch_long) if re.search('(text|data|icon|bitmap|image|Image|JPEG|empty|movie|[Dd]ocument)', arch_long) or \ re.search('(text|data|icon|bitmap|image|JPEG|empty|movie|[Dd]ocument)', arch_short): # We are not interested in these here. # # Note we do the double test because some several image types only have # their name in 'arch_short'. #print "Yep, we are not an executable or library: we are text or data! %s" % arch_long sys.stdout.write(".") sys.stdout.flush() return # print out results print "-------" print app_path if re.search('architectures', magic_result): print app_name + ": " + overview + found #print "L150: %s: OVW %s FOUND: %s" % (app_name, overview, found) else: print "%s: %s" % (app_name, arch_short) def yeah_we_know_already(target): known_32_bit = { '/Applications/exelis/idl82': 'IDL82', '/Applications/exelis/idl83': 'IDL83', '/Applications/exelis/idl84': 'IDL84', '/Applications/exelis/idl85': 'IDL85', '/Applications/Microsoft Office 2008': 'MS Office 2008', '/Applications/Microsoft Office 2011': 'MS Office 2011', #'/Applications/ASD Standard Apps': 'ASD Std Apps', '/Applications/Pulse Secure.app': 'Pulse Secure', '/Applications/Adobe Acrobat Reader DC.ap': 'Adobe Reader' #only here for testing } for k,v in known_32_bit.items(): #print "L200: %s => %s" % (k,v) """ try: print "L202 KNOWN: %s target: %s" % (known_match[k], target) if known_match[k] == v: return True except: # not seen yet dummyA = 1 """ if re.search('^' + k, target): # Does the target path include the regex snippet in 'known_32_bit' keys? #print "Ooooooh, yes, we've seen this one before: %s for %s" % (v, target) #print "Known to have 32-bit issues: [%s] (one such component: %s" % (v, target) known_match[k] = v return True return False ##################### main program ################### signal.signal(signal.SIGINT, signal_handler) if len(sys.argv) == 1: #i.e., just the script itself print "Please specify a file or directory on the command line." sys.exit(0) elif len(sys.argv) == 2: #i.e., the script and one argument all_filenames.append(sys.argv[1] ) elif len(sys.argv) > 2: #i.e., the script and multiple arguments all_filenames = sys.argv all_filenames.pop(0) #The 0th argument is this script's name print "All cmd line arguments: [%s]" % all_filenames print "\nKey: '+' means a file which is part of something already known to have 32-bit parts" print " '.' means a file which is not an executable or library (image, data, etc)" for this_file in all_filenames: # this_file could be a file or a directory if os.path.isfile(this_file): # If it is just a file then do a simple lookup app_path = sys.argv[1] #to be dragged and dropped to command line #yeah_we_know_already(app_path) find_magic_info_for(app_path) else: # For a directory, let's traverse ("walk") it. for root, dirs, files in os.walk(this_file, topdown=False, followlinks=False): #print "ROOT: %s DIRS %s FILES %s" % (root, dirs, files) for name in files: fullpath = os.path.join(root, name) #print "FILE is %s and full path is %s" % (name, fullpath) if os.path.islink(fullpath): continue #skip symlinks if not os.path.exists(fullpath): continue if name == '.DS_Store': continue if re.search(r'\.app/Contents/(Frameworks|Resources|BundleLibrary|PlugIns)', fullpath): continue is_executable = os.access(fullpath, os.X_OK) #i.e., via UNIX permissions do_we_skip_this = yeah_we_know_already(fullpath) #print "L256 Examining: skip? %s %s" % (do_we_skip_this, fullpath) if do_we_skip_this: sys.stdout.write("+") sys.stdout.flush() continue #print "L260 Yes, we are continuing for %s" % fullpath if is_executable or re.search('\.app$', fullpath): find_magic_info_for(fullpath) print "\n--------------------------------------------------------" toplevelapps = {} if not_64bit_apps: print "\nThe following are not 64-bit applications:" j = 1 for m in sorted(not_64bit_apps): #print m print "%4u %s" % (j, m) j += 1 appbasename = re.split(r'/Contents/', m)[0] print appbasename toplevelapps[appbasename] = 'not 64-bit' if toplevelapps: q = 1 print "\nHere is a list of the affected top-level non-64-bit apps:" for p in sorted(toplevelapps): print "%4u %s" % (q,p) q += 1 else: print "\nApparently, all apps in the requested area [%s] are 64-bit (or at least, not solely ppc or i386)." % sys.argv[1] print " " try: for k,v in known_match.items(): print "Package known to have (at least some) 32-bit components: %s" % v except: dummy2=1