validate app bundle before shipping
authorDiane Trout <diane@caltech.edu>
Tue, 17 Oct 2006 01:36:10 +0000 (01:36 +0000)
committerDiane Trout <diane@caltech.edu>
Tue, 17 Oct 2006 01:36:10 +0000 (01:36 +0000)
ticket:180
This path adds some support for parsing otool results so I can test
to make sure that object files are only linked against /usr/lib,
/System/Library or @executable_path entries.

I changed it to use subprocess (which required installation on OS X
because of python2.3) and it'll also handle the uploading of packages
now.

makelib/osxdist.py

index 02ba255163ae7d8708a60102465cd5f57da262b6..37fe3b6e3e660bc13b7305e758759fb532b99bc2 100644 (file)
@@ -1,16 +1,61 @@
 #!/usr/bin/env python
 
 import os
+import re
 import sys
 import getopt
 
+try:
+  from subprocess import call, Popen, PIPE
+except ImportError, e:
+  print >>sys.stderr, "Need to install subprocess or use python >= 2.4"
+  print >>sys.stderr, "See http://www.lysator.liu.se/~astrand/popen5/"
+  raise e
+
 from shutil import copy, copytree
 
-def system(cmdline):
-  #print >>sys.stderr, cmdline
-  return os.system(cmdline)
+class otool(object):
+  """Gather information from an object file using otool"""
+  def __init__(self, object_path):
+    self.otool = "/usr/bin/otool"
+    self.object_path = object_path
+    if not os.path.exists(self.object_path):
+      raise RuntimeError("path not found")
+    
+  def __run_command(self, option):
+    p = Popen([self.otool, option, self.object_path],
+              bufsize=1, stdin=PIPE, stdout=PIPE, close_fds=True)
+    os.waitpid(p.pid, 0)
+    return p
+  
+  def _is_object(self):
+    """"Check to see if object_path is an object-file"""
+    p = self.__run_command("-h")
+    header = p.stdout.read()
+    if re.search("Mach header", header):
+      return True
+    else:
+      return False
+  isObject = property(_is_object)
+                          
+  def _get_shared_libraries(self):
+    """Return list of shared libraries"""
+    if not self.isObject:
+      raise RuntimeError("Not object")
+    p = self.__run_command("-L")
+    libraries = []
+    output_lines = p.stdout.readlines()
+    # ignore the header, or perhaps we should test it
+    # to see if it matches self.object_path
+    for line in output_lines[1:]:
+      if len(line) > 0:
+        libraries.append(line.split()[0].strip())
+    return libraries
+  SharedLibraries = property(_get_shared_libraries)  
 
 def mkdir(path):
+  """make all the components of a specified path
+  """
   path_list = []
   head, tail = os.path.split(path)
   path_list.insert(0, tail)
@@ -22,7 +67,16 @@ def mkdir(path):
     created_path = os.path.join(created_path, path_element)
     if not os.path.exists(created_path):
       os.mkdir(created_path)
-    
+      
+def ship(filepath, desturl):
+  """Ship filepath via scp to desturl
+  """
+  if os.path.exists(filepath):
+    result = call(["/usr/bin/scp", filepath, desturl])
+  else:
+    print >>sys.stderr, "%s doesn't exist" %( filepath )
+  return None
+  
 # useful information about building dmgs
 # http://developer.apple.com/documentation/Porting/Conceptual/PortingUnix
 # http://developer.apple.com/documentation/developertools/Conceptual/SoftwareDistribution/Concepts/sd_disk_images.html
@@ -30,31 +84,32 @@ def mkdir(path):
 def makedmg(dirlist, volname):
   """copy a list of directories into a dmg named volname
   """
-
   # need to detect what the real volume name is
-  mussa_mount = '/Volumes/%s' %(volname)
-  mussarw_dmg = '%sw.dmg' %(volname)
-  mussa_dmg = '%s.dmg' %(volname)
-  system('hdiutil detach '+mussa_mount)
-  if os.path.exists(mussa_mount):
-    print >>sys.stderr, "Something is in", mussa_mount
+  mount_point = '/Volumes/%s' %(volname)
+  rwdmg = '%sw.dmg' %(volname)
+  dmg = '%s.dmg' %(volname)
+  call(['hdiutil','detach',mount_point])
+  if os.path.exists(mount_point):
+    print >>sys.stderr, "Something is in", mount_point
     return 
-  if os.path.exists(mussarw_dmg):
-    os.unlink(mussarw_dmg)
-  if os.path.exists(mussa_dmg):
-    os.unlink(mussa_dmg)
-  system('hdiutil create -size 256m -fs HFS+ -volname "%s" %s'%(volname, mussarw_dmg))
-  system('hdiutil attach '+mussarw_dmg)
+  if os.path.exists(rwdmg):
+    os.unlink(rwdmg)
+  if os.path.exists(dmg):
+    os.unlink(dmg)
+  create=['hdiutil','create','-size','256m','-fs','HFS+','-volname',volname, rwdmg]
+  call(create)
+  call(['hdiutil','attach',rwdmg])
   # copy files
   for d in dirlist:
     if d[-1] == '/':
       d = d[:-1]
     tail = os.path.split(d)[-1]
-    copytree(d, os.path.join(mussa_mount, tail))
+    copytree(d, os.path.join(mount_point, tail))
 
-  system('hdiutil detach '+mussa_mount)
-  system('hdiutil convert '+mussarw_dmg +' -format UDZO -o '+mussa_dmg)
-  #system('hdiutil internet-enable -yes '+mussa_dmg)
+  call(['hdiutil','detach',mount_point])
+  call(['hdiutil','convert',rwdmg,'-format','UDZO','-o',dmg])
+  #call('hdiutil','internet-enable','-yes',dmg)
+  return dmg
 
 def prelinkqt(app_name, bundle_dir, qt_lib_dir):
   """
@@ -76,36 +131,74 @@ def prelinkqt(app_name, bundle_dir, qt_lib_dir):
     mkdir(os.path.split(appframe)[0])
     # update binary
     copy(qtframe, appframe)
-    system("install_name_tool -id "+exe_path+" "+appframe)
-    system("install_name_tool -change "+qtframe+" "+exe_path+" "+app_binary)
+    call(['install_name_tool','-id',exe_path,appframe])
+    call(['install_name_tool','-change',qtframe,exe_path,app_binary])
     # now that everything is in place change the frameworks 
     # references
     for frame2 in frameworks:
       qtframe2 = qt_framework % ({'framework': frame2})
       contents_exe_path = "@executable_path/../Frameworks/" + framework_subpath
       contents_exe_path %= ({'framework': frame2})
-      system("install_name_tool -change "+qtframe2+" "+contents_exe_path+" "+
-             appframe)
-
+      call(['install_name_tool','-change',qtframe2,contents_exe_path,appframe])
+      
+def validate_bundle(bundle_path):
+  """
+  Go look through an OS X bundle for references to things that aren't in
+  /System/Library/Framework /usr/lib or @executable_path
+  """
+  def isAllowed(lib):
+    """Is lib one of the allowed paths?"""
+    allowed_paths = [ re.compile("/usr/lib/"), 
+                      re.compile("/System/Library/Frameworks/.*"),
+                      re.compile("\@executable_path/.*") ]
+    for allowed in allowed_paths:
+      if allowed.match(lib) is not None:
+        return True
+    # if no pattern matched its not allowed
+    return False
+  
+  valid = True
+  for curdir, subdirs, files in os.walk(bundle_path):
+    for f in files:
+      curpath = os.path.join(curdir, f)
+      obj = otool(curpath)
+      if obj.isObject: 
+        for lib in obj.SharedLibraries:
+          if not isAllowed(lib):
+            print >>sys.stderr, "invalid library",lib,"in",curpath
+            valid = False
+  return valid
+  
 def main(args):
   qtroot = "/usr/local/qt/4.1.4"
-  bundle_dir = None
   app_name = None
+  build_num = None
+  bundle_dir = None
+  dmgfile = None
+  desturl = None
   
-  opts, args = getopt.getopt(args, "b:n:q:h", 
-                             ["bundle=", "name=", "qt-root=", "help"])
+  opts, args = getopt.getopt(args, "a:b:d:n:q:h", 
+                             ["appbundle=", "build-num=", "destination=",
+                              "name=", "qt-root=", "help"])
   for option, argument in opts:
-    if option in ("-b", "--bundle"):
+    if option in ("-a", "--appbundle"):
       bundle_dir = argument
+    elif option in ("-b", "--build-num"):
+      build_num = argument
+    elif option in ("-d", "--destination"):
+      desturl = argument
     elif option in ("-n", "--name"):
       app_name = argument
     elif option in ("-q", "--qt-root"):
       qtroot = argument
     elif option in ("-h", "--help"):
-      print "-b | --bundle   specify path to application bundle dir"
-      print "-n | --name     specify application name (and volume name)"
-      print "-q | --qtdir    where is qt is installed"
-      print "-h | --help     how to use"
+      print "-a | --appbundle    specify path to application bundle dir"
+      print "-b | --build-num    specify build number, used to construct"
+      print "                    volume name"      
+      print "-n | --name         specify application name and base volume name"
+      print "-q | --qtdir        where is qt is installed"
+      print "-d | --destination  scp destination for distribution"
+      print "-h | --help         usage"
       return 0
 
   # compute bundle name/dir
@@ -121,6 +214,11 @@ def main(args):
     path, file = os.path.split(bundle_dir)
     app_name = os.path.splitext(file)[0]
 
+  if build_num:
+    dmg_name = app_name + "-" + build_num
+  else:
+    dmg_name = app_name
+    
   if not os.path.exists(bundle_dir):
     print >>sys.stderr, "couldn't find an app at %s" % (app_bundle)
     return 1
@@ -128,7 +226,15 @@ def main(args):
   qt_lib_dir = os.path.join(qtroot, 'lib')
 
   prelinkqt(app_name, bundle_dir, qt_lib_dir)
-  makedmg([bundle_dir]+args, app_name)
+  if validate_bundle(bundle_dir):
+    dmgfile = makedmg([bundle_dir]+args, dmg_name)
+  else:
+    print >>sys.stderr, "Invalid libraries found"
+    return 1
+
+  if dmgfile and desturl:
+    print "Uploading",dmgfile,"to",desturl
+    ship(dmgfile, desturl)
 
 if __name__ == "__main__":
-  main(sys.argv[1:])
+  sys.exit(main(sys.argv[1:]))