1#!/usr/local/bin/python3.8
2################################################################################
3# Copyright (C) The Qt Company Ltd.
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions are met:
8#
9#   * Redistributions of source code must retain the above copyright notice,
10#     this list of conditions and the following disclaimer.
11#   * Redistributions in binary form must reproduce the above copyright notice,
12#     this list of conditions and the following disclaimer in the documentation
13#     and/or other materials provided with the distribution.
14#   * Neither the name of The Qt Company Ltd, nor the names of its contributors
15#     may be used to endorse or promote products derived from this software
16#     without specific prior written permission.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
22# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28################################################################################
29
30import argparse
31import collections
32import os
33import locale
34import sys
35import subprocess
36import re
37import shutil
38from glob import glob
39
40import common
41
42debug_build = False
43encoding = locale.getdefaultlocale()[1]
44
45def get_args():
46    parser = argparse.ArgumentParser(description='Deploy Qt Creator dependencies for packaging')
47    parser.add_argument('-i', '--ignore-errors', help='For backward compatibility',
48                        action='store_true', default=False)
49    parser.add_argument('--elfutils-path',
50                        help='Path to elfutils installation for use by perfprofiler (Windows, Linux)')
51    # TODO remove defaulting to LLVM_INSTALL_DIR when we no longer build qmake based packages
52    parser.add_argument('--llvm-path',
53                        help='Path to LLVM installation',
54                        default=os.environ.get('LLVM_INSTALL_DIR'))
55    parser.add_argument('qtcreator_binary', help='Path to Qt Creator binary (or the app bundle on macOS)')
56    parser.add_argument('qmake_binary', help='Path to qmake binary')
57
58    args = parser.parse_args()
59
60    args.qtcreator_binary = os.path.abspath(args.qtcreator_binary)
61    if common.is_mac_platform():
62        if not args.qtcreator_binary.lower().endswith(".app"):
63            args.qtcreator_binary = args.qtcreator_binary + ".app"
64        check = os.path.isdir
65    else:
66        check = os.path.isfile
67        if common.is_windows_platform() and not args.qtcreator_binary.lower().endswith(".exe"):
68            args.qtcreator_binary = args.qtcreator_binary + ".exe"
69
70    if not check(args.qtcreator_binary):
71        print('Cannot find Qt Creator binary.')
72        sys.exit(1)
73
74    args.qmake_binary = which(args.qmake_binary)
75    if not args.qmake_binary:
76        print('Cannot find qmake binary.')
77        sys.exit(2)
78
79    return args
80
81def usage():
82    print("Usage: %s <existing_qtcreator_binary> [qmake_path]" % os.path.basename(sys.argv[0]))
83
84def which(program):
85    def is_exe(fpath):
86        return os.path.exists(fpath) and os.access(fpath, os.X_OK)
87
88    fpath = os.path.dirname(program)
89    if fpath:
90        if is_exe(program):
91            return program
92        if common.is_windows_platform():
93            if is_exe(program + ".exe"):
94                return program  + ".exe"
95    else:
96        for path in os.environ["PATH"].split(os.pathsep):
97            exe_file = os.path.join(path, program)
98            if is_exe(exe_file):
99                return exe_file
100            if common.is_windows_platform():
101                if is_exe(exe_file + ".exe"):
102                    return exe_file  + ".exe"
103
104    return None
105
106def is_debug(fpath):
107    # match all Qt Core dlls from Qt4, Qt5beta2 and Qt5rc1 and later
108    # which all have the number at different places
109    coredebug = re.compile(r'Qt[1-9]?Core[1-9]?d[1-9]?.dll')
110    # bootstrap exception
111    if coredebug.search(fpath):
112        return True
113    # try to use dumpbin (MSVC) or objdump (MinGW), otherwise ship all .dlls
114    if which('dumpbin'):
115        output = subprocess.check_output(['dumpbin', '/imports', fpath])
116    elif which('objdump'):
117        output = subprocess.check_output(['objdump', '-p', fpath])
118    else:
119        return debug_build
120    return coredebug.search(output.decode(encoding)) != None
121
122def is_ignored_windows_file(use_debug, basepath, filename):
123    ignore_patterns = ['.lib', '.pdb', '.exp', '.ilk']
124    if use_debug:
125        ignore_patterns.extend(['libEGL.dll', 'libGLESv2.dll'])
126    else:
127        ignore_patterns.extend(['libEGLd.dll', 'libGLESv2d.dll'])
128    for ip in ignore_patterns:
129        if filename.endswith(ip):
130            return True
131    if filename.endswith('.dll'):
132        filepath = os.path.join(basepath, filename)
133        if use_debug != is_debug(filepath):
134            return True
135    return False
136
137def ignored_qt_lib_files(path, filenames):
138    if not common.is_windows_platform():
139        return []
140    return [fn for fn in filenames if is_ignored_windows_file(debug_build, path, fn)]
141
142def copy_qt_libs(target_qt_prefix_path, qt_bin_dir, qt_libs_dir, qt_plugin_dir, qt_qml_dir, plugins):
143    print("copying Qt libraries...")
144
145    if common.is_windows_platform():
146        libraries = glob(os.path.join(qt_libs_dir, '*.dll'))
147    else:
148        libraries = glob(os.path.join(qt_libs_dir, '*.so.*'))
149
150    if common.is_windows_platform():
151        lib_dest = os.path.join(target_qt_prefix_path)
152    else:
153        lib_dest = os.path.join(target_qt_prefix_path, 'lib')
154
155    if not os.path.exists(lib_dest):
156        os.makedirs(lib_dest)
157
158    if common.is_windows_platform():
159        libraries = [lib for lib in libraries if not is_ignored_windows_file(debug_build, '', lib)]
160
161    for library in libraries:
162        print(library, '->', lib_dest)
163        if os.path.islink(library):
164            linkto = os.readlink(library)
165            try:
166                os.symlink(linkto, os.path.join(lib_dest, os.path.basename(library)))
167            except OSError:
168                pass
169        else:
170            shutil.copy(library, lib_dest)
171
172    print("Copying plugins:", plugins)
173    for plugin in plugins:
174        target = os.path.join(target_qt_prefix_path, 'plugins', plugin)
175        if (os.path.exists(target)):
176            shutil.rmtree(target)
177        pluginPath = os.path.join(qt_plugin_dir, plugin)
178        if (os.path.exists(pluginPath)):
179            print('{0} -> {1}'.format(pluginPath, target))
180            common.copytree(pluginPath, target, ignore=ignored_qt_lib_files, symlinks=True)
181
182    if (os.path.exists(qt_qml_dir)):
183        print("Copying qt quick 2 imports")
184        target = os.path.join(target_qt_prefix_path, 'qml')
185        if (os.path.exists(target)):
186            shutil.rmtree(target)
187        print('{0} -> {1}'.format(qt_qml_dir, target))
188        common.copytree(qt_qml_dir, target, ignore=ignored_qt_lib_files, symlinks=True)
189
190    print("Copying qtdiag")
191    bin_dest = target_qt_prefix_path if common.is_windows_platform() else os.path.join(target_qt_prefix_path, 'bin')
192    qtdiag_src = os.path.join(qt_bin_dir, 'qtdiag.exe' if common.is_windows_platform() else 'qtdiag')
193    if not os.path.exists(bin_dest):
194        os.makedirs(bin_dest)
195    shutil.copy(qtdiag_src, bin_dest)
196
197
198def add_qt_conf(target_path, qt_prefix_path):
199    qtconf_filepath = os.path.join(target_path, 'qt.conf')
200    prefix_path = os.path.relpath(qt_prefix_path, target_path).replace('\\', '/')
201    print('Creating qt.conf in "{0}":'.format(qtconf_filepath))
202    f = open(qtconf_filepath, 'w')
203    f.write('[Paths]\n')
204    f.write('Prefix={0}\n'.format(prefix_path))
205    f.write('Binaries={0}\n'.format('bin' if common.is_linux_platform() else '.'))
206    f.write('Libraries={0}\n'.format('lib' if common.is_linux_platform() else '.'))
207    f.write('Plugins=plugins\n')
208    f.write('Qml2Imports=qml\n')
209    f.close()
210
211def copy_translations(install_dir, qt_tr_dir):
212    translations = glob(os.path.join(qt_tr_dir, '*.qm'))
213    tr_dir = os.path.join(install_dir, 'share', 'qtcreator', 'translations')
214
215    print("copying translations...")
216    for translation in translations:
217        print(translation, '->', tr_dir)
218        shutil.copy(translation, tr_dir)
219
220def copyPreservingLinks(source, destination):
221    if os.path.islink(source):
222        linkto = os.readlink(source)
223        destFilePath = destination
224        if os.path.isdir(destination):
225            destFilePath = os.path.join(destination, os.path.basename(source))
226        os.symlink(linkto, destFilePath)
227    else:
228        shutil.copy(source, destination)
229
230def deploy_libclang(install_dir, llvm_install_dir, chrpath_bin):
231    # contains pairs of (source, target directory)
232    deployinfo = []
233    resourcesource = os.path.join(llvm_install_dir, 'lib', 'clang')
234    if common.is_windows_platform():
235        clangbindirtarget = os.path.join(install_dir, 'bin', 'clang', 'bin')
236        if not os.path.exists(clangbindirtarget):
237            os.makedirs(clangbindirtarget)
238        clanglibdirtarget = os.path.join(install_dir, 'bin', 'clang', 'lib')
239        if not os.path.exists(clanglibdirtarget):
240            os.makedirs(clanglibdirtarget)
241        deployinfo.append((os.path.join(llvm_install_dir, 'bin', 'libclang.dll'),
242                           os.path.join(install_dir, 'bin')))
243        for binary in ['clang', 'clang-cl', 'clangd', 'clang-tidy', 'clazy-standalone']:
244            binary_filepath = os.path.join(llvm_install_dir, 'bin', binary + '.exe')
245            if os.path.exists(binary_filepath):
246                deployinfo.append((binary_filepath, clangbindirtarget))
247        resourcetarget = os.path.join(clanglibdirtarget, 'clang')
248    else:
249        # libclang -> Qt Creator libraries
250        libsources = glob(os.path.join(llvm_install_dir, 'lib', 'libclang.so*'))
251        for libsource in libsources:
252            deployinfo.append((libsource, os.path.join(install_dir, 'lib', 'qtcreator')))
253        # clang binaries -> clang libexec
254        clangbinary_targetdir = os.path.join(install_dir, 'libexec', 'qtcreator', 'clang', 'bin')
255        if not os.path.exists(clangbinary_targetdir):
256            os.makedirs(clangbinary_targetdir)
257        for binary in ['clang', 'clangd', 'clang-tidy', 'clazy-standalone']:
258            binary_filepath = os.path.join(llvm_install_dir, 'bin', binary)
259            if os.path.exists(binary_filepath):
260                deployinfo.append((binary_filepath, clangbinary_targetdir))
261                # add link target if binary is actually a symlink (to a binary in the same directory)
262                if os.path.islink(binary_filepath):
263                    linktarget = os.readlink(binary_filepath)
264                    deployinfo.append((os.path.join(os.path.dirname(binary_filepath), linktarget),
265                                       os.path.join(clangbinary_targetdir, linktarget)))
266        clanglibs_targetdir = os.path.join(install_dir, 'libexec', 'qtcreator', 'clang', 'lib')
267        # support libraries (for clazy) -> clang libexec
268        if not os.path.exists(clanglibs_targetdir):
269            os.makedirs(clanglibs_targetdir)
270        # on RHEL ClazyPlugin is in lib64
271        for lib_pattern in ['lib64/ClazyPlugin.so', 'lib/ClazyPlugin.so', 'lib/libclang-cpp.so*']:
272            for lib in glob(os.path.join(llvm_install_dir, lib_pattern)):
273                deployinfo.append((lib, clanglibs_targetdir))
274        resourcetarget = os.path.join(install_dir, 'libexec', 'qtcreator', 'clang', 'lib', 'clang')
275
276    print("copying libclang...")
277    for source, target in deployinfo:
278        print(source, '->', target)
279        copyPreservingLinks(source, target)
280
281    if common.is_linux_platform():
282        # libclang was statically compiled, so there is no need for the RPATHs
283        # and they are confusing when fixing RPATHs later in the process.
284        # Also fix clazy-standalone RPATH.
285        print("fixing Clang RPATHs...")
286        for source, target in deployinfo:
287            filename = os.path.basename(source)
288            targetfilepath = target if not os.path.isdir(target) else os.path.join(target, filename)
289            if filename == 'clazy-standalone':
290                subprocess.check_call([chrpath_bin, '-r', '$ORIGIN/../lib', targetfilepath])
291            elif not os.path.islink(target):
292                targetfilepath = target if not os.path.isdir(target) else os.path.join(target, os.path.basename(source))
293                subprocess.check_call([chrpath_bin, '-d', targetfilepath])
294
295    print(resourcesource, '->', resourcetarget)
296    if (os.path.exists(resourcetarget)):
297        shutil.rmtree(resourcetarget)
298    common.copytree(resourcesource, resourcetarget, symlinks=True)
299
300def deploy_elfutils(qtc_install_dir, chrpath_bin, args):
301    if common.is_mac_platform():
302        return
303
304    def lib_name(name, version):
305        return ('lib' + name + '.so.' + version if common.is_linux_platform()
306                else name + '.dll')
307
308    version = '1'
309    libs = ['elf', 'dw']
310    elfutils_lib_path = os.path.join(args.elfutils_path, 'lib')
311    if common.is_linux_platform():
312        install_path = os.path.join(qtc_install_dir, 'lib', 'elfutils')
313        backends_install_path = install_path
314    elif common.is_windows_platform():
315        install_path = os.path.join(qtc_install_dir, 'bin')
316        backends_install_path = os.path.join(qtc_install_dir, 'lib', 'elfutils')
317        libs.append('eu_compat')
318    if not os.path.exists(install_path):
319        os.makedirs(install_path)
320    if not os.path.exists(backends_install_path):
321        os.makedirs(backends_install_path)
322    # copy main libs
323    libs = [os.path.join(elfutils_lib_path, lib_name(lib, version)) for lib in libs]
324    for lib in libs:
325        print(lib, '->', install_path)
326        shutil.copy(lib, install_path)
327    # fix rpath
328    if common.is_linux_platform():
329        relative_path = os.path.relpath(backends_install_path, install_path)
330        subprocess.check_call([chrpath_bin, '-r', os.path.join('$ORIGIN', relative_path),
331                               os.path.join(install_path, lib_name('dw', version))])
332    # copy backend files
333    # only non-versioned, we never dlopen the versioned ones
334    files = glob(os.path.join(elfutils_lib_path, 'elfutils', '*ebl_*.*'))
335    versioned_files = glob(os.path.join(elfutils_lib_path, 'elfutils', '*ebl_*.*-*.*.*'))
336    unversioned_files = [file for file in files if file not in versioned_files]
337    for file in unversioned_files:
338        print(file, '->', backends_install_path)
339        shutil.copy(file, backends_install_path)
340
341def deploy_mac(args):
342    (_, qt_install) = get_qt_install_info(args.qmake_binary)
343
344    env = dict(os.environ)
345    if args.llvm_path:
346        env['LLVM_INSTALL_DIR'] = args.llvm_path
347
348    script_path = os.path.dirname(os.path.realpath(__file__))
349    deployqtHelper_mac = os.path.join(script_path, 'deployqtHelper_mac.sh')
350    common.check_print_call([deployqtHelper_mac, args.qtcreator_binary, qt_install.bin,
351                             qt_install.translations, qt_install.plugins, qt_install.qml],
352                            env=env)
353
354def get_qt_install_info(qmake_binary):
355    qt_install_info = common.get_qt_install_info(qmake_binary)
356    QtInstallInfo = collections.namedtuple('QtInstallInfo', ['bin', 'lib', 'plugins',
357                                                             'qml', 'translations'])
358    return (qt_install_info,
359            QtInstallInfo(bin=qt_install_info['QT_INSTALL_BINS'],
360                          lib=qt_install_info['QT_INSTALL_LIBS'],
361                          plugins=qt_install_info['QT_INSTALL_PLUGINS'],
362                          qml=qt_install_info['QT_INSTALL_QML'],
363                          translations=qt_install_info['QT_INSTALL_TRANSLATIONS']))
364
365def main():
366    args = get_args()
367    if common.is_mac_platform():
368        deploy_mac(args)
369        return
370
371    (qt_install_info, qt_install) = get_qt_install_info(args.qmake_binary)
372
373    qtcreator_binary_path = os.path.dirname(args.qtcreator_binary)
374    install_dir = os.path.abspath(os.path.join(qtcreator_binary_path, '..'))
375    if common.is_linux_platform():
376        qt_deploy_prefix = os.path.join(install_dir, 'lib', 'Qt')
377    else:
378        qt_deploy_prefix = os.path.join(install_dir, 'bin')
379
380    chrpath_bin = None
381    if common.is_linux_platform():
382        chrpath_bin = which('chrpath')
383        if chrpath_bin == None:
384            print("Cannot find required binary 'chrpath'.")
385            sys.exit(2)
386
387    plugins = ['assetimporters', 'accessible', 'codecs', 'designer', 'iconengines', 'imageformats', 'platformthemes',
388               'platforminputcontexts', 'platforms', 'printsupport', 'qmltooling', 'sqldrivers', 'styles',
389               'xcbglintegrations',
390               'wayland-decoration-client',
391               'wayland-graphics-integration-client',
392               'wayland-shell-integration',
393               'tls'
394               ]
395
396    if common.is_windows_platform():
397        global debug_build
398        debug_build = is_debug(args.qtcreator_binary)
399
400    if common.is_windows_platform():
401        copy_qt_libs(qt_deploy_prefix, qt_install.bin, qt_install.bin, qt_install.plugins, qt_install.qml, plugins)
402    else:
403        copy_qt_libs(qt_deploy_prefix, qt_install.bin, qt_install.lib, qt_install.plugins, qt_install.qml, plugins)
404    copy_translations(install_dir, qt_install.translations)
405    if args.llvm_path:
406        deploy_libclang(install_dir, args.llvm_path, chrpath_bin)
407
408    if args.elfutils_path:
409        deploy_elfutils(install_dir, chrpath_bin, args)
410    if not common.is_windows_platform():
411        print("fixing rpaths...")
412        common.fix_rpaths(install_dir, os.path.join(qt_deploy_prefix, 'lib'), qt_install_info, chrpath_bin)
413        add_qt_conf(os.path.join(install_dir, 'libexec', 'qtcreator'), qt_deploy_prefix) # e.g. for qml2puppet
414        add_qt_conf(os.path.join(qt_deploy_prefix, 'bin'), qt_deploy_prefix) # e.g. qtdiag
415    add_qt_conf(os.path.join(install_dir, 'bin'), qt_deploy_prefix)
416
417if __name__ == "__main__":
418    main()
419