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