diff --git a/ShellB3/sb3bin/duolipo.py b/ShellB3/sb3bin/duolipo.py new file mode 100755 index 0000000000000000000000000000000000000000..fdf2b9f421395d10577439bfa855d8f233ca998d --- /dev/null +++ b/ShellB3/sb3bin/duolipo.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python +import os, sys, stat +import logging, re +import subprocess as sp +import platform +import shutil + +LOG = logging.getLogger(__name__) +import macholib.MachO +import macholib.mach_o + +def acceptableMachoHeaders(path=None,machoobj=None,includeall=False): + if machoobj is None: + try: + machoobj=macholib.MachO.MachO(path) + except: + return + is64='64' in os.getenv('CPUTYPE',platform.machine()) + headerclass = macholib.mach_o.mach_header_64 if is64 else macholib.mach_o.mach_header + for header in machoobj.headers: + if header.mach_header is not headerclass and not path.endswith('.in') and not includeall: + continue + yield header + +def twheismacho(path,linkok=False,bareObjectsToo=False,stubsToo=False,dsymsToo=False): + try: + #print path + if not linkok and os.path.islink(path): + return False + if path.endswith('.o') and not bareObjectsToo: + return False + m=macholib.MachO.MachO(path) + #print m.headers[0].size,path + if m==None: + return False + for h in acceptableMachoHeaders(path,m): + if h.filetype=='dylib_stub' and not stubsToo: + LOG.debug(path+" is a stub. can't rpath it") + return False#suspected stub + if h.filetype=="dsym" and not dsymsToo: + LOG.debug(path+" is a DWARF symbol table. can't rpath it") + return False + return True + except: + return False + + +def copyperms(path,frompath): + perms=stat.S_IMODE(os.stat(frompath).st_mode) + os.chmod(path,perms) + +def recursivemkdir(path,matching=None): + if os.path.exists(path): + return + if not os.path.exists(os.path.dirname(path)): + recursivemkdir(os.path.dirname(path),matching=None if matching is None else os.path.dirname(matching)) + os.mkdir(path) + if matching is not None: + copyperms(path,matching) + +def commandline(*args): + pid=sp.Popen(args, stdout=sp.PIPE, stderr=sp.PIPE) + out,err = pid.communicate() + return pid.returncode,out.decode('utf8'),err.decode('utf8') + +def inplacelipo(arch1,src1,arch2,src2): + os.rename(src1,src1+'.'+arch1) + r=lipo(src1,arch1,src1+'.'+arch1,arch2,src2) + if r==0: + os.unlink(src1+'.'+arch1) + else: + os.rename(src1+'.'+arch1,src1) + return r + +def lipo(output,arch1,src1,arch2,src2): + if not os.path.exists(os.path.dirname(output)): + recursivemkdir(os.path.dirname(output),matching=os.path.dirname(src1)) + return commandline('lipo','-create','-arch',arch1,src1,'-arch',arch2,src2,'-output',output)[0] + +def rsync(src,dest,name=None): + if os.path.isdir(src): + src=src+'/' + LOG.warning("Syncing {} {}".format(name or dest,"" if not name else dest)) + return commandline("rsync","-a","--delete",src,dest)[0] + +def ismacho(path): + ret=twheismacho(path,bareObjectsToo=True,stubsToo=True,dsymsToo=True) + #LOG.error("{} is{} macho".format(path,"" if ret else " NOT")) + return ret + +def sameFiles(p1,p2): + s1=os.stat(p1) + s2=os.stat(p2) + #if s1.st_size!=s2.st_size: + # return False + f1=open(p1,"rb").read() + f2=open(p2,"rb").read() + return f1==f2 + +def recurse(path,links=False,files=False,dirs=False,dorecurse=True): + dd=os.listdir(path) + dd.sort() + for f in dd: + if f.startswith("."): + continue + fp=os.path.join(path,f) + if os.path.islink(fp): + if links: + yield fp + elif os.path.isdir(fp): + if dirs: + yield fp + if dorecurse: + for y in recurse(fp,links=links,files=files,dirs=dirs): + yield y + else: + if files: + yield fp + +def tryMergePaths(platform1,base1,platform2,base2,dry_run=False,skip=None): + syncedDirectories=list() + for p1 in recurse(base1,links=True,files=True,dirs=True): + if skip is not None and skip(p1): + LOG.info("Skipping {}".format(p1)) + continue + if p1.startswith(tuple(syncedDirectories)): + continue + p2=p1.replace(base1,base2) + common=p1.replace(base1+"/","") + LOG.info("Comparing {} {}".format(common,p2)) + if not os.path.exists(p2): + if os.path.basename(p2)=="__pycache__": + LOG.debug("{} no collision, but won't sync just a cache folder") + continue + LOG.debug("{} no collision".format(common)) + if not dry_run: + recursivemkdir(os.path.dirname(p2),matching=os.path.dirname(p1)) + rsync(p1,p2,name=common) + syncedDirectories.append(p1) + continue + if os.path.islink(p1) or os.path.islink(p2): + if not (os.path.islink(p1) and os.path.islink(p2)): + LOG.error("{} aren't both links. FAIL".format(common)) + continue + l1=os.readlink(p1) + l2=os.readlink(p2) + if l1!=l2: + LOG.info("{} differ in link content {} and {}".format(common,l1,l2)) + if os.path.basename(os.path.dirname(common)) in ("bin","MacOS"): + #smart link + LOG.warning("link {} to {}:{} and {}:{}".format(common,platform1,l1,platform2,l2)) + if not dry_run: + os.unlink(p2) + f=open(p2,"w") + f.write("#!/usr/bin/python\n") + f.write("import platform\n") + f.write("import os,sys\n") + f.write("runoptions={\n") + f.write(" '{}':'{}',\n".format(platform1,l1)) + f.write(" '{}':'{}'\n".format(platform2,l2)) + f.write("}\n") + f.write("bn=os.path.dirname(sys.argv[0])\n") + f.write("mach=platform.machine()\n") + f.write("if mach not in runoptions:") + f.write(" raise RuntimeError('Unsupported platform '+mach)\n") + f.write("binval=runoptions[mach]\n") + f.write("os.execv(os.path.join(bn,binval),[os.path.join(bn,binval)]+list(sys.argv[1:]))\n") + del f + os.chmod(p2,0o755) + elif os.path.basename(common)=="Current": + #current link. remove it + if not dry_run: + os.unlink(p2) + elif common.endswith(".dylib"): + #symlink to a dylib. drop + LOG.info("Would remove {} for compiletime library".format(common)) + if not dry_run: + os.unlink(p2) + else: + LOG.error("Can't fix link {} to {} or {}".format(common,l1,l2)) + continue + LOG.debug("{} match link content {}".format(common,l1)) + continue + if os.path.isdir(p1) or os.path.isdir(p2): + if not (os.path.isdir(p1) and os.path.isdir(p2)): + LOG.error("{} aren't both directories. FAIL".format(common)) + continue + continue + if ismacho(p1) or ismacho(p2): + if not (ismacho(p1) and ismacho(p2)): + LOG.error("{} aren't both macho. FAIL".format(common)) + continue + if dry_run: + LOG.info("Would lipo {}".format(common)) + elif inplacelipo(platform2,p2,platform1,p1)==0: + LOG.debug("Merged {}".format(common)) + else: + LOG.error("Failed to merge {}".format(common)) + elif sameFiles(p1,p2): + LOG.debug("{} match content".format(common)) + else: + #pass + + if common.startswith("doc/") or "/man/" in common or "/info/" in common: + LOG.info("Skipping documentation collision {}".format(common)) + elif common.endswith(('.cmake','.pc','.h','.mod',"Config.sh",".prl",".prf",".pri")) or "/Headers/" in common or "/include/" in common or common.startswith("include/"): + LOG.info("Skipping collision of compile-time file {}".format(common)) + elif common.endswith(('.qml','.qmltypes','/qmldir','.metainfo','.ttf',".plist",".icns",".qm")): + LOG.error("Version collision on file {}".format(common)) + elif common in ("trim",): + if not dry_run: + os.rename(p2,p2+"."+platform2) + rsync(p1,p2+"."+platform1,name=common+"."+platform1) + elif os.path.basename(os.path.dirname(common)) in ("bin","MacOS"): + LOG.info("Collision of {} to be replaced with a script".format(common)) + if not dry_run: + os.rename(p2,p2+"."+platform2) + rsync(p1,p2+"."+platform1,name=common+"."+platform1) + if True: + f=open(p2,"w") + f.write("#!/usr/bin/python\n") + f.write("import platform\n") + f.write("import os,sys\n") + f.write("os.execv(sys.argv[0]+'.'+platform.machine(),sys.argv)\n") + del f + os.chmod(p2,0o755) + elif '/__pycache__/' in common and common.endswith(".pyc"): #is part of a pycache. should purge because one won't like it + LOG.info("Collision of {} is not critical, but bad for both. Removing from merged".format(common)) + if not dry_run: + os.unlink(p2) + elif common.endswith('.py'): + LOG.warning("Python module Collision of {} to be replaced with a python module proxy".format(common)) + if not dry_run: + p2base=os.path.dirname(p2) + modulename=os.path.basename(p2)[:-3] + while modulename.startswith('_'): + modulename=modulename[1:] + modulename='lipo_'+modulename + module1=modulename+"_"+platform1 + module2=modulename+"_"+platform2 + form=dict(platform1=platform1,platform2=platform2,module1=module1,module2=module2) + os.rename(p2,os.path.join(p2base,module2+".py")) + rsync(p1,os.path.join(p2base,module1+".py"),name=os.path.join(os.path.dirname(common),module1+".py")) + if True: + f=open(p2,"w") + f.write("import sys\n") + f.write("import platform\n") + f.write("import logging\n") + f.write("logger=logging.getLogger(__name__)\n") + f.write("self=sys.modules[__name__]\n") + f.write("if platform.machine() == '{platform1}':\n".format(**form)) + f.write(" logger.debug('Will proxy load {module1} version '+platform.machine()+' in place of '+__name__)\n".format(**form)) + f.write(" try:\n") + f.write(" from . import {module1} as lipod\n".format(**form)) + f.write(" except ImportError:\n") + f.write(" import {module1} as lipod\n".format(**form)) + f.write("elif platform.machine() == '{platform2}':\n".format(**form)) + f.write(" logger.debug('Will proxy load {module2} version '+platform.machine()+' in place of '+__name__)\n".format(**form)) + f.write(" try:\n") + f.write(" from . import {module2} as lipod\n".format(**form)) + f.write(" except ImportError:\n") + f.write(" import {module2} as lipod\n".format(**form)) + f.write("else:\n") + f.write(" raise RuntimeError('Unknown platform '+platform.machine())\n") + f.write("lipod.__name__=__name__\n") + f.write("sys.modules[__name__]=lipod\n") + f.write("\n") + del f + os.chmod(p2,0o755) + else: + LOG.error("{} contents differ".format(common)) + #r=["","Would compare {}".format(common)] + #r=commandline("diff","-u",p1,p2) + #print(r[1]) + +def skipLameFiles(f): + if os.path.basename(f)==".DS_Store" or os.path.basename(f).startswith('.'): + return True + return False + +def main(): + import optparse + usage = """ +%prog [options] [platform1 base1] [platform2 base2] newbase +example: +python duolipo.py x86_64 /somewhere/ShellB3-x86path/ arm64 /somewhere/ShellB3-arm64path/ /somewhere/ShellB3-mergetargetpath/ +""" + parser = optparse.OptionParser(usage) + parser.add_option('-d', '--dry-run', dest="dry_run", + action="store_true", default=False, help="show what commands would be done") + parser.add_option('-v', '--verbose', dest='verbosity', action="count", default=0, + help='each occurrence increases verbosity 1 level through ERROR-WARNING-INFO-DEBUG') + # parser.add_option('-I', '--include-path', dest="includes", + # action="append", help="include path to append to GCCXML call") + + if len(sys.argv)==1: + parser.print_help() + return 0 + (options, args) = parser.parse_args() + #print args + + levels = [logging.ERROR, logging.WARN, logging.INFO, logging.DEBUG] + logging.basicConfig(level = levels[min(3,options.verbosity)]) + + # make options a globally accessible structure, e.g. OPTS. + global OPTS + OPTS = options + + platform1=args[0] + base1=args[1] + platform2=args[2] + base2=args[3] + if len(args)>4: + newbase=args[4] + assert(len(args)==5) + else: + options.dry_run=True + + if options.dry_run: + tryMergePaths(platform2,base2,platform1,base1,dry_run=True,skip=skipLameFiles) + # FIXME - run any self-tests + # import doctest + # doctest.testmod() + sys.exit(0) + + rsync(base1,newbase,name="base") + tryMergePaths(platform2,base2,platform1,newbase,skip=skipLameFiles) + + return 0 + +if __name__=='__main__': + sys.exit(main())