c1cddf55f63711beba078c453101365eba212be3
[quassel.git] / scripts / build / macosx_DeployApp.py
1 #!/usr/bin/python
2 # -*- coding: iso-8859-1 -*-
3
4 ################################################################################
5 #                                                                              #
6 # 2008 June 27th by Marcus 'EgS' Eggenberger <egs@quassel-irc.org>             #
7 #                                                                              #
8 # The author disclaims copyright to this source code.                          #
9 # This Python Script is in the PUBLIC DOMAIN.                                  #
10 #                                                                              #
11 ################################################################################
12
13 # ==============================
14 #  Imports
15 # ==============================
16 import sys
17 import os
18 import os.path
19
20 from subprocess import Popen, PIPE
21
22 # ==============================
23 #  Constants
24 # ==============================
25 QT_CONFIG = """[Paths]
26  Plugins = plugins
27 """
28
29 QT_CONFIG_NOBUNDLE = """[Paths]
30  Prefix = ../
31  Plugins = plugins
32 """
33
34
35
36
37 class InstallQt(object):
38     def __init__(self, appdir, bundle = True, requestedPlugins=[]):
39         self.appDir = appdir
40         self.bundle = bundle
41         self.frameworkDir = self.appDir + "/Frameworks"
42         self.pluginDir = self.appDir + "/plugins"
43         self.executableDir = self.appDir
44         if bundle:
45             self.executableDir += "/MacOS"
46
47         self.installedFrameworks = set()
48
49         self.findFrameworkPath()
50
51         executables = [self.executableDir + "/" + executable for executable in os.listdir(self.executableDir)]
52         for executable in executables:
53             self.resolveDependancies(executable)
54
55
56         self.findPluginsPath()
57         self.installPlugins(requestedPlugins)
58         self.installQtConf()
59
60     def qtProperty(self, qtProperty):
61         """
62         Query persistent property of Qt via qmake
63         """
64         VALID_PROPERTIES = ['QT_INSTALL_PREFIX',
65                             'QT_INSTALL_DATA',
66                             'QT_INSTALL_DOCS',
67                             'QT_INSTALL_HEADERS',
68                             'QT_INSTALL_LIBS',
69                             'QT_INSTALL_BINS',
70                             'QT_INSTALL_PLUGINS',
71                             'QT_INSTALL_IMPORTS',
72                             'QT_INSTALL_TRANSLATIONS',
73                             'QT_INSTALL_CONFIGURATION',
74                             'QT_INSTALL_EXAMPLES',
75                             'QT_INSTALL_DEMOS',
76                             'QMAKE_MKSPECS',
77                             'QMAKE_VERSION',
78                             'QT_VERSION'
79                             ]
80         if qtProperty not in VALID_PROPERTIES:
81             return None
82
83         qmakeProcess = Popen('qmake -query %s' % qtProperty, shell=True, stdout=PIPE, stderr=PIPE)
84         result = qmakeProcess.stdout.read().strip()
85         qmakeProcess.stdout.close()
86         qmakeProcess.wait()
87         return result
88
89     def findFrameworkPath(self):
90         self.sourceFrameworkPath = self.qtProperty('QT_INSTALL_LIBS')
91
92     def findPluginsPath(self):
93         self.sourcePluginsPath = self.qtProperty('QT_INSTALL_PLUGINS')
94
95     def findPlugin(self, pluginname):
96         qmakeProcess = Popen('find %s -name %s' % (self.sourcePluginsPath, pluginname), shell=True, stdout=PIPE, stderr=PIPE)
97         result = qmakeProcess.stdout.read().strip()
98         qmakeProcess.stdout.close()
99         qmakeProcess.wait()
100         if not result:
101             raise OSError
102         return result
103
104
105     def installPlugins(self, requestedPlugins):
106         try:
107             os.mkdir(self.pluginDir)
108         except:
109             pass
110
111         for plugin in requestedPlugins:
112             if not plugin.isalnum():
113                 print "Skipping library '%s'..." % plugin
114                 continue
115
116             pluginName = "lib%s.dylib" % plugin
117             pluginSource = ''
118             try:
119                 pluginSource = self.findPlugin(pluginName)
120             except OSError:
121                 print "WARNING: Requested library does not exist: '%s'" % plugin
122                 continue
123
124             pluginSubDir = os.path.dirname(pluginSource)
125             pluginSubDir = pluginSubDir.replace(self.sourcePluginsPath, '').strip('/')
126             try:
127                 os.mkdir("%s/%s" % (self.pluginDir, pluginSubDir))
128             except OSError:
129                 pass
130
131             os.system('cp "%s" "%s/%s"' % (pluginSource, self.pluginDir, pluginSubDir))
132
133             self.resolveDependancies("%s/%s/%s" % (self.pluginDir, pluginSubDir, pluginName))
134
135     def installQtConf(self):
136         qtConfName = self.appDir + "/qt.conf"
137         qtConfContent = QT_CONFIG_NOBUNDLE
138         if self.bundle:
139             qtConfContent = QT_CONFIG
140             qtConfName = self.appDir + "/Resources/qt.conf"
141
142         qtConf = open(qtConfName, 'w')
143         qtConf.write(qtConfContent)
144         qtConf.close()
145
146     def resolveDependancies(self, obj):
147         # obj must be either an application binary or a framework library
148         #print "resolving deps for:", obj
149         for framework, lib in self.determineDependancies(obj):
150             self.installFramework(framework)
151             self.changeDylPath(obj, framework, lib)
152
153     def installFramework(self, framework):
154         # skip if framework is already installed.
155         if framework in self.installedFrameworks:
156             return
157
158         self.installedFrameworks.add(framework)
159
160         # ensure that the framework directory exists
161         try:
162             os.mkdir(self.frameworkDir)
163         except:
164             pass
165
166         if not framework.startswith('/'):
167             framework = "%s/%s" % (self.sourceFrameworkPath, framework)
168
169         # Copy Framework
170         os.system('cp -R "%s" "%s"' % (framework, self.frameworkDir))
171
172         frameworkname = framework.split('/')[-1]
173         localframework = self.frameworkDir + "/" + frameworkname
174
175         # De-Myllify
176         os.system('find "%s" -name *debug* -exec rm -f {} \;' % localframework)
177         os.system('find "%s" -name Headers -exec rm -rf {} \; 2>/dev/null' % localframework)
178
179         # Install new Lib ID and Change Path to Frameworks for the Dynamic linker
180         for lib in os.listdir(localframework + "/Versions/Current"):
181             lib = "%s/Versions/Current/%s" % (localframework, lib)
182             otoolProcess = Popen('otool -D "%s"' % lib, shell=True, stdout=PIPE, stderr=PIPE)
183             try:
184                 libname = [line for line in otoolProcess.stdout][1].strip()
185             except:
186                 libname = ''
187             otoolProcess.stdout.close()
188             if otoolProcess.wait() == 1: # we found some Resource dir or similar -> skip
189                 continue
190             frameworkpath, libpath = libname.split(frameworkname)
191             if self.bundle:
192                 newlibname = "@executable_path/../%s%s" % (frameworkname, libpath)
193             else:
194                 newlibname = "@executable_path/%s%s" % (frameworkname, libpath)
195             #print 'install_name_tool -id "%s" "%s"' % (newlibname, lib)
196             os.system('install_name_tool -id "%s" "%s"' % (newlibname, lib))
197
198             self.resolveDependancies(lib)
199
200     def determineDependancies(self, app):
201         otoolPipe = Popen('otool -L "%s"' % app, shell=True, stdout=PIPE).stdout
202         otoolOutput = [line for line in otoolPipe]
203         otoolPipe.close()
204         libs = [line.split()[0] for line in otoolOutput[1:] if ("Qt" in line
205                                                                or "phonon" in line)
206                                                                and not "@executable_path" in line]
207         frameworks = [lib[:lib.find(".framework")+len(".framework")] for lib in libs]
208         frameworks = [framework[framework.rfind('/')+1:] for framework in frameworks]
209         return zip(frameworks, libs)
210
211
212     def changeDylPath(self, obj, framework, lib):
213         newlibname = framework + lib.split(framework)[1]
214         if self.bundle:
215             newlibname = "@executable_path/../Frameworks/%s" % newlibname
216         else:
217             newlibname = "@executable_path/Frameworks/%s" % newlibname
218
219         #print 'install_name_tool -change "%s" "%s" "%s"' % (lib, newlibname, obj)
220         os.system('install_name_tool -change "%s" "%s" "%s"' % (lib, newlibname, obj))
221
222 if __name__ == "__main__":
223     if len(sys.argv) < 2:
224         print "Wrong Argument Count (Syntax: %s [--nobundle] [--plugins=plugin1,plugin2,...] $TARGET_APP)" % sys.argv[0]
225         sys.exit(1)
226     else:
227         bundle = True
228         plugins = []
229         offset = 1
230
231         while offset < len(sys.argv) and sys.argv[offset].startswith("--"):
232             if sys.argv[offset] == "--nobundle":
233                 bundle = False
234
235             if sys.argv[offset].startswith("--plugins="):
236                 plugins = sys.argv[offset].split('=')[1].split(',')
237
238             offset += 1
239
240         targetDir = sys.argv[offset]
241         if bundle:
242             targetDir += "/Contents"
243
244         InstallQt(targetDir, bundle, plugins)