modernize: Replace most remaining old-style connects by PMF ones
[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 class InstallQt(object):
36     def __init__(self, appdir, bundle=True, requestedPlugins=[], skipInstallQtConf=False):
37         self.appDir = appdir
38         self.bundle = bundle
39         self.frameworkDir = self.appDir + "/Frameworks"
40         self.pluginDir = self.appDir + "/plugins"
41         self.executableDir = self.appDir
42         if bundle:
43             self.executableDir += "/MacOS"
44
45         self.installedFrameworks = set()
46
47         self.findFrameworkPath()
48
49         executables = [self.executableDir + "/" + executable for executable in os.listdir(self.executableDir)]
50         for executable in executables:
51             self.resolveDependancies(executable)
52
53         self.findPluginsPath()
54         self.installPlugins(requestedPlugins)
55         if not skipInstallQtConf:
56             self.installQtConf()
57
58     def qtProperty(self, qtProperty):
59         """
60         Query persistent property of Qt via qmake
61         """
62         VALID_PROPERTIES = ['QT_INSTALL_PREFIX',
63                             'QT_INSTALL_DATA',
64                             'QT_INSTALL_DOCS',
65                             'QT_INSTALL_HEADERS',
66                             'QT_INSTALL_LIBS',
67                             'QT_INSTALL_BINS',
68                             'QT_INSTALL_PLUGINS',
69                             'QT_INSTALL_IMPORTS',
70                             'QT_INSTALL_TRANSLATIONS',
71                             'QT_INSTALL_CONFIGURATION',
72                             'QT_INSTALL_EXAMPLES',
73                             'QT_INSTALL_DEMOS',
74                             'QMAKE_MKSPECS',
75                             'QMAKE_VERSION',
76                             'QT_VERSION'
77                             ]
78         if qtProperty not in VALID_PROPERTIES:
79             return None
80
81         qmakeProcess = Popen('qmake -query %s' % qtProperty, shell=True, stdout=PIPE, stderr=PIPE)
82         result = qmakeProcess.stdout.read().strip()
83         qmakeProcess.stdout.close()
84         qmakeProcess.wait()
85         return result
86
87     def findFrameworkPath(self):
88         self.sourceFrameworkPath = self.qtProperty('QT_INSTALL_LIBS')
89
90     def findPluginsPath(self):
91         self.sourcePluginsPath = self.qtProperty('QT_INSTALL_PLUGINS')
92
93     def findPlugin(self, pluginname):
94         qmakeProcess = Popen('find %s -name %s' % (self.sourcePluginsPath, pluginname), shell=True, stdout=PIPE, stderr=PIPE)
95         result = qmakeProcess.stdout.read().strip()
96         qmakeProcess.stdout.close()
97         qmakeProcess.wait()
98         if not result:
99             raise OSError
100         return result
101
102     def installPlugins(self, requestedPlugins):
103         try:
104             os.mkdir(self.pluginDir)
105         except:
106             pass
107
108         for plugin in requestedPlugins:
109             if not plugin.isalnum():
110                 print "Skipping library '%s'..." % plugin
111                 continue
112
113             pluginName = "lib%s.dylib" % plugin
114             pluginSource = ''
115             try:
116                 pluginSource = self.findPlugin(pluginName)
117             except OSError:
118                 print "WARNING: Requested library does not exist: '%s'" % plugin
119                 continue
120
121             pluginSubDir = os.path.dirname(pluginSource)
122             pluginSubDir = pluginSubDir.replace(self.sourcePluginsPath, '').strip('/')
123             try:
124                 os.mkdir("%s/%s" % (self.pluginDir, pluginSubDir))
125             except OSError:
126                 pass
127
128             os.system('cp "%s" "%s/%s"' % (pluginSource, self.pluginDir, pluginSubDir))
129
130             self.resolveDependancies("%s/%s/%s" % (self.pluginDir, pluginSubDir, pluginName))
131
132     def installQtConf(self):
133         qtConfName = self.appDir + "/qt.conf"
134         qtConfContent = QT_CONFIG_NOBUNDLE
135         if self.bundle:
136             qtConfContent = QT_CONFIG
137             qtConfName = self.appDir + "/Resources/qt.conf"
138
139         qtConf = open(qtConfName, 'w')
140         qtConf.write(qtConfContent)
141         qtConf.close()
142
143     def resolveDependancies(self, obj):
144         # obj must be either an application binary or a framework library
145         # print "resolving deps for:", obj
146         for framework, lib in self.determineDependancies(obj):
147             self.installFramework(framework)
148             self.changeDylPath(obj, framework, lib)
149
150     def installFramework(self, framework):
151         # skip if framework is already installed.
152         if framework in self.installedFrameworks:
153             return
154
155         self.installedFrameworks.add(framework)
156
157         # if the Framework-Folder is a Symlink we are in a Helper-Process ".app" (e.g. in QtWebEngine),
158         # in this case skip copying/installing on existing folders
159         skipExisting = False;
160         if os.path.islink(self.frameworkDir):
161             skipExisting = True;
162
163         # ensure that the framework directory exists
164         try:
165             os.mkdir(self.frameworkDir)
166         except:
167             pass
168
169         if not framework.startswith('/'):
170             framework = "%s/%s" % (self.sourceFrameworkPath, framework)
171
172         frameworkname = framework.split('/')[-1]
173         localframework = self.frameworkDir + "/" + frameworkname
174
175         # Framework already installed in previous run ... see above
176         if skipExisting and os.path.isdir(localframework):
177             return
178
179         # Copy Framework
180         os.system('cp -R "%s" "%s"' % (framework, self.frameworkDir))
181
182         # De-Myllify
183         os.system('find "%s" -name *debug* -exec rm -f {} \;' % localframework)
184         os.system('find "%s" -name Headers -exec rm -rf {} \; 2>/dev/null' % localframework)
185
186         # Install new Lib ID and Change Path to Frameworks for the Dynamic linker
187         for lib in os.listdir(localframework + "/Versions/Current"):
188             lib = "%s/Versions/Current/%s" % (localframework, lib)
189             otoolProcess = Popen('otool -D "%s"' % lib, shell=True, stdout=PIPE, stderr=PIPE)
190             try:
191                 libname = [line for line in otoolProcess.stdout][1].strip()
192             except:
193                 libname = ''
194             otoolProcess.stdout.close()
195             if otoolProcess.wait() == 1:  # we found some Resource dir or similar -> skip
196                 continue
197             frameworkpath, libpath = libname.split(frameworkname)
198             if self.bundle:
199                 newlibname = "@executable_path/../%s%s" % (frameworkname, libpath)
200             else:
201                 newlibname = "@executable_path/%s%s" % (frameworkname, libpath)
202             # print 'install_name_tool -id "%s" "%s"' % (newlibname, lib)
203             os.system('chmod +w "%s"' % (lib))
204             os.system('install_name_tool -id "%s" "%s"' % (newlibname, lib))
205
206             self.resolveDependancies(lib)
207
208     def determineDependancies(self, app):
209         otoolPipe = Popen('otool -L "%s"' % app, shell=True, stdout=PIPE).stdout
210         otoolOutput = [line for line in otoolPipe]
211         otoolPipe.close()
212         libs = [line.split()[0] for line in otoolOutput[1:] if ("Qt" in line or "phonon" in line) and "@executable_path" not in line]
213         frameworks = [lib[:lib.find(".framework") + len(".framework")] for lib in libs]
214         frameworks = [framework[framework.rfind('/') + 1:] for framework in frameworks]
215         return zip(frameworks, libs)
216
217     def changeDylPath(self, obj, framework, lib):
218         newlibname = framework + lib.split(framework)[1]
219         if self.bundle:
220             newlibname = "@executable_path/../Frameworks/%s" % newlibname
221         else:
222             newlibname = "@executable_path/Frameworks/%s" % newlibname
223
224         # print 'install_name_tool -change "%s" "%s" "%s"' % (lib, newlibname, obj)
225         os.system('chmod +w "%s"' % (lib))
226         os.system('chmod +w "%s"' % (obj))
227         os.system('install_name_tool -change "%s" "%s" "%s"' % (lib, newlibname, obj))
228
229 if __name__ == "__main__":
230     if len(sys.argv) < 2:
231         print "Wrong Argument Count (Syntax: %s [--nobundle] [--plugins=plugin1,plugin2,...] $TARGET_APP)" % sys.argv[0]
232         sys.exit(1)
233     else:
234         bundle = True
235         plugins = []
236         offset = 1
237
238         while offset < len(sys.argv) and sys.argv[offset].startswith("--"):
239             if sys.argv[offset] == "--nobundle":
240                 bundle = False
241
242             if sys.argv[offset].startswith("--plugins="):
243                 plugins = sys.argv[offset].split('=')[1].split(',')
244
245             offset += 1
246
247         targetDir = sys.argv[offset]
248         if bundle:
249             targetDir += "/Contents"
250
251         InstallQt(targetDir, bundle, plugins)
252
253         if bundle:
254             webenginetarget = targetDir + '/Frameworks/QtWebEngineCore.framework/Helpers/QtWebEngineProcess.app/Contents'
255
256             if os.path.isdir(webenginetarget):
257                 os.system('ln -s ../../../../../../ "%s"/Frameworks' % webenginetarget)
258                 InstallQt(webenginetarget, bundle, [], True)