b6b6134ae52de814d2b4d97b4b2f6d4bf526d120
[mussa.git] / makelib / osxdist.py
1 #!/usr/bin/env python
2
3 import os
4 import re
5 import sys
6 import getopt
7
8 try:
9   from subprocess import call, Popen, PIPE
10 except ImportError, e:
11   print >>sys.stderr, "Need to install subprocess or use python >= 2.4"
12   print >>sys.stderr, "See http://www.lysator.liu.se/~astrand/popen5/"
13   raise e
14
15 from shutil import copy, copytree
16
17 class otool(object):
18   """Gather information from an object file using otool"""
19   def __init__(self, object_path):
20     self.otool = "/usr/bin/otool"
21     self.object_path = object_path
22     if not os.path.exists(self.object_path):
23       raise RuntimeError("path not found")
24     
25   def __run_command(self, option):
26     p = Popen([self.otool, option, self.object_path],
27               bufsize=1, stdin=PIPE, stdout=PIPE, close_fds=True)
28     os.waitpid(p.pid, 0)
29     return p
30   
31   def _is_object(self):
32     """"Check to see if object_path is an object-file"""
33     p = self.__run_command("-h")
34     header = p.stdout.read()
35     if re.search("Mach header", header):
36       return True
37     else:
38       return False
39   isObject = property(_is_object)
40                           
41   def _get_shared_libraries(self):
42     """Return list of shared libraries"""
43     if not self.isObject:
44       raise RuntimeError("Not object")
45     p = self.__run_command("-L")
46     libraries = []
47     output_lines = p.stdout.readlines()
48     # ignore the header, or perhaps we should test it
49     # to see if it matches self.object_path
50     for line in output_lines[1:]:
51       if len(line) > 0:
52         libraries.append(line.split()[0].strip())
53     return libraries
54   SharedLibraries = property(_get_shared_libraries)  
55
56 def mkdir(path):
57   """make all the components of a specified path
58   """
59   path_list = []
60   head, tail = os.path.split(path)
61   path_list.insert(0, tail)
62   while len(head) > 0:
63     head, tail = os.path.split(head)
64     path_list.insert(0, tail)
65   created_path = ""
66   for path_element in path_list:
67     created_path = os.path.join(created_path, path_element)
68     if not os.path.exists(created_path):
69       os.mkdir(created_path)
70       
71 def ship(filepath, desturl):
72   """Ship filepath via scp to desturl
73   """
74   if os.path.exists(filepath):
75     result = call(["/usr/bin/scp", filepath, desturl])
76   else:
77     print >>sys.stderr, "%s doesn't exist" %( filepath )
78   return None
79   
80 # useful information about building dmgs
81 # http://developer.apple.com/documentation/Porting/Conceptual/PortingUnix
82 # http://developer.apple.com/documentation/developertools/Conceptual/SoftwareDistribution/Concepts/sd_disk_images.html
83
84 def makedmg(dirlist, volname):
85   """copy a list of directories into a dmg named volname
86   """
87   # need to detect what the real volume name is
88   mount_point = '/Volumes/%s' %(volname)
89   rwdmg = '%sw.dmg' %(volname)
90   dmg = '%s.dmg' %(volname)
91   call(['hdiutil','detach',mount_point])
92   if os.path.exists(mount_point):
93     print >>sys.stderr, "Something is in", mount_point
94     return 
95   if os.path.exists(rwdmg):
96     os.unlink(rwdmg)
97   if os.path.exists(dmg):
98     os.unlink(dmg)
99   create=['hdiutil','create','-size','256m','-fs','HFS+','-volname',volname, rwdmg]
100   call(create)
101   call(['hdiutil','attach',rwdmg])
102   # copy files
103   for d in dirlist:
104     if d[-1] == '/':
105       d = d[:-1]
106     tail = os.path.split(d)[-1]
107     copytree(d, os.path.join(mount_point, tail))
108
109   call(['hdiutil','detach',mount_point])
110   call(['hdiutil','convert',rwdmg,'-format','UDZO','-o',dmg])
111   #call('hdiutil','internet-enable','-yes',dmg)
112   return dmg
113
114 def prelinkqt(app_name, bundle_dir, qt_lib_dir):
115   """
116   OS X's treatment of dynamic loading is annoying
117   properly prelink all the annoying qt components.
118   """
119   framework_subpath = os.path.join("%(framework)s.framework", "Versions", "4", "%(framework)s")
120   frameworks = ['QtCore', 'QtGui', 'QtOpenGL', 'QtAssistantClient', 'QtNetwork']
121
122   qt_framework=os.path.join(qt_lib_dir, framework_subpath)
123   app_binary=bundle_dir+"/Contents/MacOS/"+app_name
124   app_framework=os.path.join(bundle_dir, "Contents", "Frameworks", framework_subpath)
125   # install frameworks and update binary
126   for frame in frameworks:
127     qtframe = qt_framework % ({'framework': frame})
128     appframe = app_framework % ({'framework': frame})
129     exe_path = "@executable_path/../Frameworks/" + framework_subpath
130     exe_path %= ({'framework': frame})
131     mkdir(os.path.split(appframe)[0])
132     # update binary
133     copy(qtframe, appframe)
134     call(['install_name_tool','-id',exe_path,appframe])
135     call(['install_name_tool','-change',qtframe,exe_path,app_binary])
136     # now that everything is in place change the frameworks 
137     # references
138     for frame2 in frameworks:
139       qtframe2 = qt_framework % ({'framework': frame2})
140       contents_exe_path = "@executable_path/../Frameworks/" + framework_subpath
141       contents_exe_path %= ({'framework': frame2})
142       call(['install_name_tool','-change',qtframe2,contents_exe_path,appframe])
143       
144 def validate_bundle(bundle_path):
145   """
146   Go look through an OS X bundle for references to things that aren't in
147   /System/Library/Framework /usr/lib or @executable_path
148   """
149   def isAllowed(lib):
150     """Is lib one of the allowed paths?"""
151     allowed_paths = [ re.compile("/usr/lib/"), 
152                       re.compile("/System/Library/Frameworks/.*"),
153                       re.compile("\@executable_path/.*") ]
154     for allowed in allowed_paths:
155       if allowed.match(lib) is not None:
156         return True
157     # if no pattern matched its not allowed
158     return False
159   
160   valid = True
161   for curdir, subdirs, files in os.walk(bundle_path):
162     for f in files:
163       curpath = os.path.join(curdir, f)
164       obj = otool(curpath)
165       if obj.isObject: 
166         for lib in obj.SharedLibraries:
167           if not isAllowed(lib):
168             print >>sys.stderr, "invalid library",lib,"in",curpath
169             valid = False
170   return valid
171   
172 def main(args):
173   qtroot = "/usr/local/qt/4.2.1"
174   app_name = None
175   build_num = None
176   bundle_dir = None
177   dmgfile = None
178   desturl = None
179   
180   opts, args = getopt.getopt(args, "a:b:d:n:q:h", 
181                              ["appbundle=", "build-num=", "destination=",
182                               "name=", "qt-root=", "help"])
183   for option, argument in opts:
184     if option in ("-a", "--appbundle"):
185       bundle_dir = argument
186     elif option in ("-b", "--build-num"):
187       build_num = argument
188     elif option in ("-d", "--destination"):
189       desturl = argument
190     elif option in ("-n", "--name"):
191       app_name = argument
192     elif option in ("-q", "--qt-root"):
193       qtroot = argument
194     elif option in ("-h", "--help"):
195       print "-a | --appbundle    specify path to application bundle dir"
196       print "-b | --build-num    specify build number, used to construct"
197       print "                    volume name"      
198       print "-n | --name         specify application name and base volume name"
199       print "-q | --qtdir        where is qt is installed"
200       print "-d | --destination  scp destination for distribution"
201       print "-h | --help         usage"
202       return 0
203
204   # compute bundle name/dir
205   if bundle_dir is None and app_name is None:
206     print >>sys.stderr, "I need a name or bundle path"
207     return 1
208   elif bundle_dir is None:
209     bundle_dir = app_name + ".app"
210   elif app_name is None:
211     # strip off trailing /
212     if bundle_dir[-1] == os.path.sep:
213       bundle_dir = bundle_dir[:-1]
214     path, file = os.path.split(bundle_dir)
215     app_name = os.path.splitext(file)[0]
216
217   if build_num:
218     dmg_name = app_name + "-" + build_num
219   else:
220     dmg_name = app_name
221     
222   if not os.path.exists(bundle_dir):
223     print >>sys.stderr, "couldn't find an app at %s" % (app_bundle)
224     return 1
225   
226   qt_lib_dir = os.path.join(qtroot, 'lib')
227
228   prelinkqt(app_name, bundle_dir, qt_lib_dir)
229   if validate_bundle(bundle_dir):
230     dmgfile = makedmg([bundle_dir]+args, dmg_name)
231   else:
232     print >>sys.stderr, "Invalid libraries found"
233     return 1
234
235   if dmgfile and desturl:
236     print "Uploading",dmgfile,"to",desturl
237     ship(dmgfile, desturl)
238
239 if __name__ == "__main__":
240   sys.exit(main(sys.argv[1:]))