be6f605b908b52bc734e5ad2b086a38af15476d1
[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 head != os.path.sep and 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     if (os.path.isdir(d)):
108       copytree(d, os.path.join(mount_point, tail))
109     else:
110       copy(d, os.path.join(mount_point, tail))
111
112   call(['hdiutil','detach',mount_point])
113   call(['hdiutil','convert',rwdmg,'-format','UDZO','-o',dmg])
114   #call('hdiutil','internet-enable','-yes',dmg)
115   return dmg
116
117 def prelinkqt(app_name, bundle_dir, qt_lib_dir):
118   """
119   OS X's treatment of dynamic loading is annoying
120   properly prelink all the annoying qt components.
121   """
122   print >>sys.stderr, "Prelinking", app_name, "in", bundle_dir,
123   framework_subpath = os.path.join("%(framework)s.framework", "Versions", "4", "%(framework)s")
124   frameworks = ['QtCore', 'QtGui', 'QtOpenGL', 'QtAssistant', 'QtNetwork']
125
126   qt_framework=os.path.join(qt_lib_dir, framework_subpath)
127   app_binary=bundle_dir+"/Contents/MacOS/"+app_name
128   app_framework=os.path.join(bundle_dir, "Contents", "Frameworks", framework_subpath)
129   # install frameworks and update binary
130   for frame in frameworks:
131     qtframe = qt_framework % ({'framework': frame})
132     appframe = app_framework % ({'framework': frame})
133     exe_path = "@executable_path/../Frameworks/" + framework_subpath
134     exe_path %= ({'framework': frame})
135     mkdir(os.path.split(appframe)[0])
136     # update binary
137     copy(qtframe, appframe)
138     call(['install_name_tool','-id',exe_path,appframe])
139     call(['install_name_tool','-change',qtframe,exe_path,app_binary])
140     # now that everything is in place change the frameworks 
141     # references
142     for frame2 in frameworks:
143       qtframe2 = qt_framework % ({'framework': frame2})
144       contents_exe_path = "@executable_path/../Frameworks/" + framework_subpath
145       contents_exe_path %= ({'framework': frame2})
146       call(['install_name_tool','-change',qtframe2,contents_exe_path,appframe])
147   print "."
148       
149 def validate_bundle(bundle_path):
150   """
151   Go look through an OS X bundle for references to things that aren't in
152   /System/Library/Framework /usr/lib or @executable_path
153   """
154   def isAllowed(lib):
155     """Is lib one of the allowed paths?"""
156     allowed_paths = [ re.compile("/usr/lib/"), 
157                       re.compile("/System/Library/Frameworks/.*"),
158                       re.compile("\@executable_path/.*") ]
159     for allowed in allowed_paths:
160       if allowed.match(lib) is not None:
161         return True
162     # if no pattern matched its not allowed
163     return False
164   
165   valid = True
166   for curdir, subdirs, files in os.walk(bundle_path):
167     for f in files:
168       curpath = os.path.join(curdir, f)
169       obj = otool(curpath)
170       if obj.isObject: 
171         for lib in obj.SharedLibraries:
172           if not isAllowed(lib):
173             print >>sys.stderr, "invalid library",lib,"in",curpath
174             valid = False
175   return valid
176   
177 def main(args):
178   qtroot = "/usr/local/qt/4.2.2"
179   app_name = None
180   build_num = None
181   bundle_dir = None
182   dmgfile = None
183   desturl = None
184   prelink_only = False
185   
186   opts, args = getopt.getopt(args, "a:b:d:n:q:h", 
187                              ["appbundle=", "build-num=", "destination=",
188                               "name=", "prelink-only", "qt-root=", "help"])
189
190   for option, argument in opts:
191     if option in ("-a", "--appbundle"):
192       bundle_dir = argument
193     elif option in ("-b", "--build-num"):
194       build_num = argument
195     elif option in ("-d", "--destination"):
196       desturl = argument
197     elif option in ("-n", "--name"):
198       app_name = argument
199     elif option in ("--prelink-only"):
200       prelink_only = True
201     elif option in ("-q", "--qt-root"):
202       qtroot = argument
203     elif option in ("-h", "--help"):
204       print "-a | --appbundle    specify path to application bundle dir"
205       print "-b | --build-num    specify build number, used to construct"
206       print "                    volume name"      
207       print "-n | --name         specify application name and base volume name"
208       print "-q | --qtdir        where is qt is installed"
209       print "-d | --destination  scp destination for distribution"
210       print "-h | --help         usage"
211       return 0
212
213   # compute bundle name/dir
214   if bundle_dir is None and app_name is None:
215     print >>sys.stderr, "I need a name or bundle path"
216     return 1
217   elif bundle_dir is None:
218     bundle_dir = app_name + ".app"
219   elif app_name is None:
220     # strip off trailing /
221     if bundle_dir[-1] == os.path.sep:
222       bundle_dir = bundle_dir[:-1]
223     path, file = os.path.split(bundle_dir)
224     app_name = os.path.splitext(file)[0]
225
226   if build_num:
227     dmg_name = app_name + "-" + build_num
228   else:
229     dmg_name = app_name
230     
231   bundle_dir = os.path.expanduser(bundle_dir)
232   
233   if not os.path.exists(bundle_dir):
234     print >>sys.stderr, "couldn't find an app at %s" % (app_bundle)
235     return 1
236   
237   qt_lib_dir = os.path.join(qtroot, 'lib')
238
239   prelinkqt(app_name, bundle_dir, qt_lib_dir)
240   if not validate_bundle(bundle_dir):
241     print >>sys.stderr, "Invalid libraries found"
242     return 1
243
244   if prelink_only:
245     return 0
246   
247   dmgfile = makedmg([bundle_dir]+args, dmg_name)
248   if dmgfile and desturl:
249     print "Uploading",dmgfile,"to",desturl
250     ship(dmgfile, desturl)
251
252 if __name__ == "__main__":
253   sys.exit(main(sys.argv[1:]))