############################################################################# ## ## Copyright (C) 2018 The Qt Company Ltd. ## Contact: https://www.qt.io/licensing/ ## ## This file is part of Qt for Python. ## ## $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$ ## ############################################################################# from __future__ import print_function from distutils.version import LooseVersion import os import platform import re import sys from textwrap import dedent import time from .config import config from .utils import get_python_dict from .options import DistUtilsCommandMixin, OPTION from .wheel_utils import (get_package_version, get_qt_version, get_package_timestamp, macos_plat_name, macos_pyside_min_deployment_target) setup_script_dir = os.getcwd() build_scripts_dir = os.path.join(setup_script_dir, 'build_scripts') setup_py_path = os.path.join(setup_script_dir, "setup.py") start_time = int(time.time()) def elapsed(): return int(time.time()) - start_time def get_setuptools_extension_modules(): # Setting py_limited_api on the extension is the "correct" thing # to do, but it doesn't actually do anything, because we # override build_ext. So this is just foolproofing for the # future. extension_args = ('QtCore', []) extension_kwargs = {} if OPTION["LIMITED_API"] == 'yes': extension_kwargs['py_limited_api'] = True extension_modules = [Extension(*extension_args, **extension_kwargs)] return extension_modules def _get_make(platform_arch, build_type): """Helper for retrieving the make command and CMake generator name""" makespec = OPTION["MAKESPEC"] if makespec == "make": return ("make", "Unix Makefiles") if makespec == "msvc": nmake_path = find_executable("nmake") if nmake_path is None or not os.path.exists(nmake_path): log.info("nmake not found. Trying to initialize the MSVC env...") init_msvc_env(platform_arch, build_type) nmake_path = find_executable("nmake") if not nmake_path or not os.path.exists(nmake_path): raise DistutilsSetupError('"nmake" could not be found.') if not OPTION["NO_JOM"]: jom_path = find_executable("jom") if jom_path: log.info("jom was found in {}".format(jom_path)) return (jom_path, "NMake Makefiles JOM") log.info("nmake was found in {}".format(nmake_path)) if OPTION["JOBS"]: msg = "Option --jobs can only be used with 'jom' on Windows." raise DistutilsSetupError(msg) return (nmake_path, "NMake Makefiles") if makespec == "mingw": return ("mingw32-make", "mingw32-make") if makespec == "ninja": return ("ninja", "Ninja") m = 'Invalid option --make-spec "{}".'.format(makespec) raise DistutilsSetupError(m) def get_make(platform_arch, build_type): """Retrieve the make command and CMake generator name""" (make_path, make_generator) = _get_make(platform_arch, build_type) if not os.path.isabs(make_path): make_path = find_executable(make_path) if not make_path or not os.path.exists(make_path): raise DistutilsSetupError("You need the program '{}' on your system path to " "compile PySide2.".format(make_path)) return (make_path, make_generator) def _get_py_library_win(build_type, py_version, py_prefix, py_libdir, py_include_dir): """Helper for finding the Python library on Windows""" if py_include_dir is None or not os.path.exists(py_include_dir): py_include_dir = os.path.join(py_prefix, "include") if py_libdir is None or not os.path.exists(py_libdir): # For virtual environments on Windows, the py_prefix will contain a # path pointing to it, instead of the system Python installation path. # Since INCLUDEPY contains a path to the system location, we use the # same base directory to define the py_libdir variable. py_libdir = os.path.join(os.path.dirname(py_include_dir), "libs") if not os.path.isdir(py_libdir): raise DistutilsSetupError("Failed to locate the 'libs' directory") dbg_postfix = "_d" if build_type == "Debug" else "" if OPTION["MAKESPEC"] == "mingw": static_lib_name = "libpython{}{}.a".format( py_version.replace(".", ""), dbg_postfix) return os.path.join(py_libdir, static_lib_name) v = py_version.replace(".", "") python_lib_name = "python{}{}.lib".format(v, dbg_postfix) return os.path.join(py_libdir, python_lib_name) def _get_py_library_unix(build_type, py_version, py_prefix, py_libdir, py_include_dir): """Helper for finding the Python library on UNIX""" if py_libdir is None or not os.path.exists(py_libdir): py_libdir = os.path.join(py_prefix, "lib") if py_include_dir is None or not os.path.exists(py_include_dir): dir = "include/python{}".format(py_version) py_include_dir = os.path.join(py_prefix, dir) dbg_postfix = "_d" if build_type == "Debug" else "" lib_exts = ['.so'] if sys.platform == 'darwin': lib_exts.append('.dylib') if sys.version_info[0] > 2: lib_suff = getattr(sys, 'abiflags', None) else: # Python 2 lib_suff = '' lib_exts.append('.so.1') # Suffix for OpenSuSE 13.01 lib_exts.append('.so.1.0') # static library as last gasp lib_exts.append('.a') if sys.version_info[0] == 2 and dbg_postfix: # For Python2 add a duplicate set of extensions combined with the # dbg_postfix, so we test for both the debug version of the lib # and the normal one. This allows a debug PySide2 to be built with a # non-debug Python. lib_exts = [dbg_postfix + e for e in lib_exts] + lib_exts libs_tried = [] for lib_ext in lib_exts: lib_name = "libpython{}{}{}".format(py_version, lib_suff, lib_ext) py_library = os.path.join(py_libdir, lib_name) if os.path.exists(py_library): return py_library libs_tried.append(py_library) # At least on macOS 10.11, the system Python 2.6 does not include a # symlink to the framework file disguised as a .dylib file, thus finding # the library would fail. Manually check if a framework file "Python" # exists in the Python framework bundle. if sys.platform == 'darwin' and sys.version_info[:2] == (2, 6): # These manipulations essentially transform # /System/Library/Frameworks/Python.framework/Versions/2.6/lib # to # /System/Library/Frameworks/Python.framework/Versions/2.6/Python possible_framework_path = os.path.realpath(os.path.join(py_libdir, '..')) possible_framework_version = os.path.basename(possible_framework_path) possible_framework_library = os.path.join(possible_framework_path, 'Python') if (possible_framework_version == '2.6' and os.path.exists(possible_framework_library)): return possible_framework_library libs_tried.append(possible_framework_library) # Try to find shared libraries which have a multi arch # suffix. py_multiarch = get_config_var("MULTIARCH") if py_multiarch: try_py_libdir = os.path.join(py_libdir, py_multiarch) libs_tried = [] for lib_ext in lib_exts: lib_name = "libpython{}{}{}".format(py_version, lib_suff, lib_ext) py_library = os.path.join(try_py_libdir, lib_name) if os.path.exists(py_library): return py_library libs_tried.append(py_library) m = "Failed to locate the Python library with {}".format(", ".join(libs_tried)) raise DistutilsSetupError(m) def get_py_library(build_type, py_version, py_prefix, py_libdir, py_include_dir): """Find the Python library""" if sys.platform == "win32": py_library = _get_py_library_win(build_type, py_version, py_prefix, py_libdir, py_include_dir) else: py_library = _get_py_library_unix(build_type, py_version, py_prefix, py_libdir, py_include_dir) if py_library.endswith('.a'): # Python was compiled as a static library log.error("Failed to locate a dynamic Python library, using {}".format(py_library)) return py_library # Git submodules: ["submodule_name", "location_relative_to_sources_folder"] submodules = [["pyside2-tools"]] try: import setuptools except ImportError: from ez_setup import use_setuptools use_setuptools() from setuptools import Extension from setuptools.command.install import install as _install from setuptools.command.install_lib import install_lib as _install_lib from setuptools.command.bdist_egg import bdist_egg as _bdist_egg from setuptools.command.develop import develop as _develop from setuptools.command.build_py import build_py as _build_py import distutils.log as log from distutils.errors import DistutilsSetupError from distutils.sysconfig import get_config_var from distutils.sysconfig import get_python_lib from distutils.spawn import find_executable from distutils.command.build import build as _build from distutils.command.build_ext import build_ext as _build_ext from distutils.util import get_platform from distutils.cmd import Command from .qtinfo import QtInfo from .utils import rmtree, detect_clang, copyfile, copydir, run_process_output, run_process from .utils import update_env_path, init_msvc_env, filter_match from .utils import macos_fix_rpaths_for_library from .utils import linux_fix_rpaths_for_library from .platforms.unix import prepare_packages_posix from .platforms.windows_desktop import prepare_packages_win32 from .wheel_override import wheel_module_exists, get_bdist_wheel_override def check_allowed_python_version(): """ Make sure that setup.py is run with an allowed python version. """ import re pattern = r'Programming Language :: Python :: (\d+)\.(\d+)' supported = [] for line in config.python_version_classifiers: found = re.search(pattern, line) if found: major = int(found.group(1)) minor = int(found.group(2)) supported.append((major, minor)) this_py = sys.version_info[:2] if this_py not in supported: print("Unsupported python version detected. Only these python versions are supported: {}" .format(supported)) sys.exit(1) qt_src_dir = '' def is_debug_python(): return getattr(sys, "gettotalrefcount", None) is not None # Return a prefix suitable for the _install/_build directory def prefix(): virtual_env_name = os.environ.get('VIRTUAL_ENV', None) if virtual_env_name is not None: name = os.path.basename(virtual_env_name) else: name = "pyside" name += str(sys.version_info[0]) if OPTION["DEBUG"]: name += "d" if is_debug_python(): name += "p" if OPTION["LIMITED_API"] == "yes" and sys.version_info[0] == 3: name += "a" return name # Initialize, pull and checkout submodules def prepare_sub_modules(): print("Initializing submodules for PySide2 version: {}".format( get_package_version())) submodules_dir = os.path.join(setup_script_dir, "sources") # Create list of [name, desired branch, absolute path, desired # branch] and determine whether all submodules are present need_init_sub_modules = False for m in submodules: module_name = m[0] module_dir = m[1] if len(m) > 1 else '' module_dir = os.path.join(submodules_dir, module_dir, module_name) # Check for non-empty directory (repository checked out) if not os.listdir(module_dir): need_init_sub_modules = True break if need_init_sub_modules: git_update_cmd = ["git", "submodule", "update", "--init"] if run_process(git_update_cmd) != 0: m = "Failed to initialize the git submodules: update --init failed" raise DistutilsSetupError(m) git_pull_cmd = ["git", "submodule", "foreach", "git", "fetch", "--all"] if run_process(git_pull_cmd) != 0: m = "Failed to initialize the git submodules: git fetch --all failed" raise DistutilsSetupError(m) else: print("All submodules present.") git_update_cmd = ["git", "submodule", "update"] if run_process(git_update_cmd) != 0: m = "Failed to checkout the correct git submodules SHA1s." raise DistutilsSetupError(m) def prepare_build(): if (os.path.isdir(".git") and not OPTION["IGNOREGIT"] and not OPTION["ONLYPACKAGE"] and not OPTION["REUSE_BUILD"]): prepare_sub_modules() # Clean up temp build folder. for n in ["build"]: d = os.path.join(setup_script_dir, n) if os.path.isdir(d): log.info("Removing {}".format(d)) try: rmtree(d) except Exception as e: print('***** problem removing "{}"'.format(d)) print('ignored error: {}'.format(e)) # locate Qt sources for the documentation if OPTION["QT_SRC"] is None: install_prefix = QtInfo().prefix_dir if install_prefix: global qt_src_dir # In-source, developer build if install_prefix.endswith("qtbase"): qt_src_dir = install_prefix else: # SDK: Use 'Src' directory qt_src_dir = os.path.join(os.path.dirname(install_prefix), 'Src', 'qtbase') class PysideInstall(_install, DistUtilsCommandMixin): user_options = _install.user_options + DistUtilsCommandMixin.mixin_user_options def __init__(self, *args, **kwargs): _install.__init__(self, *args, **kwargs) DistUtilsCommandMixin.__init__(self) def initialize_options(self): _install.initialize_options(self) if sys.platform == 'darwin': # Because we change the plat_name to include a correct # deployment target on macOS distutils thinks we are # cross-compiling, and throws an exception when trying to # execute setup.py install. The check looks like this # if self.warn_dir and build_plat != get_platform(): # raise DistutilsPlatformError("Can't install when " # "cross-compiling") # Obviously get_platform will return the old deployment # target. The fix is to disable the warn_dir flag, which # was created for bdist_* derived classes to override, for # similar cases. self.warn_dir = False def finalize_options(self): DistUtilsCommandMixin.mixin_finalize_options(self) _install.finalize_options(self) def run(self): _install.run(self) print('--- Install completed ({}s)'.format(elapsed())) class PysideDevelop(_develop): def __init__(self, *args, **kwargs): _develop.__init__(self, *args, **kwargs) def run(self): self.run_command("build") _develop.run(self) class PysideBdistEgg(_bdist_egg): def __init__(self, *args, **kwargs): _bdist_egg.__init__(self, *args, **kwargs) def run(self): self.run_command("build") _bdist_egg.run(self) class PysideBuildExt(_build_ext): def __init__(self, *args, **kwargs): _build_ext.__init__(self, *args, **kwargs) def run(self): pass class PysideBuildPy(_build_py): def __init__(self, *args, **kwargs): _build_py.__init__(self, *args, **kwargs) # _install_lib is reimplemented to preserve # symlinks when distutils / setuptools copy files to various # directories from the setup tools build dir to the install dir. class PysideInstallLib(_install_lib): def __init__(self, *args, **kwargs): _install_lib.__init__(self, *args, **kwargs) def install(self): """ Installs files from build/xxx directory into final site-packages/PySide2 directory. """ if os.path.isdir(self.build_dir): # Using our own copydir makes sure to preserve symlinks. outfiles = copydir(os.path.abspath(self.build_dir), os.path.abspath(self.install_dir)) else: self.warn("'{}' does not exist -- no Python modules to install".format(self.build_dir)) return return outfiles class PysideBuild(_build, DistUtilsCommandMixin): user_options = _build.user_options + DistUtilsCommandMixin.mixin_user_options def __init__(self, *args, **kwargs): _build.__init__(self, *args, **kwargs) DistUtilsCommandMixin.__init__(self) def finalize_options(self): os_name_backup = os.name DistUtilsCommandMixin.mixin_finalize_options(self) if sys.platform == 'darwin': self.plat_name = macos_plat_name() # This is a hack to circumvent the dubious check in # distutils.commands.build -> finalize_options, which only # allows setting the plat_name for windows NT. # That is not the case for the wheel module though (which # does allow setting plat_name), so we circumvent by faking # the os name when finalizing the options, and then # restoring the original os name. os.name = "nt" _build.finalize_options(self) if sys.platform == 'darwin': os.name = os_name_backup def initialize_options(self): _build.initialize_options(self) self.make_path = None self.make_generator = None self.script_dir = None self.sources_dir = None self.build_dir = None self.install_dir = None self.py_executable = None self.py_include_dir = None self.py_library = None self.py_version = None self.py_arch = None self.build_type = "Release" self.qtinfo = None self.build_tests = False def run(self): prepare_build() platform_arch = platform.architecture()[0] log.info("Python architecture is {}".format(platform_arch)) self.py_arch = platform_arch[:-3] build_type = "Debug" if OPTION["DEBUG"] else "Release" if OPTION["RELWITHDEBINFO"]: build_type = 'RelWithDebInfo' # Check env make_path = None make_generator = None if not OPTION["ONLYPACKAGE"]: (make_path, make_generator) = get_make(platform_arch, build_type) # Prepare parameters py_executable = sys.executable py_version = "{}.{}".format(sys.version_info[0], sys.version_info[1]) py_include_dir = get_config_var("INCLUDEPY") py_libdir = get_config_var("LIBDIR") py_prefix = get_config_var("prefix") if not py_prefix or not os.path.exists(py_prefix): py_prefix = sys.prefix self.py_prefix = py_prefix if sys.platform == "win32": py_scripts_dir = os.path.join(py_prefix, "Scripts") else: py_scripts_dir = os.path.join(py_prefix, "bin") self.py_scripts_dir = py_scripts_dir self.qtinfo = QtInfo() qt_dir = os.path.dirname(OPTION["QMAKE"]) qt_version = get_qt_version() # Update the PATH environment variable additional_paths = [self.py_scripts_dir, qt_dir] # Add Clang to path for Windows. # Revisit once Clang is bundled with Qt. if (sys.platform == "win32" and LooseVersion(self.qtinfo.version) >= LooseVersion("5.7.0")): clang_dir = detect_clang() if clang_dir[0]: clangBinDir = os.path.join(clang_dir[0], 'bin') if clangBinDir not in os.environ.get('PATH'): log.info("Adding {} as detected by {} to PATH".format(clangBinDir, clang_dir[1])) additional_paths.append(clangBinDir) else: raise DistutilsSetupError("Failed to detect Clang when checking " "LLVM_INSTALL_DIR, CLANG_INSTALL_DIR, llvm-config") update_env_path(additional_paths) # Used for test blacklists and registry test. self.build_classifiers = "py{}-qt{}-{}-{}".format(py_version, qt_version, platform.architecture()[0], build_type.lower()) if OPTION["SHORTER_PATHS"]: build_name = "p{}".format(py_version) else: build_name = self.build_classifiers script_dir = setup_script_dir sources_dir = os.path.join(script_dir, "sources") build_dir = os.path.join(script_dir, prefix() + "_build", "{}".format(build_name)) install_dir = os.path.join(script_dir, prefix() + "_install", "{}".format(build_name)) self.make_path = make_path self.make_generator = make_generator self.script_dir = script_dir self.st_build_dir = os.path.join(self.script_dir, self.build_lib) self.sources_dir = sources_dir self.build_dir = build_dir self.install_dir = install_dir self.py_executable = py_executable self.py_include_dir = py_include_dir self.py_library = get_py_library(build_type, py_version, py_prefix, py_libdir, py_include_dir) self.py_version = py_version self.build_type = build_type self.site_packages_dir = get_python_lib(1, 0, prefix=install_dir) self.build_tests = OPTION["BUILDTESTS"] # Save the shiboken build dir path for clang deployment # purposes. self.shiboken_build_dir = os.path.join(self.build_dir, "shiboken2") self.log_pre_build_info() # Prepare folders if not os.path.exists(self.sources_dir): log.info("Creating sources folder {}...".format(self.sources_dir)) os.makedirs(self.sources_dir) if not os.path.exists(self.build_dir): log.info("Creating build folder {}...".format(self.build_dir)) os.makedirs(self.build_dir) if not os.path.exists(self.install_dir): log.info("Creating install folder {}...".format(self.install_dir)) os.makedirs(self.install_dir) if (not OPTION["ONLYPACKAGE"] and not config.is_internal_shiboken_generator_build_and_part_of_top_level_all()): # Build extensions for ext in config.get_buildable_extensions(): self.build_extension(ext) if OPTION["BUILDTESTS"]: # we record the latest successful build and note the # build directory for supporting the tests. timestamp = time.strftime('%Y-%m-%d_%H%M%S') build_history = os.path.join(setup_script_dir, 'build_history') unique_dir = os.path.join(build_history, timestamp) os.makedirs(unique_dir) fpath = os.path.join(unique_dir, 'build_dir.txt') with open(fpath, 'w') as f: print(build_dir, file=f) print(self.build_classifiers, file=f) log.info("Created {}".format(build_history)) if not OPTION["SKIP_PACKAGING"]: # Build patchelf if needed self.build_patchelf() # Prepare packages self.prepare_packages() # Build packages _build.run(self) else: log.info("Skipped preparing and building packages.") print('--- Build completed ({}s)'.format(elapsed())) def log_pre_build_info(self): if config.is_internal_shiboken_generator_build_and_part_of_top_level_all(): return setuptools_install_prefix = get_python_lib(1) if OPTION["FINAL_INSTALL_PREFIX"]: setuptools_install_prefix = OPTION["FINAL_INSTALL_PREFIX"] log.info("=" * 30) log.info("Package version: {}".format(get_package_version())) log.info("Build type: {}".format(self.build_type)) log.info("Build tests: {}".format(self.build_tests)) log.info("-" * 3) log.info("Make path: {}".format(self.make_path)) log.info("Make generator: {}".format(self.make_generator)) log.info("Make jobs: {}".format(OPTION["JOBS"])) log.info("-" * 3) log.info("setup.py directory: {}".format(self.script_dir)) log.info("Build scripts directory: {}".format(build_scripts_dir)) log.info("Sources directory: {}".format(self.sources_dir)) log.info(dedent(""" Building {st_package_name} will create and touch directories in the following order: make build directory (py*_build/*/*) -> make install directory (py*_install/*/*) -> setuptools build directory (build/*/*) -> setuptools install directory (usually path-installed-python/lib/python*/site-packages/*) """).format(st_package_name=config.package_name())) log.info("make build directory: {}".format(self.build_dir)) log.info("make install directory: {}".format(self.install_dir)) log.info("setuptools build directory: {}".format(self.st_build_dir)) log.info("setuptools install directory: {}".format(setuptools_install_prefix)) log.info(dedent(""" make-installed site-packages directory: {} (only relevant for copying files from 'make install directory' to 'setuptools build directory' """).format( self.site_packages_dir)) log.info("-" * 3) log.info("Python executable: {}".format(self.py_executable)) log.info("Python includes: {}".format(self.py_include_dir)) log.info("Python library: {}".format(self.py_library)) log.info("Python prefix: {}".format(self.py_prefix)) log.info("Python scripts: {}".format(self.py_scripts_dir)) log.info("-" * 3) log.info("Qt qmake: {}".format(self.qtinfo.qmake_command)) log.info("Qt version: {}".format(self.qtinfo.version)) log.info("Qt bins: {}".format(self.qtinfo.bins_dir)) log.info("Qt docs: {}".format(self.qtinfo.docs_dir)) log.info("Qt plugins: {}".format(self.qtinfo.plugins_dir)) log.info("-" * 3) if sys.platform == 'win32': log.info("OpenSSL dll directory: {}".format(OPTION["OPENSSL"])) if sys.platform == 'darwin': pyside_macos_deployment_target = ( macos_pyside_min_deployment_target() ) log.info("MACOSX_DEPLOYMENT_TARGET set to: {}".format( pyside_macos_deployment_target)) log.info("=" * 30) def build_patchelf(self): if not sys.platform.startswith('linux'): return self._patchelf_path = find_executable('patchelf') if self._patchelf_path: if not os.path.isabs(self._patchelf_path): self._patchelf_path = os.path.join(os.getcwd(), self._patchelf_path) log.info("Using {} ...".format(self._patchelf_path)) return log.info("Building patchelf...") module_src_dir = os.path.join(self.sources_dir, "patchelf") build_cmd = ["g++", "{}/patchelf.cc".format(module_src_dir), "-o", "patchelf"] if run_process(build_cmd) != 0: raise DistutilsSetupError("Error building patchelf") self._patchelf_path = os.path.join(self.script_dir, "patchelf") def build_extension(self, extension): # calculate the subrepos folder name log.info("Building module {}...".format(extension)) # Prepare folders os.chdir(self.build_dir) module_build_dir = os.path.join(self.build_dir, extension) skipflag_file = "{} -skip".format(module_build_dir) if os.path.exists(skipflag_file): log.info("Skipping {} because {} exists".format(extension, skipflag_file)) return module_build_exists = os.path.exists(module_build_dir) if module_build_exists: if not OPTION["REUSE_BUILD"]: log.info("Deleting module build folder {}...".format(module_build_dir)) try: rmtree(module_build_dir) except Exception as e: print('***** problem removing "{}"'.format(module_build_dir)) print('ignored error: {}'.format(e)) else: log.info("Reusing module build folder {}...".format(module_build_dir)) if not os.path.exists(module_build_dir): log.info("Creating module build folder {}...".format(module_build_dir)) os.makedirs(module_build_dir) os.chdir(module_build_dir) module_src_dir = os.path.join(self.sources_dir, extension) # Build module cmake_cmd = [OPTION["CMAKE"]] if OPTION["QUIET"]: # Pass a special custom option, to allow printing a lot less information when doing # a quiet build. cmake_cmd.append('-DQUIET_BUILD=1') if self.make_generator == "Unix Makefiles": # Hide progress messages for each built source file. # Doesn't seem to work if set within the cmake files themselves. cmake_cmd.append('-DCMAKE_RULE_MESSAGES=0') cmake_cmd += [ "-G", self.make_generator, "-DBUILD_TESTS={}".format(self.build_tests), "-DQt5Help_DIR={}".format(self.qtinfo.docs_dir), "-DCMAKE_BUILD_TYPE={}".format(self.build_type), "-DCMAKE_INSTALL_PREFIX={}".format(self.install_dir), module_src_dir ] cmake_cmd.append("-DPYTHON_EXECUTABLE={}".format(self.py_executable)) cmake_cmd.append("-DPYTHON_INCLUDE_DIR={}".format(self.py_include_dir)) cmake_cmd.append("-DPYTHON_LIBRARY={}".format(self.py_library)) # If a custom shiboken cmake config directory path was provided, pass it to CMake. if OPTION["SHIBOKEN_CONFIG_DIR"] and config.is_internal_pyside_build(): if os.path.exists(OPTION["SHIBOKEN_CONFIG_DIR"]): log.info("Using custom provided shiboken2 installation: {}" .format(OPTION["SHIBOKEN_CONFIG_DIR"])) cmake_cmd.append("-DShiboken2_DIR={}".format(OPTION["SHIBOKEN_CONFIG_DIR"])) else: log.info("Custom provided shiboken2 installation not found. Path given: {}" .format(OPTION["SHIBOKEN_CONFIG_DIR"])) if OPTION["MODULE_SUBSET"]: module_sub_set = '' for m in OPTION["MODULE_SUBSET"].split(','): if m.startswith('Qt'): m = m[2:] if module_sub_set: module_sub_set += ';' module_sub_set += m cmake_cmd.append("-DMODULES={}".format(module_sub_set)) if OPTION["SKIP_MODULES"]: skip_modules = '' for m in OPTION["SKIP_MODULES"].split(','): if m.startswith('Qt'): m = m[2:] if skip_modules: skip_modules += ';' skip_modules += m cmake_cmd.append("-DSKIP_MODULES={}".format(skip_modules)) # Add source location for generating documentation cmake_src_dir = OPTION["QT_SRC"] if OPTION["QT_SRC"] else qt_src_dir cmake_cmd.append("-DQT_SRC_DIR={}".format(cmake_src_dir)) log.info("Qt Source dir: {}".format(cmake_src_dir)) if self.build_type.lower() == 'debug': cmake_cmd.append("-DPYTHON_DEBUG_LIBRARY={}".format( self.py_library)) if OPTION["LIMITED_API"] == "yes": cmake_cmd.append("-DFORCE_LIMITED_API=yes") elif OPTION["LIMITED_API"] == "no": cmake_cmd.append("-DFORCE_LIMITED_API=no") elif not OPTION["LIMITED_API"]: pass else: raise DistutilsSetupError("option limited-api must be 'yes' or 'no' " "(default yes if applicable, i.e. python version >= 3.5)") if OPTION["VERBOSE_BUILD"]: cmake_cmd.append("-DCMAKE_VERBOSE_MAKEFILE:BOOL=ON") if OPTION["SANITIZE_ADDRESS"]: # Some simple sanity checking. Only use at your own risk. if (sys.platform.startswith('linux') or sys.platform.startswith('darwin')): cmake_cmd.append("-DSANITIZE_ADDRESS=ON") else: raise DistutilsSetupError("Address sanitizer can only be used on Linux and macOS.") if extension.lower() == "pyside2": pyside_qt_conf_prefix = '' if OPTION["QT_CONF_PREFIX"]: pyside_qt_conf_prefix = OPTION["QT_CONF_PREFIX"] else: if OPTION["STANDALONE"]: pyside_qt_conf_prefix = '"Qt"' if sys.platform == 'win32': pyside_qt_conf_prefix = '"."' cmake_cmd.append("-DPYSIDE_QT_CONF_PREFIX={}".format( pyside_qt_conf_prefix)) # Pass package version to CMake, so this string can be # embedded into _config.py file. package_version = get_package_version() cmake_cmd.append("-DPACKAGE_SETUP_PY_PACKAGE_VERSION={}".format(package_version)) # In case if this is a snapshot build, also pass the # timestamp as a separate value, because it is the only # version component that is actually generated by setup.py. timestamp = '' if OPTION["SNAPSHOT_BUILD"]: timestamp = get_package_timestamp() cmake_cmd.append("-DPACKAGE_SETUP_PY_PACKAGE_TIMESTAMP={}".format(timestamp)) if extension.lower() in ["shiboken2", "pyside2-tools"]: cmake_cmd.append("-DCMAKE_INSTALL_RPATH_USE_LINK_PATH=yes") if sys.version_info[0] > 2: cmake_cmd.append("-DUSE_PYTHON_VERSION=3.3") if sys.platform == 'darwin': if OPTION["MACOS_ARCH"]: # also tell cmake which architecture to use cmake_cmd.append("-DCMAKE_OSX_ARCHITECTURES:STRING={}".format(OPTION["MACOS_ARCH"])) if OPTION["MACOS_USE_LIBCPP"]: # Explicitly link the libc++ standard library (useful # for macOS deployment targets lower than 10.9). # This is not on by default, because most libraries and # executables on macOS <= 10.8 are linked to libstdc++, # and mixing standard libraries can lead to crashes. # On macOS >= 10.9 with a similar minimum deployment # target, libc++ is linked in implicitly, thus the # option is a no-op in those cases. cmake_cmd.append("-DOSX_USE_LIBCPP=ON") if OPTION["MACOS_SYSROOT"]: cmake_cmd.append("-DCMAKE_OSX_SYSROOT={}".format( OPTION["MACOS_SYSROOT"])) else: latest_sdk_path = run_process_output(['xcrun', '--sdk', 'macosx', '--show-sdk-path']) if latest_sdk_path: latest_sdk_path = latest_sdk_path[0] cmake_cmd.append("-DCMAKE_OSX_SYSROOT={}".format( latest_sdk_path)) # Set macOS minimum deployment target (version). # This is required so that calling # run_process -> distutils.spawn() # does not set its own minimum deployment target # environment variable which is based on the python # interpreter sysconfig value. # Doing so could break the detected clang include paths # for example. deployment_target = macos_pyside_min_deployment_target() cmake_cmd.append("-DCMAKE_OSX_DEPLOYMENT_TARGET={}".format(deployment_target)) os.environ['MACOSX_DEPLOYMENT_TARGET'] = deployment_target if OPTION["DOC_BUILD_ONLINE"]: log.info("Output format will be HTML") cmake_cmd.append("-DDOC_OUTPUT_FORMAT=html") else: log.info("Output format will be qthelp") cmake_cmd.append("-DDOC_OUTPUT_FORMAT=qthelp") # Build the whole documentation (rst + API) by default cmake_cmd.append("-DFULLDOCSBUILD=1") if not OPTION["SKIP_CMAKE"]: log.info("Configuring module {} ({})...".format(extension, module_src_dir)) if run_process(cmake_cmd) != 0: raise DistutilsSetupError("Error configuring {}".format(extension)) else: log.info("Reusing old configuration for module {} ({})...".format( extension, module_src_dir)) log.info("-- Compiling module {}...".format(extension)) cmd_make = [self.make_path] if OPTION["JOBS"]: cmd_make.append(OPTION["JOBS"]) if run_process(cmd_make) != 0: raise DistutilsSetupError("Error compiling {}".format(extension)) if not OPTION["SKIP_DOCS"]: if extension.lower() == "shiboken2": try: # Check if sphinx is installed to proceed. import sphinx log.info("Generating Shiboken documentation") if run_process([self.make_path, "doc"]) != 0: raise DistutilsSetupError("Error generating documentation " "for {}".format(extension)) except ImportError: log.info("Sphinx not found, skipping documentation build") else: log.info("Skipped documentation generation") if not OPTION["SKIP_MAKE_INSTALL"]: log.info("Installing module {}...".format(extension)) # Need to wait a second, so installed file timestamps are # older than build file timestamps. # See https://gitlab.kitware.com/cmake/cmake/issues/16155 # for issue details. if sys.platform == 'darwin': log.info("Waiting 1 second, to ensure installation is successful...") time.sleep(1) # ninja: error: unknown target 'install/fast' target = 'install/fast' if self.make_generator != 'Ninja' else 'install' if run_process([self.make_path, target]) != 0: raise DistutilsSetupError("Error pseudo installing {}".format( extension)) else: log.info("Skipped installing module {}".format(extension)) os.chdir(self.script_dir) def prepare_packages(self): """ This will copy all relevant files from the various locations in the "cmake install dir", to the setup tools build dir (which is read from self.build_lib provided by distutils). After that setuptools.command.build_py is smart enough to copy everything from the build dir to the install dir (the virtualenv site-packages for example). """ try: log.info("\nPreparing setup tools build directory.\n") vars = { "site_packages_dir": self.site_packages_dir, "sources_dir": self.sources_dir, "install_dir": self.install_dir, "build_dir": self.build_dir, "script_dir": self.script_dir, "st_build_dir": self.st_build_dir, "cmake_package_name": config.package_name(), "st_package_name": config.package_name(), "ssl_libs_dir": OPTION["OPENSSL"], "py_version": self.py_version, "qt_version": self.qtinfo.version, "qt_bin_dir": self.qtinfo.bins_dir, "qt_doc_dir": self.qtinfo.docs_dir, "qt_lib_dir": self.qtinfo.libs_dir, "qt_lib_execs_dir": self.qtinfo.lib_execs_dir, "qt_plugins_dir": self.qtinfo.plugins_dir, "qt_prefix_dir": self.qtinfo.prefix_dir, "qt_translations_dir": self.qtinfo.translations_dir, "qt_qml_dir": self.qtinfo.qml_dir, "target_arch": self.py_arch, } # Needed for correct file installation in generator build # case. if config.is_internal_shiboken_generator_build(): vars['cmake_package_name'] = config.shiboken_module_option_name os.chdir(self.script_dir) if sys.platform == "win32": vars['dbg_postfix'] = OPTION["DEBUG"] and "_d" or "" return prepare_packages_win32(self, vars) else: return prepare_packages_posix(self, vars) except IOError as e: print('setup.py/prepare_packages: ', e) raise def qt_is_framework_build(self): if os.path.isdir(self.qtinfo.headers_dir + "/../lib/QtCore.framework"): return True return False def get_built_pyside_config(self, vars): # Get config that contains list of built modules, and # SOVERSIONs of the built libraries. st_build_dir = vars['st_build_dir'] config_path = os.path.join(st_build_dir, config.package_name(), "_config.py") temp_config = get_python_dict(config_path) if 'built_modules' not in temp_config: temp_config['built_modules'] = [] return temp_config def is_webengine_built(self, built_modules): return ('WebEngineWidgets' in built_modules or 'WebEngineCore' in built_modules or 'WebEngine' in built_modules) def prepare_standalone_clang(self, is_win=False): """ Copies the libclang library to the shiboken2-generator package so that the shiboken executable works. """ log.info('Finding path to the libclang shared library.') cmake_cmd = [ OPTION["CMAKE"], "-L", # Lists variables "-N", # Just inspects the cache (faster) "--build", # Specifies the build dir self.shiboken_build_dir ] out = run_process_output(cmake_cmd) lines = [s.strip() for s in out] pattern = re.compile(r"CLANG_LIBRARY:FILEPATH=(.+)$") clang_lib_path = None for line in lines: match = pattern.search(line) if match: clang_lib_path = match.group(1) break if not clang_lib_path: raise RuntimeError("Could not find the location of the libclang " "library inside the CMake cache file.") if is_win: # clang_lib_path points to the static import library # (lib/libclang.lib), whereas we want to copy the shared # library (bin/libclang.dll). clang_lib_path = re.sub(r'lib/libclang.lib$', 'bin/libclang.dll', clang_lib_path) else: # shiboken2 links against libclang.so.6 or a similarly # named library. # If the linked against library is a symlink, resolve # the symlink once (but not all the way to the real # file) on Linux and macOS, # so that we get the path to the "SO version" symlink # (the one used as the install name in the shared library # dependency section). # E.g. On Linux libclang.so -> libclang.so.6 -> # libclang.so.6.0. # "libclang.so.6" is the name we want for the copied file. if os.path.islink(clang_lib_path): link_target = os.readlink(clang_lib_path) if os.path.isabs(link_target): clang_lib_path = link_target else: # link_target is relative, transform to absolute. clang_lib_path = os.path.join(os.path.dirname(clang_lib_path), link_target) clang_lib_path = os.path.abspath(clang_lib_path) # The destination will be the shiboken package folder. vars = {} vars['st_build_dir'] = self.st_build_dir vars['st_package_name'] = config.package_name() destination_dir = "{st_build_dir}/{st_package_name}".format(**vars) if os.path.exists(clang_lib_path): basename = os.path.basename(clang_lib_path) log.info('Copying libclang shared library {} to the package folder as {}.'.format( clang_lib_path, basename)) destination_path = os.path.join(destination_dir, basename) # Need to modify permissions in case file is not writable # (a reinstall would cause a permission denied error). copyfile(clang_lib_path, destination_path, force_copy_symlink=True, make_writable_by_owner=True) else: raise RuntimeError("Error copying libclang library " "from {} to {}. ".format(clang_lib_path, destination_dir)) def update_rpath(self, package_path, executables): if sys.platform.startswith('linux'): pyside_libs = [lib for lib in os.listdir( package_path) if filter_match(lib, ["*.so", "*.so.*"])] def rpath_cmd(srcpath): final_rpath = '' # Command line rpath option takes precedence over # automatically added one. if OPTION["RPATH_VALUES"]: final_rpath = OPTION["RPATH_VALUES"] else: # Add rpath values pointing to $ORIGIN and the # installed qt lib directory. final_rpath = self.qtinfo.libs_dir if OPTION["STANDALONE"]: final_rpath = "$ORIGIN/Qt/lib" override = OPTION["STANDALONE"] linux_fix_rpaths_for_library(self._patchelf_path, srcpath, final_rpath, override=override) elif sys.platform == 'darwin': pyside_libs = [lib for lib in os.listdir( package_path) if filter_match(lib, ["*.so", "*.dylib"])] def rpath_cmd(srcpath): final_rpath = '' # Command line rpath option takes precedence over # automatically added one. if OPTION["RPATH_VALUES"]: final_rpath = OPTION["RPATH_VALUES"] else: if OPTION["STANDALONE"]: final_rpath = "@loader_path/Qt/lib" else: final_rpath = self.qtinfo.libs_dir macos_fix_rpaths_for_library(srcpath, final_rpath) else: raise RuntimeError('Not configured for platform {}'.format(sys.platform)) pyside_libs.extend(executables) # Update rpath in PySide2 libs for srcname in pyside_libs: srcpath = os.path.join(package_path, srcname) if os.path.isdir(srcpath) or os.path.islink(srcpath): continue if not os.path.exists(srcpath): continue rpath_cmd(srcpath) log.info("Patched rpath to '$ORIGIN/' (Linux) or " "updated rpath (OS/X) in {}.".format(srcpath)) class PysideRstDocs(Command, DistUtilsCommandMixin): description = "Build .rst documentation only" user_options = DistUtilsCommandMixin.mixin_user_options def initialize_options(self): DistUtilsCommandMixin.__init__(self) log.info("-- This build process will not include the API documentation." "API documentation requires a full build of pyside/shiboken.") self.skip = False if config.is_internal_shiboken_generator_build(): self.skip = True if not self.skip: self.name = config.package_name().lower() self.doc_dir = os.path.join(config.setup_script_dir, "sources") self.doc_dir = os.path.join(self.doc_dir, self.name) self.doc_dir = os.path.join(self.doc_dir, "doc") try: # Check if sphinx is installed to proceed. import sphinx if self.name == "shiboken2": log.info("-- Generating Shiboken documentation") log.info("-- Documentation directory: 'html/pyside2/shiboken2/'") elif self.name == "pyside2": log.info("-- Generating PySide documentation") log.info("-- Documentation directory: 'html/pyside2/'") except ImportError: raise DistutilsSetupError("Sphinx not found - aborting") self.html_dir = "html" # creating directories html/pyside2/shiboken2 try: if not os.path.isdir(self.html_dir): os.mkdir(self.html_dir) if self.name == "shiboken2": out_pyside = os.path.join(self.html_dir, "pyside2") if not os.path.isdir(out_pyside): os.mkdir(out_pyside) out_shiboken = os.path.join(out_pyside, "shiboken2") if not os.path.isdir(out_shiboken): os.mkdir(out_shiboken) self.out_dir = out_shiboken # We know that on the shiboken step, we already created the # 'pyside2' directory elif self.name == "pyside2": self.out_dir = os.path.join(self.html_dir, "pyside2") except: raise DistutilsSetupError("Error while creating directories for {}".format(self.doc_dir)) def run(self): if not self.skip: cmake_cmd = [OPTION["CMAKE"]] cmake_cmd += [ "-S", self.doc_dir, "-B", self.out_dir, "-DDOC_OUTPUT_FORMAT=html", "-DFULLDOCSBUILD=0", ] if run_process(cmake_cmd) != 0: raise DistutilsSetupError("Error running CMake for {}".format(self.doc_dir)) if self.name == "pyside2": self.sphinx_src = os.path.join(self.out_dir, "rst") elif self.name == "shiboken2": self.sphinx_src = self.out_dir sphinx_cmd = ["sphinx-build", "-b", "html", "-c", self.sphinx_src, self.doc_dir, self.out_dir] if run_process(sphinx_cmd) != 0: raise DistutilsSetupError("Error running CMake for {}".format(self.doc_dir)) # Last message if not self.skip and self.name == "pyside2": log.info("-- The documentation was built. Check html/pyside2/index.html") def finalize_options(self): DistUtilsCommandMixin.mixin_finalize_options(self) cmd_class_dict = { 'build': PysideBuild, 'build_py': PysideBuildPy, 'build_ext': PysideBuildExt, 'bdist_egg': PysideBdistEgg, 'develop': PysideDevelop, 'install': PysideInstall, 'install_lib': PysideInstallLib, 'build_rst_docs': PysideRstDocs, } if wheel_module_exists: pyside_bdist_wheel = get_bdist_wheel_override() if pyside_bdist_wheel: cmd_class_dict['bdist_wheel'] = pyside_bdist_wheel