Skip to content
Snippets Groups Projects
Commit f868a300 authored by Joe Garcia's avatar Joe Garcia
Browse files

macos script using lipo to merge multiple platforms into a single deliverable

parent 18595426
No related branches found
No related tags found
No related merge requests found
#!/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())
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment