diff options
Diffstat (limited to 'build_scripts/utils.py')
-rw-r--r-- | build_scripts/utils.py | 1075 |
1 files changed, 1075 insertions, 0 deletions
diff --git a/build_scripts/utils.py b/build_scripts/utils.py new file mode 100644 index 000000000..e69c9a58a --- /dev/null +++ b/build_scripts/utils.py @@ -0,0 +1,1075 @@ +############################################################################# +## +## Copyright (C) 2018 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the Qt for Python project. +## +## $QT_BEGIN_LICENSE:LGPL$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU Lesser General Public License Usage +## Alternatively, this file may be used under the terms of the GNU Lesser +## General Public License version 3 as published by the Free Software +## Foundation and appearing in the file LICENSE.LGPL3 included in the +## packaging of this file. Please review the following information to +## ensure the GNU Lesser General Public License version 3 requirements +## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 2.0 or (at your option) the GNU General +## Public license version 3 or any later version approved by the KDE Free +## Qt Foundation. The licenses are as published by the Free Software +## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-2.0.html and +## https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +import sys +import os +import re +import stat +import errno +import time +import shutil +import subprocess +import fnmatch +import glob +import itertools +import popenasync +import glob + +# There is no urllib.request in Python2 +try: + import urllib.request as urllib +except ImportError: + import urllib + +from distutils import log +from distutils.errors import DistutilsOptionError +from distutils.errors import DistutilsSetupError +from distutils.spawn import spawn +from distutils.spawn import DistutilsExecError + +try: + WindowsError +except NameError: + WindowsError = None + + +def has_option(name): + try: + sys.argv.remove("--{}".format(name)) + return True + except ValueError: + pass + return False + + +def option_value(name): + for index, option in enumerate(sys.argv): + if option == '--' + name: + if index+1 >= len(sys.argv): + raise DistutilsOptionError("The option {} requires a " + "value".format(option)) + value = sys.argv[index+1] + sys.argv[index:index+2] = [] + return value + if option.startswith('--' + name + '='): + value = option[len(name)+3:] + sys.argv[index:index+1] = [] + return value + env_val = os.getenv(name.upper().replace('-', '_')) + return env_val + + +def filter_match(name, patterns): + for pattern in patterns: + if pattern is None: + continue + if fnmatch.fnmatch(name, pattern): + return True + return False + + +def update_env_path(newpaths): + paths = os.environ['PATH'].lower().split(os.pathsep) + for path in newpaths: + if not path.lower() in paths: + log.info("Inserting path '{}' to environment".format(path)) + paths.insert(0, path) + os.environ['PATH'] = path + os.pathsep + os.environ['PATH'] + + +def winsdk_setenv(platform_arch, build_type): + from distutils.msvc9compiler import VERSION as MSVC_VERSION + from distutils.msvc9compiler import Reg + from distutils.msvc9compiler import HKEYS + from distutils.msvc9compiler import WINSDK_BASE + + sdk_version_map = { + "v6.0a": 9.0, + "v6.1": 9.0, + "v7.0": 9.0, + "v7.0a": 10.0, + "v7.1": 10.0 + } + + log.info("Searching Windows SDK with MSVC compiler version {}".format( + MSVC_VERSION)) + setenv_paths = [] + for base in HKEYS: + sdk_versions = Reg.read_keys(base, WINSDK_BASE) + if sdk_versions: + for sdk_version in sdk_versions: + installationfolder = Reg.get_value(WINSDK_BASE + "\\" + + sdk_version, "installationfolder") + productversion = Reg.get_value(WINSDK_BASE + "\\" + + sdk_version, "productversion") + setenv_path = os.path.join(installationfolder, os.path.join( + 'bin', 'SetEnv.cmd')) + if not os.path.exists(setenv_path): + continue + if not sdk_version in sdk_version_map: + continue + if sdk_version_map[sdk_version] != MSVC_VERSION: + continue + setenv_paths.append(setenv_path) + if len(setenv_paths) == 0: + raise DistutilsSetupError( + "Failed to find the Windows SDK with MSVC compiler " + "version {}".format(MSVC_VERSION)) + for setenv_path in setenv_paths: + log.info("Found {}".format(setenv_path)) + + # Get SDK env (use latest SDK version installed on system) + setenv_path = setenv_paths[-1] + log.info("Using {} ".format(setenv_path)) + build_arch = "/x86" if platform_arch.startswith("32") else "/x64" + build_type = "/Debug" if build_type.lower() == "debug" else "/Release" + setenv_cmd = [setenv_path, build_arch, build_type] + setenv_env = get_environment_from_batch_command(setenv_cmd) + setenv_env_paths = os.pathsep.join([setenv_env[k] for k in setenv_env if k.upper() == 'PATH']).split(os.pathsep) + setenv_env_without_paths = dict([(k, setenv_env[k]) for k in setenv_env if k.upper() != 'PATH']) + + # Extend os.environ with SDK env + log.info("Initializing Windows SDK env...") + update_env_path(setenv_env_paths) + for k in sorted(setenv_env_without_paths): + v = setenv_env_without_paths[k] + log.info("Inserting '{} = {}' to environment".format(k, v)) + os.environ[k] = v + log.info("Done initializing Windows SDK env") + + +def find_vcdir(version): + """ + This is the customized version of + distutils.msvc9compiler.find_vcvarsall method + """ + from distutils.msvc9compiler import VS_BASE + from distutils.msvc9compiler import Reg + from distutils import log + vsbase = VS_BASE % version + try: + productdir = Reg.get_value(r"{}\Setup\VC".format(vsbase), "productdir") + except KeyError: + productdir = None + + # trying Express edition + if productdir is None: + try: + from distutils.msvc9compiler import VSEXPRESS_BASE + except ImportError: + pass + else: + vsbase = VSEXPRESS_BASE % version + try: + productdir = Reg.get_value(r"{}\Setup\VC".format(vsbase), + "productdir") + except KeyError: + productdir = None + log.debug("Unable to find productdir in registry") + + if not productdir or not os.path.isdir(productdir): + toolskey = "VS%0.f0COMNTOOLS" % version + toolsdir = os.environ.get(toolskey, None) + + if toolsdir and os.path.isdir(toolsdir): + productdir = os.path.join(toolsdir, os.pardir, os.pardir, "VC") + productdir = os.path.abspath(productdir) + if not os.path.isdir(productdir): + log.debug("{} is not a valid directory".format(productdir)) + return None + else: + log.debug("Env var {} is not set or invalid".format(toolskey)) + if not productdir: + log.debug("No productdir found") + return None + return productdir + + +def init_msvc_env(platform_arch, build_type): + from distutils.msvc9compiler import VERSION as MSVC_VERSION + + log.info("Searching MSVC compiler version {}".format(MSVC_VERSION)) + vcdir_path = find_vcdir(MSVC_VERSION) + if not vcdir_path: + raise DistutilsSetupError( + "Failed to find the MSVC compiler version {} on your " + "system.".formar(MSVC_VERSION)) + else: + log.info("Found {}".format(vcdir_path)) + + log.info("Searching MSVC compiler {} environment init script".format( + MSVC_VERSION)) + if platform_arch.startswith("32"): + vcvars_path = os.path.join(vcdir_path, "bin", "vcvars32.bat") + else: + vcvars_path = os.path.join(vcdir_path, "bin", "vcvars64.bat") + if not os.path.exists(vcvars_path): + vcvars_path = os.path.join(vcdir_path, "bin", "amd64", + "vcvars64.bat") + if not os.path.exists(vcvars_path): + vcvars_path = os.path.join(vcdir_path, "bin", "amd64", + "vcvarsamd64.bat") + + if not os.path.exists(vcvars_path): + # MSVC init script not found, try to find and init Windows SDK env + log.error("Failed to find the MSVC compiler environment init script " + "(vcvars.bat) on your system.") + winsdk_setenv(platform_arch, build_type) + return + else: + log.info("Found {}".format(vcvars_path)) + + # Get MSVC env + log.info("Using MSVC {} in {}".format(MSVC_VERSION, vcvars_path)) + msvc_arch = "x86" if platform_arch.startswith("32") else "amd64" + log.info("Getting MSVC env for {} architecture".format(msvc_arch)) + vcvars_cmd = [vcvars_path, msvc_arch] + msvc_env = get_environment_from_batch_command(vcvars_cmd) + msvc_env_paths = os.pathsep.join([msvc_env[k] for k in msvc_env if k.upper() == 'PATH']).split(os.pathsep) + msvc_env_without_paths = dict([(k, msvc_env[k]) for k in msvc_env if k.upper() != 'PATH']) + + # Extend os.environ with MSVC env + log.info("Initializing MSVC env...") + update_env_path(msvc_env_paths) + for k in sorted(msvc_env_without_paths): + v = msvc_env_without_paths[k] + log.info("Inserting '{} = {}' to environment".format(k, v)) + os.environ[k] = v + log.info("Done initializing MSVC env") + + +def copyfile(src, dst, force=True, vars=None, force_copy_symlink=False): + if vars is not None: + src = src.format(**vars) + dst = dst.format(**vars) + + if not os.path.exists(src) and not force: + log.info("**Skiping copy file {} to {}. " + "Source does not exists.".format(src, dst)) + return + + if not os.path.islink(src) or force_copy_symlink: + log.info("Copying file {} to {}.".format(src, dst)) + shutil.copy2(src, dst) + else: + linkTargetPath = os.path.realpath(src) + if os.path.dirname(linkTargetPath) == os.path.dirname(src): + linkTarget = os.path.basename(linkTargetPath) + linkName = os.path.basename(src) + currentDirectory = os.getcwd() + try: + targetDir = dst if os.path.isdir(dst) else os.path.dirname(dst) + os.chdir(targetDir) + if os.path.exists(linkName): + os.remove(linkName) + log.info("Symlinking {} -> {} in {}.".format(linkName, + linkTarget, targetDir)) + os.symlink(linkTarget, linkName) + except OSError: + log.error("{} -> {}: Error creating symlink".format(linkName, + linkTarget)) + finally: + os.chdir(currentDirectory) + else: + log.error("{} -> {}: Can only create symlinks within the same " + "directory".format(src, linkTargetPath)) + + return dst + + +def makefile(dst, content=None, vars=None): + if vars is not None: + if content is not None: + content = content.format(**vars) + dst = dst.format(**vars) + + log.info("Making file {}.".format(dst)) + + dstdir = os.path.dirname(dst) + if not os.path.exists(dstdir): + os.makedirs(dstdir) + + f = open(dst, "wt") + if content is not None: + f.write(content) + f.close() + + +def copydir(src, dst, filter=None, ignore=None, force=True, recursive=True, + vars=None, dir_filter_function=None, file_filter_function=None, + force_copy_symlinks=False): + + if vars is not None: + src = src.format(**vars) + dst = dst.format(**vars) + if filter is not None: + for i in range(len(filter)): + filter[i] = filter[i].format(**vars) + if ignore is not None: + for i in range(len(ignore)): + ignore[i] = ignore[i].format(**vars) + + if not os.path.exists(src) and not force: + log.info("**Skiping copy tree {} to {}. Source does not exists. " + "filter={}. ignore={}.".format(src, dst, filter, ignore)) + return [] + + log.info("Copying tree {} to {}. filter={}. ignore={}.".format(src, dst, + filter, ignore)) + + names = os.listdir(src) + + results = [] + errors = [] + for name in names: + srcname = os.path.join(src, name) + dstname = os.path.join(dst, name) + try: + if os.path.isdir(srcname): + if (dir_filter_function and + not dir_filter_function(name, src, srcname)): + continue + if recursive: + results.extend( + copydir(srcname, dstname, filter, ignore, force, + recursive, vars, dir_filter_function, + file_filter_function, force_copy_symlinks)) + else: + if ((file_filter_function is not None and + not file_filter_function(name, srcname)) or + (filter is not None and + not filter_match(name, filter)) or + (ignore is not None and filter_match(name, ignore))): + continue + if not os.path.exists(dst): + os.makedirs(dst) + results.append(copyfile(srcname, dstname, True, vars, + force_copy_symlinks)) + # catch the Error from the recursive copytree so that we can + # continue with other files + except shutil.Error as err: + errors.extend(err.args[0]) + except EnvironmentError as why: + errors.append((srcname, dstname, str(why))) + try: + if os.path.exists(dst): + shutil.copystat(src, dst) + except OSError as why: + if WindowsError is not None and isinstance(why, WindowsError): + # Copying file access times may fail on Windows + pass + else: + errors.extend((src, dst, str(why))) + if errors: + raise EnvironmentError(errors) + return results + + +def rmtree(dirname): + def handleRemoveReadonly(func, path, exc): + excvalue = exc[1] + if func in (os.rmdir, os.remove) and excvalue.errno == errno.EACCES: + os.chmod(path, stat.S_IRWXU| stat.S_IRWXG| stat.S_IRWXO) # 0777 + func(path) + else: + raise + shutil.rmtree(dirname, ignore_errors=False, onerror=handleRemoveReadonly) + +def run_process_output(args, initial_env=None): + if initial_env is None: + initial_env = os.environ + stdOut = subprocess.Popen(args, env = initial_env, universal_newlines = 1, + stdout=subprocess.PIPE).stdout + result = [] + for rawLine in stdOut.readlines(): + line = rawLine if sys.version_info >= (3,) else rawLine.decode('utf-8') + result.append(line.rstrip()) + return result + +def run_process(args, initial_env=None): + def _log(buffer, checkNewLine=False): + endsWithNewLine = False + if buffer.endswith('\n'): + endsWithNewLine = True + if checkNewLine and buffer.find('\n') == -1: + return buffer + lines = buffer.splitlines() + buffer = '' + if checkNewLine and not endsWithNewLine: + buffer = lines[-1] + lines = lines[:-1] + for line in lines: + log.info(line.rstrip('\r')) + return buffer + _log("Running process in {0}: {1}".format(os.getcwd(), + " ".join([(" " in x and '"{0}"'.format(x) or x) for x in args]))) + + if sys.platform != "win32": + try: + spawn(args) + return 0 + except DistutilsExecError: + return -1 + + shell = False + if sys.platform == "win32": + shell = True + + if initial_env is None: + initial_env = os.environ + + proc = popenasync.Popen(args, + stdin = subprocess.PIPE, + stdout = subprocess.PIPE, + stderr = subprocess.STDOUT, + universal_newlines = 1, + shell = shell, + env = initial_env) + + log_buffer = None; + while proc.poll() is None: + log_buffer = _log(proc.read_async(wait=0.1, e=0)) + if log_buffer: + _log(log_buffer) + + proc.wait() + return proc.returncode + + +def get_environment_from_batch_command(env_cmd, initial=None): + """ + Take a command (either a single command or list of arguments) + and return the environment created after running that command. + Note that if the command must be a batch file or .cmd file, or the + changes to the environment will not be captured. + + If initial is supplied, it is used as the initial environment passed + to the child process. + """ + + def validate_pair(ob): + try: + if not (len(ob) == 2): + print("Unexpected result: {}".format(ob)) + raise ValueError + except: + return False + return True + + def consume(iter): + try: + while True: next(iter) + except StopIteration: + pass + + if not isinstance(env_cmd, (list, tuple)): + env_cmd = [env_cmd] + # construct the command that will alter the environment + env_cmd = subprocess.list2cmdline(env_cmd) + # create a tag so we can tell in the output when the proc is done + tag = 'Done running command' + # construct a cmd.exe command to do accomplish this + cmd = 'cmd.exe /E:ON /V:ON /s /c "{} && echo "{}" && set"'.format(env_cmd, + tag) + # launch the process + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=initial) + # parse the output sent to stdout + lines = proc.stdout + if sys.version_info[0] > 2: + # make sure the lines are strings + make_str = lambda s: s.decode() + lines = map(make_str, lines) + # consume whatever output occurs until the tag is reached + consume(itertools.takewhile(lambda l: tag not in l, lines)) + # define a way to handle each KEY=VALUE line + handle_line = lambda l: l.rstrip().split('=',1) + # parse key/values into pairs + pairs = map(handle_line, lines) + # make sure the pairs are valid + valid_pairs = filter(validate_pair, pairs) + # construct a dictionary of the pairs + result = dict(valid_pairs) + # let the process finish + proc.communicate() + return result + + +def regenerate_qt_resources(src, pyside_rcc_path, pyside_rcc_options): + names = os.listdir(src) + for name in names: + srcname = os.path.join(src, name) + if os.path.isdir(srcname): + regenerate_qt_resources(srcname, + pyside_rcc_path, + pyside_rcc_options) + elif srcname.endswith('.qrc'): + # Replace last occurence of '.qrc' in srcname + srcname_split = srcname.rsplit('.qrc', 1) + dstname = '_rc.py'.join(srcname_split) + if os.path.exists(dstname): + log.info('Regenerating {} from {}'.format(dstname, + os.path.basename(srcname))) + run_process([pyside_rcc_path, + pyside_rcc_options, + srcname, '-o', dstname]) + + +def back_tick(cmd, ret_err=False): + """ + Run command `cmd`, return stdout, or stdout, stderr, + return_code if `ret_err` is True. + + Roughly equivalent to ``check_output`` in Python 2.7 + + Parameters + ---------- + cmd : str + command to execute + ret_err : bool, optional + If True, return stderr and return_code in addition to stdout. + If False, just return stdout + + Returns + ------- + out : str or tuple + If `ret_err` is False, return stripped string containing stdout from + `cmd`. + If `ret_err` is True, return tuple of (stdout, stderr, return_code) + where ``stdout`` is the stripped stdout, and ``stderr`` is the stripped + stderr, and ``return_code`` is the process exit code. + + Raises + ------ + Raises RuntimeError if command returns non-zero exit code when ret_err + isn't set. + """ + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, shell=True) + out, err = proc.communicate() + if not isinstance(out, str): + # python 3 + out = out.decode() + err = err.decode() + retcode = proc.returncode + if retcode is None and not ret_err: + proc.terminate() + raise RuntimeError(cmd + ' process did not terminate') + if retcode != 0 and not ret_err: + raise RuntimeError("{} process returned code {}\n*** {}".format( + (cmd, retcode, err))) + out = out.strip() + if not ret_err: + return out + return out, err.strip(), retcode + + +MACOS_OUTNAME_RE = re.compile(r'\(compatibility version [\d.]+, current version ' + '[\d.]+\)') + +def macos_get_install_names(libpath): + """ + Get macOS library install names from library `libpath` using ``otool`` + + Parameters + ---------- + libpath : str + path to library + + Returns + ------- + install_names : list of str + install names in library `libpath` + """ + out = back_tick('otool -L ' + libpath) + libs = [line for line in out.split('\n')][1:] + return [MACOS_OUTNAME_RE.sub('', lib).strip() for lib in libs] + + +MACOS_RPATH_RE = re.compile(r"path (.+) \(offset \d+\)") + +def macos_get_rpaths(libpath): + """ Get rpath load commands from library `libpath` using ``otool`` + + Parameters + ---------- + libpath : str + path to library + + Returns + ------- + rpaths : list of str + rpath values stored in ``libpath`` + + Notes + ----- + See ``man dyld`` for more information on rpaths in libraries + """ + lines = back_tick('otool -l ' + libpath).split('\n') + ctr = 0 + rpaths = [] + while ctr < len(lines): + line = lines[ctr].strip() + if line != 'cmd LC_RPATH': + ctr += 1 + continue + assert lines[ctr + 1].strip().startswith('cmdsize') + rpath_line = lines[ctr + 2].strip() + match = MACOS_RPATH_RE.match(rpath_line) + if match is None: + raise RuntimeError('Unexpected path line: ' + rpath_line) + rpaths.append(match.groups()[0]) + ctr += 3 + return rpaths + + +def macos_fix_rpaths_for_library(library_path, qt_lib_dir): + """ Adds required rpath load commands to given library. + + This is a necessary post-installation step, to allow loading PySide + modules without setting DYLD_LIBRARY_PATH or DYLD_FRAMEWORK_PATH. + The CMake rpath commands which are added at build time are used only + for testing (make check), and they are stripped once the equivalent + of make install is executed (except for shiboken, which currently + uses CMAKE_INSTALL_RPATH_USE_LINK_PATH, which might be necessary to + remove in the future). + + Parameters + ---------- + library_path : str + path to library for which to set rpaths. + qt_lib_dir : str + rpath to installed Qt lib directory. + """ + + install_names = macos_get_install_names(library_path) + existing_rpath_commands = macos_get_rpaths(library_path) + + needs_loader_path = False + for install_name in install_names: + # Absolute path, skip it. + if install_name[0] == '/': + continue + + # If there are dynamic library install names that contain + # @rpath tokens, we will provide an rpath load command with the + # value of "@loader_path". This will allow loading dependent + # libraries from within the same directory as 'library_path'. + if install_name[0] == '@': + needs_loader_path = True + break + + if needs_loader_path and "@loader_path" not in existing_rpath_commands: + back_tick('install_name_tool -add_rpath {rpath} {library_path}'.format( + rpath="@loader_path", library_path=library_path)) + + # If the library depends on a Qt library, add an rpath load comment + # pointing to the Qt lib directory. + macos_add_qt_rpath(library_path, qt_lib_dir, existing_rpath_commands, + install_names) + +def macos_add_qt_rpath(library_path, qt_lib_dir, + existing_rpath_commands = [], library_dependencies = []): + """ + Adds an rpath load command to the Qt lib directory if necessary + + Checks if library pointed to by 'library_path' has Qt dependencies, + and adds an rpath load command that points to the Qt lib directory + (qt_lib_dir). + """ + if not existing_rpath_commands: + existing_rpath_commands = macos_get_rpaths(library_path) + + # Return early if qt rpath is already present. + if qt_lib_dir in existing_rpath_commands: + return + + # Check if any library dependencies are Qt libraries (hacky). + if not library_dependencies: + library_dependencies = macos_get_install_names(library_path) + + needs_qt_rpath = False + for library in library_dependencies: + if 'Qt' in library: + needs_qt_rpath = True + break + + if needs_qt_rpath: + back_tick('install_name_tool -add_rpath {rpath} {library_path}'.format( + rpath=qt_lib_dir, library_path=library_path)) + +# Find an executable specified by a glob pattern ('foo*') in the OS path +def findGlobInPath(pattern): + result = [] + if sys.platform == 'win32': + pattern += '.exe' + + for path in os.environ.get('PATH', '').split(os.pathsep): + for match in glob.glob(os.path.join(path, pattern)): + result.append(match) + return result + +# Locate the most recent version of llvmConfig in the path. +def findLlvmConfig(): + versionRe = re.compile('(\d+)\.(\d+)\.(\d+)') + result = None + lastVersionString = '000000' + for llvmConfig in findGlobInPath('llvm-config*'): + try: + output = run_process_output([llvmConfig, '--version']) + if output: + match = versionRe.match(output[0]) + if match: + versionString = '%02d%02d%02d' % (int(match.group(1)), + int(match.group(2)), int(match.group(3))) + if (versionString > lastVersionString): + result = llvmConfig + lastVersionString = versionString + except OSError: + pass + return result + +# Add Clang to path for Windows for the shiboken ApiExtractor tests. +# Revisit once Clang is bundled with Qt. +def detectClang(): + source = 'LLVM_INSTALL_DIR' + clangDir = os.environ.get(source, None) + if not clangDir: + source = 'CLANG_INSTALL_DIR' + clangDir = os.environ.get(source, None) + if not clangDir: + source = findLlvmConfig() + try: + if source is not None: + output = run_process_output([source, '--prefix']) + if output: + clangDir = output[0] + except OSError: + pass + if clangDir: + arch = '64' if sys.maxsize > 2**31-1 else '32' + clangDir = clangDir.replace('_ARCH_', arch) + return (clangDir, source) + +def download_and_extract_7z(fileurl, target): + """ Downloads 7z file from fileurl and extract to target """ + print("Downloading fileUrl {} ".format(fileurl)) + info = "" + try: + localfile, info = urllib.urlretrieve(fileurl) + except: + print("Error downloading {} : {}".format(fileurl, info)) + raise RuntimeError(' Error downloading {}'.format(fileurl)) + + try: + outputDir = "-o" + target + print("calling 7z x {} {}".format(localfile, outputDir)) + subprocess.call(["7z", "x", "-y", localfile, outputDir]) + except: + raise RuntimeError(' Error extracting {}'.format(localfile)) + +def split_and_strip(input): + lines = [s.strip() for s in input.splitlines()] + return lines + +def ldd_get_dependencies(executable_path): + """ + Returns a dictionary of dependencies that `executable_path` + depends on. + + The keys are library names and the values are the library paths. + + """ + output = ldd(executable_path) + lines = split_and_strip(output) + pattern = re.compile(r"\s*(.*?)\s+=>\s+(.*?)\s+\(.*\)") + dependencies = {} + for line in lines: + match = pattern.search(line) + if match: + dependencies[match.group(1)] = match.group(2) + return dependencies + +def ldd_get_paths_for_dependencies(dependencies_regex, executable_path = None, + dependencies = None): + """ + Returns file paths to shared library dependencies that match given + `dependencies_regex` against given `executable_path`. + + The function retrieves the list of shared library dependencies using + ld.so for the given `executable_path` in order to search for + libraries that match the `dependencies_regex`, and then returns a + list of absolute paths of the matching libraries. + + If no matching library is found in the list of dependencies, + an empty list is returned. + """ + + if not dependencies and not executable_path: + return None + + if not dependencies: + dependencies = ldd_get_dependencies(executable_path) + + pattern = re.compile(dependencies_regex) + + paths = [] + for key in dependencies: + match = pattern.search(key) + if match: + paths.append(dependencies[key]) + + return paths + +def ldd(executable_path): + """ + Returns ld.so output of shared library dependencies for given + `executable_path`. + + This is a partial port of /usr/bin/ldd from bash to Python. + The dependency list is retrieved by setting the + LD_TRACE_LOADED_OBJECTS=1 environment variable, and executing the + given path via the dynamic loader ld.so. + + Only works on Linux. The port is required to make this work on + systems that might not have ldd. + This is because ldd (on Ubuntu) is shipped in the libc-bin package + that, which might have a + minuscule percentage of not being installed. + + Parameters + ---------- + executable_path : str + path to executable or shared library. + + Returns + ------- + output : str + the raw output retrieved from the dynamic linker. + """ + + chosen_rtld = None + # List of ld's considered by ldd on Ubuntu (here's hoping it's the + # same on all distros). + rtld_list = ["/lib/ld-linux.so.2", "/lib64/ld-linux-x86-64.so.2", + "/libx32/ld-linux-x32.so.2"] + + # Choose appropriate runtime dynamic linker. + for rtld in rtld_list: + if os.path.isfile(rtld) and os.access(rtld, os.X_OK): + (_, _, code) = back_tick(rtld, True) + # Code 127 is returned by ld.so when called without any + # arguments (some kind of sanity check I guess). + if code == 127: + (_, _, code) = back_tick("{} --verify {}".format(rtld, + executable_path), True) + # Codes 0 and 2 mean given executable_path can be + # understood by ld.so. + if code in [0, 2]: + chosen_rtld = rtld + break + + if not chosen_rtld: + raise RuntimeError("Could not find appropriate ld.so to query " + "for dependencies.") + + # Query for shared library dependencies. + rtld_env = "LD_TRACE_LOADED_OBJECTS=1" + rtld_cmd = "{} {} {}".format(rtld_env, chosen_rtld, executable_path) + (out, _, return_code) = back_tick(rtld_cmd, True) + if return_code == 0: + return out + else: + raise RuntimeError("ld.so failed to query for dependent shared " + "libraries of {} ".format(executable_path)) + +def find_files_using_glob(path, pattern): + """ Returns list of files that matched glob `pattern` in `path`. """ + final_pattern = os.path.join(path, pattern) + maybe_files = glob.glob(final_pattern) + return maybe_files + +def find_qt_core_library_glob(lib_dir): + """ Returns path to the QtCore library found in `lib_dir`. """ + maybe_file = find_files_using_glob(lib_dir, "libQt5Core.so.?") + if len(maybe_file) == 1: + return maybe_file[0] + return None + +# @TODO: Possibly fix ICU library copying on macOS and Windows. +# This would require to implement the equivalent of the custom written +# ldd for the specified platforms. +# This has less priority because ICU libs are not used in the default +# Qt configuration build. +def copy_icu_libs(destination_lib_dir): + """ + Copy ICU libraries that QtCore depends on, + to given `destination_lib_dir`. + """ + qt_core_library_path = find_qt_core_library_glob(destination_lib_dir) + + if not qt_core_library_path or not os.path.exists(qt_core_library_path): + raise RuntimeError('QtCore library does not exist at path: {}. ' + 'Failed to copy ICU libraries.'.format(qt_core_library_path)) + + dependencies = ldd_get_dependencies(qt_core_library_path) + + icu_regex = r"^libicu.+" + icu_compiled_pattern = re.compile(icu_regex) + icu_required = False + for dependency in dependencies: + match = icu_compiled_pattern.search(dependency) + if match: + icu_required = True + break + + if icu_required: + paths = ldd_get_paths_for_dependencies(icu_regex, + dependencies=dependencies) + if not paths: + raise RuntimeError("Failed to find the necessary ICU libraries " + "required by QtCore.") + log.info('Copying the detected ICU libraries required by QtCore.') + + if not os.path.exists(destination_lib_dir): + os.makedirs(destination_lib_dir) + + for path in paths: + basename = os.path.basename(path) + destination = os.path.join(destination_lib_dir, basename) + copyfile(path, destination, force_copy_symlink=True) + # Patch the ICU libraries to contain the $ORIGIN rpath + # value, so that only the local package libraries are used. + linuxSetRPaths(destination, '$ORIGIN') + + # Patch the QtCore library to find the copied over ICU libraries + # (if necessary). + log.info("Checking if QtCore library needs a new rpath to make it " + "work with ICU libs.") + rpaths = linuxGetRPaths(qt_core_library_path) + if not rpaths or not rpathsHasOrigin(rpaths): + log.info('Patching QtCore library to contain $ORIGIN rpath.') + rpaths.insert(0, '$ORIGIN') + new_rpaths_string = ":".join(rpaths) + linuxSetRPaths(qt_core_library_path, new_rpaths_string) + +def linuxSetRPaths(executable_path, rpath_string): + """ Patches the `executable_path` with a new rpath string. """ + + if not hasattr(linuxSetRPaths, "patchelf_path"): + script_dir = os.getcwd() + patchelf_path = os.path.join(script_dir, "patchelf") + setattr(linuxSetRPaths, "patchelf_path", patchelf_path) + + cmd = [linuxSetRPaths.patchelf_path, '--set-rpath', + rpath_string, executable_path] + + if run_process(cmd) != 0: + raise RuntimeError("Error patching rpath in {}".format( + executable_path)) + +def linuxGetRPaths(executable_path): + """ + Returns a list of run path values embedded in the executable or just + an empty list. + """ + + cmd = "readelf -d {}".format(executable_path) + (out, err, code) = back_tick(cmd, True) + if code != 0: + raise RuntimeError("Running `readelf -d {}` failed with error " + "output:\n {}. ".format(executable_path, err)) + lines = split_and_strip(out) + pattern = re.compile(r"^.+?\(RUNPATH\).+?\[(.+?)\]$") + + rpath_line = None + for line in lines: + match = pattern.search(line) + if match: + rpath_line = match.group(1) + break + + rpaths = [] + + if rpath_line: + rpaths = rpath_line.split(':') + + return rpaths + +def rpathsHasOrigin(rpaths): + """ + Return True if the specified list of rpaths has an "$ORIGIN" value + (aka current dir). + """ + if not rpaths: + return False + + pattern = re.compile(r"^\$ORIGIN(/)?$") + for rpath in rpaths: + match = pattern.search(rpath) + if match: + return True + return False + +def memoize(function): + """ + Decorator to wrap a function with a memoizing callable. + It returns cached values when the wrapped function is called with + the same arguments. + """ + memo = {} + def wrapper(*args): + if args in memo: + return memo[args] + else: + rv = function(*args) + memo[args] = rv + return rv + return wrapper + +def get_python_dict(python_script_path): + try: + with open(python_script_path) as f: + python_dict = {} + code = compile(f.read(), python_script_path, 'exec') + exec(code, {}, python_dict) + return python_dict + except IOError as e: + print("get_python_dict: Couldn't get dict from python " + "file: {}.".format(python_script_path)) + raise |