diff options
Diffstat (limited to 'build_scripts')
-rw-r--r-- | build_scripts/__init__.py | 77 | ||||
-rw-r--r-- | build_scripts/build_info_collector.py | 311 | ||||
-rw-r--r-- | build_scripts/build_scripts.pyproject | 6 | ||||
-rw-r--r-- | build_scripts/config.py | 193 | ||||
-rw-r--r-- | build_scripts/log.py | 15 | ||||
-rw-r--r-- | build_scripts/main.py | 1555 | ||||
-rw-r--r-- | build_scripts/options.py | 603 | ||||
-rw-r--r-- | build_scripts/platforms/__init__.py | 40 | ||||
-rw-r--r-- | build_scripts/platforms/linux.py | 181 | ||||
-rw-r--r-- | build_scripts/platforms/macos.py | 181 | ||||
-rw-r--r-- | build_scripts/platforms/unix.py | 340 | ||||
-rw-r--r-- | build_scripts/platforms/windows_desktop.py | 490 | ||||
-rw-r--r-- | build_scripts/qfp_tool.py (renamed from build_scripts/qp5_tool.py) | 223 | ||||
-rw-r--r-- | build_scripts/qtinfo.py | 488 | ||||
-rw-r--r-- | build_scripts/setup_runner.py | 252 | ||||
-rw-r--r-- | build_scripts/utils.py | 1015 | ||||
-rw-r--r-- | build_scripts/wheel_files.py | 1036 | ||||
-rw-r--r-- | build_scripts/wheel_override.py | 287 | ||||
-rw-r--r-- | build_scripts/wheel_utils.py | 124 |
19 files changed, 4739 insertions, 2678 deletions
diff --git a/build_scripts/__init__.py b/build_scripts/__init__.py index 571d37492..128bb2394 100644 --- a/build_scripts/__init__.py +++ b/build_scripts/__init__.py @@ -1,38 +1,39 @@ -############################################################################# -## -## 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$ -## -############################################################################# +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +PYSIDE = 'pyside6' +PYSIDE_MODULE = 'PySide6' +SHIBOKEN = 'shiboken6' + +PYSIDE_PYTHON_TOOLS = ["metaobjectdump", + "deploy", + "android_deploy", + "project", + "qml", + "qtpy2cpp", + "genpyi"] + +PYSIDE_UNIX_BIN_TOOLS = ["lupdate", + "lrelease", + "qmllint", + "qmlformat", + "qmlls", + "qsb", + "balsam", + "balsamui"] + +# tools that are bundled as .app in macOS, but are normal executables in Linux and Windows +PYSIDE_UNIX_BUNDLED_TOOLS = ["assistant", + "designer", + "linguist"] + +PYSIDE_LINUX_BIN_TOOLS = PYSIDE_UNIX_BIN_TOOLS + PYSIDE_UNIX_BUNDLED_TOOLS + +PYSIDE_UNIX_LIBEXEC_TOOLS = ["uic", + "rcc", + "qmltyperegistrar", + "qmlimportscanner", + "qmlcachegen"] + +# all Qt tools are in 'bin' folder in Windows +PYSIDE_WINDOWS_BIN_TOOLS = PYSIDE_UNIX_LIBEXEC_TOOLS + PYSIDE_LINUX_BIN_TOOLS diff --git a/build_scripts/build_info_collector.py b/build_scripts/build_info_collector.py new file mode 100644 index 000000000..30ce187c8 --- /dev/null +++ b/build_scripts/build_info_collector.py @@ -0,0 +1,311 @@ +# Copyright (C) 2021 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import os +import platform +import sys +import sysconfig +from pathlib import Path +from sysconfig import get_config_var + +from setuptools.errors import SetupError + +from .log import log +from .options import OPTION +from .qtinfo import QtInfo +from .utils import configure_cmake_project, parse_cmake_project_message_info +from .wheel_utils import get_qt_version + + +# Return a prefix suitable for the _install/_build directory +def prefix(): + virtual_env_name = os.environ.get('VIRTUAL_ENV', None) + has_virtual_env = False + if virtual_env_name is not None: + name = Path(virtual_env_name).name + has_virtual_env = True + else: + name = "qfp" + if OPTION["DEBUG"]: + name += "d" + if is_debug_python(): + name += "p" + if OPTION["LIMITED_API"] == "yes": + name += "a" + return Path(name), has_virtual_env + + +def is_debug_python(): + return getattr(sys, "gettotalrefcount", None) is not None + + +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 Path(py_include_dir).exists(): + py_include_dir = Path(py_prefix) / "include" + if py_libdir is None or not Path(py_libdir).exists(): + # 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 = Path(py_include_dir).parent / "libs" + if not py_libdir.is_dir(): + raise SetupError("Failed to locate the 'libs' directory") + dbg_postfix = "_d" if build_type == "Debug" else "" + if OPTION["MAKESPEC"] == "mingw": + static_lib_name = f"libpython{py_version.replace('.', '')}{dbg_postfix}.a" + return Path(py_libdir) / static_lib_name + v = py_version.replace(".", "") + python_lib_name = f"python{v}{dbg_postfix}.lib" + return Path(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 Path(py_libdir).exists(): + py_libdir = Path(py_prefix) / "lib" + if py_include_dir is None or not Path(py_include_dir).exists(): + directory = f"include/python{py_version}" + py_include_dir = Path(py_prefix) / directory + lib_exts = ['.so'] + if sys.platform == 'darwin': + lib_exts.append('.dylib') + lib_suff = getattr(sys, 'abiflags', None) + 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') + + libs_tried = [] + for lib_ext in lib_exts: + lib_name = f"libpython{py_version}{lib_suff}{lib_ext}" + py_library = Path(py_libdir) / lib_name + if py_library.exists(): + return py_library + libs_tried.append(py_library) + + # Try to find shared libraries which have a multi arch + # suffix. + py_multiarch = get_config_var("MULTIARCH") + if py_multiarch: + try_py_libdir = Path(py_libdir) / py_multiarch + libs_tried = [] + for lib_ext in lib_exts: + lib_name = f"libpython{py_version}{lib_suff}{lib_ext}" + py_library = try_py_libdir / lib_name + if py_library.exists(): + return py_library + libs_tried.append(py_library) + + # PYSIDE-535: See if this is PyPy. + if hasattr(sys, "pypy_version_info"): + vi = sys.version_info[:2] + version_quirk = ".".join(map(str, vi)) if vi >= (3, 9) else "3" + pypy_libdir = Path(py_libdir).parent / "bin" + for lib_ext in lib_exts: + lib_name = f"libpypy{version_quirk}-c{lib_ext}" + pypy_library = pypy_libdir / lib_name + if pypy_library.exists(): + return pypy_library + libs_tried.append(pypy_library) + _libs_tried = ', '.join(str(lib) for lib in libs_tried) + raise SetupError(f"Failed to locate the Python library with {_libs_tried}") + + +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 str(py_library).endswith('.a'): + # Python was compiled as a static library + log.error(f"Failed to locate a dynamic Python library, using {py_library}") + return py_library + + +class BuildInfoCollectorMixin(object): + build_base: str + build_lib: str + cmake: str + cmake_toolchain_file: str + internal_cmake_install_dir_query_file_path: str + is_cross_compile: bool + plat_name: str + python_target_path: str + + def __init__(self): + pass + + def collect_and_assign(self): + script_dir = Path.cwd() + + # build_base is not set during install command, so we default to + # the 'build command's build_base value ourselves. + build_base = self.build_base + if not build_base: + self.build_base = "build" + build_base = self.build_base + + sources_dir = script_dir / "sources" + + if self.is_cross_compile: + config_tests_dir = script_dir / build_base / "config.tests" + python_target_info_dir = (sources_dir / "shiboken6" / "config.tests" + / "target_python_info") + cmake_cache_args = [] + + if self.python_target_path: + cmake_cache_args.append(("Python_ROOT_DIR", self.python_target_path)) + + if self.cmake_toolchain_file: + cmake_cache_args.append(("CMAKE_TOOLCHAIN_FILE", self.cmake_toolchain_file)) + python_target_info_output = configure_cmake_project( + python_target_info_dir, + self.cmake, + temp_prefix_build_path=config_tests_dir, + cmake_cache_args=cmake_cache_args) + python_target_info = parse_cmake_project_message_info(python_target_info_output) + self.python_target_info = python_target_info + + build_type = "Debug" if OPTION["DEBUG"] else "Release" + if OPTION["RELWITHDEBINFO"]: + build_type = 'RelWithDebInfo' + + # Prepare parameters + if not self.is_cross_compile: + platform_arch = platform.architecture()[0] + self.py_arch = platform_arch[:-3] + + py_executable = sys.executable + _major, _minor, *_ = sys.version_info + py_version = f"{_major}.{_minor}" + py_include_dir = get_config_var("INCLUDEPY") + py_libdir = get_config_var("LIBDIR") + # sysconfig.get_config_var('prefix') returned the + # virtual environment base directory, but + # sysconfig.get_config_var returns the system's prefix. + # We use 'base' instead (although, platbase points to the + # same location) + py_prefix = get_config_var("base") + if not py_prefix or not Path(py_prefix).exists(): + py_prefix = sys.prefix + self.py_prefix = py_prefix + py_prefix = Path(py_prefix) + if sys.platform == "win32": + py_scripts_dir = py_prefix / "Scripts" + else: + py_scripts_dir = py_prefix / "bin" + self.py_scripts_dir = py_scripts_dir + else: + # We don't look for an interpreter when cross-compiling. + py_executable = None + + python_info = self.python_target_info['python_info'] + py_version = python_info['version'].split('.') + py_version = f"{py_version[0]}.{py_version[1]}" + py_include_dir = python_info['include_dirs'] + py_libdir = python_info['library_dirs'] + py_library = python_info['libraries'] + self.py_library = py_library + + # Prefix might not be set because the project that extracts + # the info is using internal API to get it. It shouldn't be + # critical though, because we don't really use neither + # py_prefix nor py_scripts_dir in important places + # when cross-compiling. + if 'prefix' in python_info: + py_prefix = python_info['prefix'] + self.py_prefix = Path(py_prefix).resolve() + + py_scripts_dir = self.py_prefix / 'bin' + if py_scripts_dir.exists(): + self.py_scripts_dir = py_scripts_dir + else: + self.py_scripts_dir = None + else: + py_prefix = None + self.py_prefix = py_prefix + self.py_scripts_dir = None + + self.qtinfo = QtInfo() + qt_version = get_qt_version() + + # Used for test blacklists and registry test. + if self.is_cross_compile: + # Querying the host platform architecture makes no sense when cross-compiling. + build_classifiers = f"py{py_version}-qt{qt_version}-{self.plat_name}-" + else: + build_classifiers = f"py{py_version}-qt{qt_version}-{platform.architecture()[0]}-" + if hasattr(sys, "pypy_version_info"): + pypy_version = ".".join(map(str, sys.pypy_version_info[:3])) + build_classifiers += f"pypy.{pypy_version}-" + build_classifiers += f"{build_type.lower()}" + self.build_classifiers = build_classifiers + + venv_prefix, has_virtual_env = prefix() + + # The virtualenv name serves as the base of the build dir + # and we consider it is distinct enough that we don't have to + # append the build classifiers, thus keeping dir names shorter. + build_name = f"{venv_prefix}" + if self.is_cross_compile and has_virtual_env: + build_name += f"-{self.plat_name}" + + # If short paths are requested and no virtual env is found, at + # least append the python version for more uniqueness. + if OPTION["SHORTER_PATHS"] and not has_virtual_env: + build_name += f"-p{py_version}" + # If no virtual env is found, use build classifiers for + # uniqueness. + elif not has_virtual_env: + build_name += f"-{self.build_classifiers}" + + common_prefix_dir = script_dir / build_base + build_dir = common_prefix_dir / build_name / "build" + install_dir = common_prefix_dir / build_name / "install" + + # Change the setuptools build_lib dir to be under the same + # directory where the cmake build and install dirs are so + # there's a common subdirectory for all build-related dirs. + # Example: + # Replaces + # build/lib.macosx-10.14-x86_64-3.7' with + # build/{venv_prefix}/package' + setup_tools_build_lib_dir = common_prefix_dir / build_name / "package" + self.build_lib = setup_tools_build_lib_dir + + self.script_dir = Path(script_dir) + self.sources_dir = Path(sources_dir) + self.build_dir = Path(build_dir) + self.install_dir = Path(install_dir) + self.py_executable = Path(py_executable) if py_executable else None + self.py_include_dir = Path(py_include_dir) + + if not self.is_cross_compile: + 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 + + if self.is_cross_compile: + site_packages_no_prefix = self.python_target_info['python_info']['site_packages_dir'] + self.site_packages_dir = install_dir / site_packages_no_prefix + else: + # Setuptools doesn't have an equivalent of a get_python_lib with a + # prefix, so we build the path manually: + # self.site_packages_dir = sconfig.get_python_lib(1, 0, prefix=install_dir) + _base = sysconfig.get_paths()["data"] + _purelib = sysconfig.get_paths()["purelib"] + assert _base in _purelib + self.site_packages_dir = f"{install_dir}{_purelib.replace(_base, '')}" + + def post_collect_and_assign(self): + # self.build_lib is only available after the base class + # finalize_options is called. + self.st_build_dir = self.script_dir / self.build_lib diff --git a/build_scripts/build_scripts.pyproject b/build_scripts/build_scripts.pyproject index 604419c10..77f1d0485 100644 --- a/build_scripts/build_scripts.pyproject +++ b/build_scripts/build_scripts.pyproject @@ -1,6 +1,8 @@ { - "files": ["main.py", "__init__.py", "config.py", "options.py", "qtinfo.py", - "setup_runner.py", "utils.py", "wheel_override.py", + "files": ["main.py", "__init__.py", "build_info_collector.py", + "config.py", "options.py", "qtinfo.py", + "setup_runner.py", "utils.py", + "wheel_files.py", "wheel_override.py", "wheel_utils.py", "platforms/__init__.py", "platforms/linux.py", "platforms/macos.py", "platforms/unix.py", "platforms/windows_desktop.py", diff --git a/build_scripts/config.py b/build_scripts/config.py index 4ec2af3de..0a6eebf78 100644 --- a/build_scripts/config.py +++ b/build_scripts/config.py @@ -1,44 +1,12 @@ -############################################################################# -## -## 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$ -## -############################################################################# - -import os -import distutils.log as log +# Copyright (C) 2018 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import sys +from .log import log, LogLevel +from pathlib import Path + +from . import PYSIDE, PYSIDE_MODULE, SHIBOKEN +from .utils import available_pyside_tools class Config(object): @@ -57,11 +25,11 @@ class Config(object): self.invocation_type = None # The type of the top-level build. - # all - build shiboken2 module, shiboken2-generator and PySide2 + # all - build shiboken6 module, shiboken6-generator and PySide6 # modules - # shiboken2 - build only shiboken2 module - # shiboken2-generator - build only the shiboken2-generator - # pyside2 - build only PySide2 modules + # shiboken6 - build only shiboken6 module + # shiboken6-generator - build only the shiboken6-generator + # pyside6 - build only PySide6 modules self.build_type = None # The internal build type, used for internal invocations of @@ -70,36 +38,47 @@ class Config(object): # Options that can be given to --build-type and # --internal-build-type - self.shiboken_module_option_name = "shiboken2" - self.shiboken_generator_option_name = "shiboken2-generator" - self.pyside_option_name = "pyside2" + self.shiboken_module_option_name = SHIBOKEN + self.shiboken_generator_option_name = f"{SHIBOKEN}-generator" + self.pyside_option_name = PYSIDE # Names to be passed to setuptools.setup() name key, # so not package name, but rather project name as it appears # in the wheel name and on PyPi. - self.shiboken_module_st_name = "shiboken2" - self.shiboken_generator_st_name = "shiboken2-generator" - self.pyside_st_name = "PySide2" + self.shiboken_module_st_name = SHIBOKEN + self.shiboken_generator_st_name = f"{SHIBOKEN}-generator" + self.pyside_st_name = PYSIDE_MODULE + + # Path to CMake toolchain file when intending to cross compile + # the project. + self.cmake_toolchain_file = None + + # Store where host shiboken is built during a cross-build. + self.shiboken_host_query_path = None # Used by check_allowed_python_version to validate the # interpreter version. self.python_version_classifiers = [ 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', ] self.setup_script_dir = None - def init_config(self, build_type=None, internal_build_type=None, - cmd_class_dict=None, package_version=None, - ext_modules=None, setup_script_dir=None, - quiet=False): + def init_config(self, + build_type=None, + internal_build_type=None, + cmd_class_dict=None, + package_version=None, + ext_modules=None, + setup_script_dir=None, + cmake_toolchain_file=None, + log_level=LogLevel.INFO, + qt_install_path: Path = None): """ Sets up the global singleton config which is used in many parts of the setup process. @@ -120,7 +99,9 @@ class Config(object): else: self.build_type = self._build_type_all - self.setup_script_dir = setup_script_dir + self.setup_script_dir = Path(setup_script_dir) + + self.cmake_toolchain_file = cmake_toolchain_file setup_kwargs = {} setup_kwargs['long_description'] = self.get_long_description() @@ -134,11 +115,10 @@ class Config(object): setup_kwargs['zip_safe'] = False setup_kwargs['cmdclass'] = cmd_class_dict setup_kwargs['version'] = package_version - setup_kwargs['python_requires'] = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.9" + setup_kwargs['python_requires'] = ">=3.9, <3.13" - - if quiet: - # Tells distutils / setuptools to be quiet, and only print warnings or errors. + if log_level == LogLevel.QUIET: + # Tells setuptools to be quiet, and only print warnings or errors. # Makes way less noise in the terminal when building. setup_kwargs['verbose'] = 0 @@ -192,6 +172,8 @@ class Config(object): 'Topic :: Software Development :: Widget Sets']) setup_kwargs['classifiers'] = common_classifiers + package_name = self.package_name() + if self.internal_build_type == self.shiboken_module_option_name: setup_kwargs['name'] = self.shiboken_module_st_name setup_kwargs['description'] = "Python / C++ bindings helper module" @@ -200,25 +182,39 @@ class Config(object): elif self.internal_build_type == self.shiboken_generator_option_name: setup_kwargs['name'] = self.shiboken_generator_st_name setup_kwargs['description'] = "Python / C++ bindings generator" - setup_kwargs['install_requires'] = ["{}=={}".format(self.shiboken_module_st_name, package_version)] + setup_kwargs['install_requires'] = [ + f"{self.shiboken_module_st_name}=={package_version}" + ] setup_kwargs['entry_points'] = { 'console_scripts': [ - 'shiboken2 = {}.scripts.shiboken_tool:main'.format(self.package_name()), + f'{SHIBOKEN} = {package_name}.scripts.shiboken_tool:main', + f'{SHIBOKEN}-genpyi = {package_name}.scripts.shiboken_tool:genpyi', ] } elif self.internal_build_type == self.pyside_option_name: setup_kwargs['name'] = self.pyside_st_name - setup_kwargs['description'] = "Python bindings for the Qt cross-platform application and UI framework" - setup_kwargs['install_requires'] = ["{}=={}".format(self.shiboken_module_st_name, package_version)] - setup_kwargs['entry_points'] = { - 'console_scripts': [ - 'pyside2-uic = {}.scripts.pyside_tool:uic'.format(self.package_name()), - 'pyside2-rcc = {}.scripts.pyside_tool:rcc'.format(self.package_name()), - 'pyside2-designer= {}.scripts.pyside_tool:designer'.format(self.package_name()), - 'pyside2-lupdate = {}.scripts.pyside_tool:main'.format(self.package_name()), - ] - } + setup_kwargs['description'] = ("Python bindings for the Qt cross-platform application " + "and UI framework") + setup_kwargs['install_requires'] = [ + f"{self.shiboken_module_st_name}=={package_version}" + ] + if qt_install_path: + _pyside_tools = available_pyside_tools(qt_tools_path=qt_install_path) + + # replacing pyside6-android_deploy by pyside6-android-deploy for consistency + # Also, the tool should not exist in any other platform than Linux + _console_scripts = [] + if ("android_deploy" in _pyside_tools) and sys.platform.startswith("linux"): + _console_scripts = [(f"{PYSIDE}-android-deploy =" + " PySide6.scripts.pyside_tool:android_deploy")] + _pyside_tools.remove("android_deploy") + + _console_scripts.extend([f'{PYSIDE}-{tool} = {package_name}.scripts.pyside_tool:' + f'{tool}' for tool in _pyside_tools]) + + setup_kwargs['entry_points'] = {'console_scripts': _console_scripts} + self.setup_kwargs = setup_kwargs def get_long_description(self): @@ -226,19 +222,19 @@ class Config(object): changes_filename = 'CHANGES.rst' if self.is_internal_shiboken_module_build(): - readme_filename = 'README.shiboken2.md' + readme_filename = f'README.{SHIBOKEN}.md' elif self.is_internal_shiboken_generator_build(): - readme_filename = 'README.shiboken2-generator.md' + readme_filename = f'README.{SHIBOKEN}-generator.md' elif self.is_internal_pyside_build(): - readme_filename = 'README.pyside2.md' + readme_filename = f'README.{PYSIDE}.md' content = '' changes = '' try: - with open(os.path.join(self.setup_script_dir, readme_filename)) as f: + with open(self.setup_script_dir / readme_filename) as f: readme = f.read() except Exception as e: - log.error("Couldn't read contents of {}.".format(readme_filename)) + log.error(f"Couldn't read contents of {readme_filename}. {e}") raise # Don't include CHANGES.rst for now, because we have not decided @@ -246,15 +242,15 @@ class Config(object): include_changes = False if include_changes: try: - with open(os.path.join(self.setup_script_dir, changes_filename)) as f: + with open(self.setup_script_dir / changes_filename) as f: changes = f.read() except Exception as e: - log.error("Couldn't read contents of {}".format(changes_filename)) + log.error(f"Couldn't read contents of {changes_filename}. {e}") raise content += readme if changes: - content += "\n\n" + changes + content += f"\n\n{changes}" return content @@ -267,11 +263,11 @@ class Config(object): dashes. """ if self.is_internal_shiboken_module_build(): - return "shiboken2" + return SHIBOKEN elif self.is_internal_shiboken_generator_build(): - return "shiboken2_generator" + return f"{SHIBOKEN}_generator" elif self.is_internal_pyside_build(): - return "PySide2" + return PYSIDE_MODULE else: return None @@ -301,8 +297,8 @@ class Config(object): the actual module packages are located. For example when building the shiboken module, setuptools will - expect to find the "shiboken2" module sources under - "sources/shiboken2/shibokenmodule". + expect to find the "shiboken6" module sources under + "sources/{SHIBOKEN}/shibokenmodule". This is really just to satisfy some checks in setuptools build_py command, and if we ever properly implement the develop @@ -310,7 +306,7 @@ class Config(object): """ if self.is_internal_shiboken_module_build(): return { - self.package_name(): "sources/shiboken2/shibokenmodule" + self.package_name(): f"sources/{SHIBOKEN}/shibokenmodule" } elif self.is_internal_shiboken_generator_build(): # This is left empty on purpose, because the shiboken @@ -318,7 +314,7 @@ class Config(object): return {} elif self.is_internal_pyside_build(): return { - self.package_name(): "sources/pyside2/PySide2", + self.package_name(): f"sources/{PYSIDE}/{PYSIDE_MODULE}", } else: return {} @@ -329,9 +325,9 @@ class Config(object): :return: A list of directory names under the sources directory. """ if self.is_internal_shiboken_module_build() or self.is_internal_shiboken_generator_build(): - return ['shiboken2'] + return [SHIBOKEN] elif self.is_internal_pyside_build(): - return ['pyside2', 'pyside2-tools'] + return [PYSIDE, 'pyside-tools'] return None def set_is_top_level_invocation(self): @@ -358,6 +354,11 @@ class Config(object): def is_top_level_build_pyside(self): return self.build_type == self.pyside_option_name + def is_cross_compile(self): + if not self.cmake_toolchain_file: + return False + return True + def set_internal_build_type(self, internal_build_type): self.internal_build_type = internal_build_type @@ -374,7 +375,7 @@ class Config(object): """ Used to skip certain build rules and output, when we know that the CMake build of shiboken was already done as part of the - top-level "all" build when shiboken2-module was built. + top-level "all" build when shiboken6-module was built. """ return self.is_internal_shiboken_generator_build() and self.is_top_level_build_all() diff --git a/build_scripts/log.py b/build_scripts/log.py new file mode 100644 index 000000000..c9ccf3fb9 --- /dev/null +++ b/build_scripts/log.py @@ -0,0 +1,15 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import logging + +from enum import Enum + +logging.basicConfig(format="[%(levelname)s]: %(message)s", level=logging.INFO) +log = logging.getLogger("qtforpython") + + +class LogLevel(Enum): + QUIET = 1 + INFO = 2 + VERBOSE = 3 diff --git a/build_scripts/main.py b/build_scripts/main.py index 55cc6a882..9a8d4fb3f 100644 --- a/build_scripts/main.py +++ b/build_scripts/main.py @@ -1,91 +1,60 @@ -############################################################################# -## -## 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 +# Copyright (C) 2018 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +import importlib import os +import platform +import re +import sys +import sysconfig import time -from .config import config -from .utils import memoize, get_python_dict -from .options import OPTION - -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()) - +from packaging.version import parse as parse_version +from pathlib import Path +from shutil import copytree, rmtree +from textwrap import dedent -def elapsed(): - return int(time.time()) - start_time +# PYSIDE-1760: Pre-load setuptools modules early to avoid racing conditions. +# may be touched (should be avoided anyway, btw.) +# Note: This bug is only visible when tools like pyenv are not used. They have some +# pre-loading effect so that setuptools is already in the cache, hiding the problem. +from setuptools import Command, Extension +from setuptools.command.bdist_egg import bdist_egg as _bdist_egg +from setuptools.command.build_ext import build_ext as _build_ext +from setuptools.command.build_py import build_py as _build_py +from setuptools.command.build import build as _build +from setuptools.command.develop import develop as _develop +from setuptools.command.install import install as _install +from setuptools.command.install_lib import install_lib as _install_lib +from setuptools.command.install_scripts import install_scripts # noqa: preload only +from .log import log, LogLevel +from setuptools.errors import SetupError -@memoize -def get_package_timestamp(): - """ In a Coin CI build the returned timestamp will be the - Coin integration id timestamp. For regular builds it's - just the current timestamp or a user provided one.""" - return OPTION["PACKAGE_TIMESTAMP"] if OPTION["PACKAGE_TIMESTAMP"] else start_time +from .build_info_collector import BuildInfoCollectorMixin +from .config import config +from .options import OPTION, CommandMixin +from .platforms.unix import prepare_packages_posix +from .platforms.windows_desktop import prepare_packages_win32 +from .qtinfo import QtInfo +from .utils import (copydir, copyfile, detect_clang, + get_numpy_location, get_python_dict, + linux_fix_rpaths_for_library, macos_fix_rpaths_for_library, + platform_cmake_options, remove_tree, run_process, + run_process_output, update_env_path, which) +from . import PYSIDE, PYSIDE_MODULE, SHIBOKEN +from .wheel_override import get_bdist_wheel_override, wheel_module_exists +from .wheel_utils import (get_package_timestamp, get_package_version, + macos_plat_name, macos_pyside_min_deployment_target) +setup_script_dir = Path.cwd() +build_scripts_dir = setup_script_dir / 'build_scripts' +setup_py_path = setup_script_dir / "setup.py" -@memoize -def get_package_version(): - """ Returns the version string for the PySide2 package. """ - pyside_version_py = os.path.join( - setup_script_dir, "sources", "pyside2", "pyside_version.py") - d = get_python_dict(pyside_version_py) +start_time = time.time() - final_version = "{}.{}.{}".format( - d['major_version'], d['minor_version'], d['patch_version']) - release_version_type = d['release_version_type'] - pre_release_version = d['pre_release_version'] - if pre_release_version and release_version_type: - final_version += release_version_type + pre_release_version - if release_version_type.startswith("comm"): - final_version += "." + release_version_type - # Add the current timestamp to the version number, to suggest it - # is a development snapshot build. - if OPTION["SNAPSHOT_BUILD"]: - final_version += ".dev{}".format(get_package_timestamp()) - return final_version +def elapsed(): + return int(time.time() - start_time) def get_setuptools_extension_modules(): @@ -95,59 +64,58 @@ def get_setuptools_extension_modules(): # future. extension_args = ('QtCore', []) extension_kwargs = {} - if OPTION["LIMITED_API"]: + if OPTION["LIMITED_API"] == 'yes': extension_kwargs['py_limited_api'] = True extension_modules = [Extension(*extension_args, **extension_kwargs)] return extension_modules -# 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() - -import sys -import platform -import re - -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 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 - -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 - -from textwrap import dedent - - -def check_allowed_python_version(): - """ - Make sure that setup.py is run with an allowed python version. - """ - - import re +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": + if not OPTION["NO_JOM"]: + jom_path = Path(which("jom")) + if jom_path: + log.info(f"jom was found in {jom_path}") + return (jom_path, "NMake Makefiles JOM") + nmake_path = Path(which("nmake")) + if nmake_path is None or not nmake_path.exists(): + raise SetupError("nmake not found") + log.info(f"nmake was found in {nmake_path}") + if OPTION["JOBS"]: + msg = "Option --jobs can only be used with 'jom' on Windows." + raise SetupError(msg) + return (nmake_path, "NMake Makefiles") + if makespec == "mingw": + return (Path("mingw32-make"), "mingw32-make") + if makespec == "ninja": + return (Path("ninja"), "Ninja") + raise SetupError(f'Invalid option --make-spec "{makespec}".') + + +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 make_path.is_absolute(): + found_path = Path(which(make_path)) + if not found_path or not found_path.exists(): + m = (f"You need the program '{make_path}' on your system path to " + f"compile {PYSIDE_MODULE}.") + raise SetupError(m) + make_path = found_path + return (make_path, make_generator) + + +_allowed_versions_cache = None + + +def get_allowed_python_versions(): + global _allowed_versions_cache + if _allowed_versions_cache is not None: + return _allowed_versions_cache pattern = r'Programming Language :: Python :: (\d+)\.(\d+)' supported = [] @@ -157,206 +125,77 @@ def check_allowed_python_version(): 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) + _allowed_versions_cache = sorted(supported) + return _allowed_versions_cache -qt_src_dir = '' -if OPTION["QT_VERSION"] is None: - OPTION["QT_VERSION"] = "5" -if OPTION["QMAKE"] is None: - OPTION["QMAKE"] = find_executable("qmake-qt5") -if OPTION["QMAKE"] is None: - OPTION["QMAKE"] = find_executable("qmake") - -# make qtinfo.py independent of relative paths. -if OPTION["QMAKE"] is not None and os.path.exists(OPTION["QMAKE"]): - OPTION["QMAKE"] = os.path.abspath(OPTION["QMAKE"]) -if OPTION["CMAKE"] is not None and os.path.exists(OPTION["CMAKE"]): - OPTION["CMAKE"] = os.path.abspath(OPTION["CMAKE"]) - -QMAKE_COMMAND = None -# Checking whether qmake executable exists -if OPTION["QMAKE"] is not None and os.path.exists(OPTION["QMAKE"]): - # Looking whether qmake path is a link and whether the link exists - if os.path.islink(OPTION["QMAKE"]) and os.path.lexists(OPTION["QMAKE"]): - # Set -qt=X here. - if "qtchooser" in os.readlink(OPTION["QMAKE"]): - QMAKE_COMMAND = [OPTION["QMAKE"], "-qt={}".format(OPTION["QT_VERSION"])] -if not QMAKE_COMMAND: - QMAKE_COMMAND = [OPTION["QMAKE"]] - -if len(QMAKE_COMMAND) == 0 or QMAKE_COMMAND[0] is None: - print("qmake could not be found.") - sys.exit(1) -if not os.path.exists(QMAKE_COMMAND[0]): - print("'{}' does not exist.".format(QMAKE_COMMAND[0])) - sys.exit(1) -if OPTION["CMAKE"] is None: - OPTION["CMAKE"] = find_executable("cmake") - -if OPTION["CMAKE"] is None: - print("cmake could not be found.") - sys.exit(1) -if not os.path.exists(OPTION["CMAKE"]): - print("'{}' does not exist.".format(OPTION["CMAKE"])) - sys.exit(1) - -# First element is default -available_mkspecs = ["msvc", "mingw", "ninja"] if sys.platform == "win32" else ["make", "ninja"] - -if OPTION["MAKESPEC"] is None: - OPTION["MAKESPEC"] = available_mkspecs[0] - -if OPTION["MAKESPEC"] not in available_mkspecs: - print('Invalid option --make-spec "{}". Available values are {}'.format(OPTION["MAKESPEC"], - available_mkspecs)) - sys.exit(1) - -if OPTION["JOBS"]: - if sys.platform == 'win32' and OPTION["NO_JOM"]: - print("Option --jobs can only be used with jom on Windows.") - sys.exit(1) - else: - if not OPTION["JOBS"].startswith('-j'): - OPTION["JOBS"] = '-j' + OPTION["JOBS"] -else: - OPTION["JOBS"] = '' - - -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) - - -# Single global instance of QtInfo to be used later in multiple code -# paths. -qtinfo = QtInfo(QMAKE_COMMAND) - - -def get_qt_version(): - qt_version = qtinfo.version - - if not qt_version: - log.error("Failed to query the Qt version with qmake {0}".format(qtinfo.qmake_command)) - sys.exit(1) +def check_allowed_python_version(): + """ + Make sure that setup.py is run with an allowed python version. + """ - if LooseVersion(qtinfo.version) < LooseVersion("5.7"): - log.error("Incompatible Qt version detected: {}. A Qt version >= 5.7 is " - "required.".format(qt_version)) + supported = get_allowed_python_versions() + this_py = sys.version_info[:2] + if this_py not in supported: + log.error(f"Unsupported python version detected. Supported versions: {supported}") sys.exit(1) - return qt_version +qt_src_dir = '' -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)) +def prepare_build(): # locate Qt sources for the documentation if OPTION["QT_SRC"] is None: - install_prefix = qtinfo.prefix_dir + 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') + maybe_qt_src_dir = Path(install_prefix).parent / 'Src' / 'qtbase' + if maybe_qt_src_dir.exists(): + qt_src_dir = maybe_qt_src_dir -class PysideInstall(_install): +class PysideInstall(_install, CommandMixin): + + user_options = _install.user_options + CommandMixin.mixin_user_options + def __init__(self, *args, **kwargs): + self.command_name = "install" _install.__init__(self, *args, **kwargs) + CommandMixin.__init__(self) def initialize_options(self): _install.initialize_options(self) - if sys.platform == 'darwin': + def finalize_options(self): + CommandMixin.mixin_finalize_options(self) + _install.finalize_options(self) + + if sys.platform == 'darwin' or self.is_cross_compile: # Because we change the plat_name to include a correct - # deployment target on macOS distutils thinks we are + # deployment target on macOS setuptools 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") + # raise PlatformError("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. + # We also do it when cross-compiling. While calling install + # command directly is dubious, bdist_wheel calls install + # internally before creating a wheel. self.warn_dir = False def run(self): _install.run(self) - print('*** Install completed ({}s)'.format(elapsed())) + log.info(f"--- Install completed ({elapsed()}s)") class PysideDevelop(_develop): @@ -391,11 +230,12 @@ class PysideBuildExt(_build_ext): class PysideBuildPy(_build_py): def __init__(self, *args, **kwargs): + self.command_name = "build_py" _build_py.__init__(self, *args, **kwargs) # _install_lib is reimplemented to preserve -# symlinks when distutils / setuptools copy files to various +# symlinks when setuptools copy files to various # directories from the setup tools build dir to the install dir. class PysideInstallLib(_install_lib): @@ -404,30 +244,45 @@ class PysideInstallLib(_install_lib): def install(self): """ - Installs files from build/xxx directory into final - site-packages/PySide2 directory. + Installs files from self.build_dir directory into final + site-packages/PySide6 directory when the command is 'install' + or into build/wheel when command is 'bdist_wheel'. """ - if os.path.isdir(self.build_dir): + if self.build_dir.is_dir(): # Using our own copydir makes sure to preserve symlinks. - outfiles = copydir(os.path.abspath(self.build_dir), os.path.abspath(self.install_dir)) + outfiles = copydir(Path(self.build_dir).resolve(), Path(self.install_dir).resolve()) else: - self.warn("'{}' does not exist -- no Python modules to install".format(self.build_dir)) + self.warn(f"'{self.build_dir}' does not exist -- no Python modules to install") return return outfiles -class PysideBuild(_build): +class PysideBuild(_build, CommandMixin, BuildInfoCollectorMixin): + + user_options = _build.user_options + CommandMixin.mixin_user_options def __init__(self, *args, **kwargs): + self.command_name = "build" _build.__init__(self, *args, **kwargs) + CommandMixin.__init__(self) + BuildInfoCollectorMixin.__init__(self) def finalize_options(self): os_name_backup = os.name - if sys.platform == 'darwin': - self.plat_name = PysideBuild.macos_plat_name() + CommandMixin.mixin_finalize_options(self) + BuildInfoCollectorMixin.collect_and_assign(self) + + use_os_name_hack = False + if self.is_cross_compile: + use_os_name_hack = True + elif sys.platform == 'darwin': + self.plat_name = macos_plat_name() + use_os_name_hack = True + + if use_os_name_hack: # This is a hack to circumvent the dubious check in - # distutils.commands.build -> finalize_options, which only + # setuptool.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 @@ -437,14 +292,16 @@ class PysideBuild(_build): _build.finalize_options(self) - if sys.platform == 'darwin': + # Must come after _build.finalize_options + BuildInfoCollectorMixin.post_collect_and_assign(self) + + if use_os_name_hack: os.name = os_name_backup def initialize_options(self): _build.initialize_options(self) self.make_path = None self.make_generator = None - self.debug = False self.script_dir = None self.sources_dir = None self.build_dir = None @@ -457,256 +314,76 @@ class PysideBuild(_build): self.build_type = "Release" self.qtinfo = None self.build_tests = False + self.python_target_info = {} 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"]: - if OPTION["MAKESPEC"] == "make": - make_name = "make" - make_generator = "Unix Makefiles" - elif OPTION["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") - assert(nmake_path is not None and os.path.exists(nmake_path)) - jom_path = None if OPTION["NO_JOM"] else find_executable("jom") - if jom_path is not None and os.path.exists(jom_path): - log.info("jom was found in {}".format(jom_path)) - make_name = "jom" - make_generator = "NMake Makefiles JOM" - else: - log.info("nmake was found in {}".format(nmake_path)) - make_name = "nmake" - make_generator = "NMake Makefiles" - if OPTION["JOBS"]: - msg = "Option --jobs can only be used with 'jom' on Windows." - raise DistutilsSetupError(msg) - elif OPTION["MAKESPEC"] == "mingw": - make_name = "mingw32-make" - make_generator = "MinGW Makefiles" - elif OPTION["MAKESPEC"] == "ninja": - make_name = "ninja" - make_generator = "Ninja" - else: - raise DistutilsSetupError("Invalid option --make-spec.") - make_path = find_executable(make_name) - if make_path is None or not os.path.exists(make_path): - raise DistutilsSetupError("You need the program '{}' on your system path to " - "compile PySide2.".format(make_name)) - - if OPTION["CMAKE"] is None or not os.path.exists(OPTION["CMAKE"]): - raise DistutilsSetupError("Failed to find cmake." - " Please specify the path to cmake with " - "--cmake parameter.") - - if OPTION["QMAKE"] is None or not os.path.exists(OPTION["QMAKE"]): - raise DistutilsSetupError("Failed to find qmake. " - "Please specify the path to qmake with --qmake parameter.") - - # 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 - if py_libdir is None or not os.path.exists(py_libdir): - if sys.platform == "win32": - # 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") - else: - py_libdir = os.path.join(py_prefix, "lib") - if py_include_dir is None or not os.path.exists(py_include_dir): - if sys.platform == "win32": - py_include_dir = os.path.join(py_prefix, "include") - else: - py_include_dir = os.path.join(py_prefix, "include/python{}".format(py_version)) - dbg_postfix = "" - if build_type == "Debug": - dbg_postfix = "_d" - if sys.platform == "win32": - if OPTION["MAKESPEC"] == "mingw": - static_lib_name = "libpython{}{}.a".format( - py_version.replace(".", ""), dbg_postfix) - py_library = os.path.join(py_libdir, static_lib_name) - else: - python_lib_name = "python{}{}.lib".format( - py_version.replace(".", ""), dbg_postfix) - py_library = os.path.join(py_libdir, python_lib_name) - 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 - - python_library_found = False - 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): - python_library_found = True - break - libs_tried.append(py_library) - else: - # 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)): - py_library = possible_framework_library - python_library_found = True - else: - libs_tried.append(possible_framework_library) - - # Try to find shared libraries which have a multi arch - # suffix. - if not python_library_found: - py_multiarch = get_config_var("MULTIARCH") - if py_multiarch and not python_library_found: - 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): - py_libdir = try_py_libdir - python_library_found = True - break - libs_tried.append(py_library) - - if not python_library_found: - raise DistutilsSetupError( - "Failed to locate the Python library with {}".format(", ".join(libs_tried))) - - 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)) - - self.qtinfo = qtinfo - qt_dir = os.path.dirname(OPTION["QMAKE"]) - qt_version = get_qt_version() + platform_arch = platform.architecture()[0] + (make_path, make_generator) = get_make(platform_arch, self.build_type) + self.qtinfo = QtInfo() # Update the PATH environment variable - additional_paths = [self.py_scripts_dir, qt_dir] + # Don't add Qt to PATH env var, we don't want it to interfere + # with CMake's find_package calls which will use + # CMAKE_PREFIX_PATH. + # Don't add the Python scripts dir to PATH env when + # cross-compiling, it could be in the device sysroot (/usr) + # which can cause CMake device QtFooToolsConfig packages to be + # picked up instead of host QtFooToolsConfig packages. + additional_paths = [] + if self.py_scripts_dir and not self.is_cross_compile: + additional_paths.append(self.py_scripts_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])) + and parse_version(self.qtinfo.version) >= parse_version("5.7.0")): + clang_dir, clang_source = detect_clang() + if clang_dir: + clangBinDir = clang_dir / 'bin' + if str(clangBinDir) not in os.environ.get('PATH'): + log.info(f"Adding {clangBinDir} as detected by {clang_source} to PATH") additional_paths.append(clangBinDir) else: - raise DistutilsSetupError("Failed to detect Clang when checking " - "LLVM_INSTALL_DIR, CLANG_INSTALL_DIR, llvm-config") + raise SetupError("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.debug = OPTION["DEBUG"] - 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 = py_library - 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.shiboken_build_dir = self.build_dir / SHIBOKEN self.log_pre_build_info() # Prepare folders - if not os.path.exists(self.sources_dir): - log.info("Creating sources folder {}...".format(self.sources_dir)) + if not self.sources_dir.exists(): + log.info(f"Creating sources folder {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)) + if not self.build_dir.exists(): + log.info(f"Creating build folder {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)) + if not self.install_dir.exists(): + log.info(f"Creating install folder {self.install_dir}...") os.makedirs(self.install_dir) + # Write the CMake install path into a file. Is used by + # SetupRunner to provide a nicer UX when cross-compiling (no + # need to specify a host shiboken path explicitly) + if self.internal_cmake_install_dir_query_file_path: + with open(self.internal_cmake_install_dir_query_file_path, 'w') as f: + f.write(os.fspath(self.install_dir)) + if (not OPTION["ONLYPACKAGE"] and not config.is_internal_shiboken_generator_build_and_part_of_top_level_all()): # Build extensions @@ -717,14 +394,14 @@ class PysideBuild(_build): # 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') + build_history = setup_script_dir / 'build_history' + unique_dir = build_history / timestamp + unique_dir.mkdir(parents=True) + fpath = unique_dir / 'build_dir.txt' with open(fpath, 'w') as f: - print(build_dir, file=f) + print(self.build_dir, file=f) print(self.build_classifiers, file=f) - log.info("Created {}".format(build_history)) + log.info(f"Created {build_history}") if not OPTION["SKIP_PACKAGING"]: # Build patchelf if needed @@ -735,212 +412,219 @@ class PysideBuild(_build): # Build packages _build.run(self) + + # Keep packaged directories for wheel construction + # This is to take advantage of the packaging step + # to keep the data in the proper structure to create + # a wheel. + _path = Path(self.st_build_dir) + _wheel_path = _path.parent / "package_for_wheels" + + _project = None + + if config.is_internal_shiboken_module_build(): + _project = "shiboken6" + elif config.is_internal_shiboken_generator_build(): + _project = "shiboken6_generator" + elif config.is_internal_pyside_build(): + _project = "PySide6" + + if _project is not None: + if not _wheel_path.exists(): + _wheel_path.mkdir(parents=True) + _src = Path(_path / _project) + _dst = Path(_wheel_path / _project) + # Remove the directory in case it exists. + # This applies to 'shiboken6', 'shiboken6_generator', + # and 'pyside6' inside the 'package_for_wheels' directory. + if _dst.exists(): + log.warning(f'Found directory "{_dst}", removing it first.') + remove_tree(_dst) + + try: + # This should be copied because the package directory + # is used when using the 'install' setup.py instruction. + copytree(_src, _dst) + except Exception as e: + log.warning(f'problem renaming "{self.st_build_dir}"') + log.warning(f'ignored error: {type(e).__name__}: {e}') else: log.info("Skipped preparing and building packages.") - print('*** Build completed ({}s)'.format(elapsed())) + log.info(f"--- Build completed ({elapsed()}s)") 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) + setuptools_install_prefix = sysconfig.get_paths()["purelib"] 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(f"Package version: {get_package_version()}") + log.info(f"Build type: {self.build_type}") + log.info(f"Build tests: {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(f"Make path: {self.make_path}") + log.info(f"Make generator: {self.make_generator}") + log.info(f"Make jobs: {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 + log.info(f"setup.py directory: {self.script_dir}") + log.info(f"Build scripts directory: {build_scripts_dir}") + log.info(f"Sources directory: {self.sources_dir}") + log.info(dedent(f""" + Building {config.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/*/*) -> + make build directory -> + make install directory -> + setuptools build directory -> 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: {} + """)) + log.info(f"make build directory: {self.build_dir}") + log.info(f"make install directory: {self.install_dir}") + log.info(f"setuptools build directory: {self.st_build_dir}") + log.info(f"setuptools install directory: {setuptools_install_prefix}") + log.info(dedent(f""" + make-installed site-packages directory: {self.site_packages_dir} (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(f"Python executable: {self.py_executable}") + log.info(f"Python includes: {self.py_include_dir}") + log.info(f"Python library: {self.py_library}") + log.info(f"Python prefix: {self.py_prefix}") + log.info(f"Python scripts: {self.py_scripts_dir}") + log.info(f"Python arch: {self.py_arch}") + 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(f"Qt prefix: {self.qtinfo.prefix_dir}") + log.info(f"Qt qmake: {self.qtinfo.qmake_command}") + log.info(f"Qt qtpaths: {self.qtinfo.qtpaths_command}") + log.info(f"Qt version: {self.qtinfo.version}") + log.info(f"Qt bins: {self.qtinfo.bins_dir}") + log.info(f"Qt docs: {self.qtinfo.docs_dir}") + log.info(f"Qt plugins: {self.qtinfo.plugins_dir}") log.info("-" * 3) if sys.platform == 'win32': - log.info("OpenSSL dll directory: {}".format(OPTION["OPENSSL"])) + log.info(f"OpenSSL dll directory: {OPTION['OPENSSL']}") if sys.platform == 'darwin': - pyside_macos_deployment_target = ( - PysideBuild.macos_pyside_min_deployment_target() - ) - log.info("MACOSX_DEPLOYMENT_TARGET set to: {}".format( - pyside_macos_deployment_target)) + pyside_macos_deployment_target = (macos_pyside_min_deployment_target()) + log.info(f"MACOSX_DEPLOYMENT_TARGET set to: {pyside_macos_deployment_target}") log.info("=" * 30) - @staticmethod - def macos_qt_min_deployment_target(): - target = qtinfo.macos_min_deployment_target - - if not target: - raise DistutilsSetupError("Failed to query for Qt's QMAKE_MACOSX_DEPLOYMENT_TARGET.") - return target - - @staticmethod - @memoize - def macos_pyside_min_deployment_target(): - """ - Compute and validate PySide2 MACOSX_DEPLOYMENT_TARGET value. - Candidate sources that are considered: - - setup.py provided value - - maximum value between minimum deployment target of the - Python interpreter and the minimum deployment target of - the Qt libraries. - If setup.py value is provided, that takes precedence. - Otherwise use the maximum of the above mentioned two values. - """ - python_target = get_config_var('MACOSX_DEPLOYMENT_TARGET') or None - qt_target = PysideBuild.macos_qt_min_deployment_target() - setup_target = OPTION["MACOS_DEPLOYMENT_TARGET"] - - qt_target_split = [int(x) for x in qt_target.split('.')] - if python_target: - python_target_split = [int(x) for x in python_target.split('.')] - if setup_target: - setup_target_split = [int(x) for x in setup_target.split('.')] - - message = ("Can't set MACOSX_DEPLOYMENT_TARGET value to {} because " - "{} was built with minimum deployment target set to {}.") - # setup.py provided OPTION["MACOS_DEPLOYMENT_TARGET"] value takes - # precedence. - if setup_target: - if python_target and setup_target_split < python_target_split: - raise DistutilsSetupError(message.format(setup_target, "Python", - python_target)) - if setup_target_split < qt_target_split: - raise DistutilsSetupError(message.format(setup_target, "Qt", - qt_target)) - # All checks clear, use setup.py provided value. - return setup_target - - # Setup.py value not provided, - # use same value as provided by Qt. - if python_target: - maximum_target = '.'.join([str(e) for e in max(python_target_split, qt_target_split)]) - else: - maximum_target = qt_target - return maximum_target - - @staticmethod - @memoize - def macos_plat_name(): - deployment_target = PysideBuild.macos_pyside_min_deployment_target() - # Example triple "macosx-10.12-x86_64". - plat = get_platform().split("-") - plat_name = "{}-{}-{}".format(plat[0], deployment_target, plat[2]) - return plat_name - def build_patchelf(self): if not sys.platform.startswith('linux'): return - self._patchelf_path = find_executable('patchelf') + self._patchelf_path = which('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)) + self._patchelf_path = Path(self._patchelf_path) + if not self._patchelf_path.is_absolute(): + self._patchelf_path = Path.cwd() / self._patchelf_path + log.info(f"Using {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") + else: + raise SetupError("patchelf not found") + + def _enable_numpy(self): + if OPTION["ENABLE_NUMPY_SUPPORT"] or OPTION["PYSIDE_NUMPY_SUPPORT"]: + return True + if OPTION["DISABLE_NUMPY_SUPPORT"]: + return False + if self.is_cross_compile: # Do not search header in host Python + return False + # Debug builds require numpy to be built in debug mode on Windows + # https://numpy.org/devdocs/user/troubleshooting-importerror.html + return sys.platform != 'win32' or self.build_type.lower() != 'debug' def build_extension(self, extension): # calculate the subrepos folder name - log.info("Building module {}...".format(extension)) + log.info(f"Building module {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)) + module_build_dir = self.build_dir / extension + skipflag_file = Path(f"{module_build_dir}-skip") + if skipflag_file.exists(): + log.info(f"Skipping {extension} because {skipflag_file} exists") return - module_build_exists = os.path.exists(module_build_dir) + module_build_exists = module_build_dir.exists() if module_build_exists: if not OPTION["REUSE_BUILD"]: - log.info("Deleting module build folder {}...".format(module_build_dir)) + log.info(f"Deleting module build folder {module_build_dir}...") try: - rmtree(module_build_dir) + remove_tree(module_build_dir) except Exception as e: - print('***** problem removing "{}"'.format(module_build_dir)) - print('ignored error: {}'.format(e)) + log.error(f'***** problem removing "{module_build_dir}"') + log.error(f'ignored error: {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)) + log.info(f"Reusing module build folder {module_build_dir}...") + if not module_build_dir.exists(): + log.info(f"Creating module build folder {module_build_dir}...") os.makedirs(module_build_dir) os.chdir(module_build_dir) - module_src_dir = os.path.join(self.sources_dir, extension) + module_src_dir = self.sources_dir / extension # Build module - cmake_cmd = [OPTION["CMAKE"]] - if OPTION["QUIET"]: + cmake_cmd = [str(OPTION["CMAKE"])] + cmake_quiet_build = 1 + cmake_rule_messages = 0 + if OPTION["LOG_LEVEL"] == LogLevel.VERBOSE: # Pass a special custom option, to allow printing a lot less information when doing # a quiet build. - cmake_cmd.append('-DQUIET_BUILD=1') + cmake_quiet_build = 0 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_rule_messages = 1 + + if OPTION["UNITY"]: + cmake_cmd.append("-DCMAKE_UNITY_BUILD=ON") + batch_size = OPTION["UNITY_BUILD_BATCH_SIZE"] + cmake_cmd.append(f"-DCMAKE_UNITY_BUILD_BATCH_SIZE={batch_size}") + log.info("Using UNITY build") 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 + f"-DBUILD_TESTS={self.build_tests}", + f"-DQt5Help_DIR={self.qtinfo.docs_dir}", + f"-DCMAKE_BUILD_TYPE={self.build_type}", + f"-DCMAKE_INSTALL_PREFIX={self.install_dir}", + # Record the minimum/maximum Python version for later use in Shiboken.__init__ + f"-DMINIMUM_PYTHON_VERSION={get_allowed_python_versions()[0]}", + f"-DMAXIMUM_PYTHON_VERSION={get_allowed_python_versions()[-1]}", + f"-DQUIET_BUILD={cmake_quiet_build}", + f"-DCMAKE_RULE_MESSAGES={cmake_rule_messages}", + str(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)) + + # When cross-compiling we set Python_ROOT_DIR to tell + # FindPython.cmake where to pick up the device python libs. + if self.is_cross_compile: + if self.python_target_path: + cmake_cmd.append(f"-DPython_ROOT_DIR={self.python_target_path}") + + # Host python is needed when cross compiling to run + # embedding_generator.py. Pass it as a separate option. + cmake_cmd.append(f"-DQFP_PYTHON_HOST_PATH={sys.executable}") + else: + cmake_cmd.append(f"-DPython_EXECUTABLE={self.py_executable}") + cmake_cmd.append(f"-DPython_INCLUDE_DIR={self.py_include_dir}") + cmake_cmd.append(f"-DPython_LIBRARY={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"])) + config_dir = OPTION["SHIBOKEN_CONFIG_DIR"] + if config_dir.exists(): + log.info(f"Using custom provided {SHIBOKEN} installation: {config_dir}") + cmake_cmd.append(f"-DShiboken6_DIR={config_dir}") else: - log.info("Custom provided shiboken2 installation not found. Path given: {}" - .format(OPTION["SHIBOKEN_CONFIG_DIR"])) + + log.info(f"Custom provided {SHIBOKEN} installation not found. " + f"Path given: {config_dir}") if OPTION["MODULE_SUBSET"]: module_sub_set = '' @@ -950,7 +634,8 @@ class PysideBuild(_build): if module_sub_set: module_sub_set += ';' module_sub_set += m - cmake_cmd.append("-DMODULES={}".format(module_sub_set)) + cmake_cmd.append(f"-DMODULES={module_sub_set}") + if OPTION["SKIP_MODULES"]: skip_modules = '' for m in OPTION["SKIP_MODULES"].split(','): @@ -959,28 +644,66 @@ class PysideBuild(_build): if skip_modules: skip_modules += ';' skip_modules += m - cmake_cmd.append("-DSKIP_MODULES={}".format(skip_modules)) + cmake_cmd.append(f"-DSKIP_MODULES={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 cmake_src_dir: + cmake_cmd.append(f"-DQT_SRC_DIR={cmake_src_dir}") + if OPTION['NO_QT_TOOLS']: + cmake_cmd.append("-DNO_QT_TOOLS=yes") + if OPTION['SKIP_DOCS']: + log.info("Warning: '--skip-docs' is deprecated and will be removed. " + "The documentation is not built by default") + if OPTION['BUILD_DOCS']: + cmake_cmd.append("-DBUILD_DOCS=yes") + log.info(f"Qt Source dir: {cmake_src_dir}") + + # Use Legacy OpenGL to avoid issues on systems like Ubuntu 20.04 + # which require to manually install the libraries which + # were previously linked to the QtGui module in 6.1 + # https://bugreports.qt.io/browse/QTBUG-89754 + cmake_cmd.append("-DOpenGL_GL_PREFERENCE=LEGACY") + + if OPTION['AVOID_PROTECTED_HACK']: + cmake_cmd.append("-DAVOID_PROTECTED_HACK=1") + + if self._enable_numpy(): + numpy = get_numpy_location() + if numpy: + cmake_cmd.append(f"-DNUMPY_INCLUDE_DIR={numpy}") + else: + log.warning('numpy include directory was not found.') - if self.build_type.lower() == 'debug': - cmake_cmd.append("-DPYTHON_DEBUG_LIBRARY={}".format( - self.py_library)) + if self.build_type.lower() != 'debug': + if OPTION['NO_STRIP']: + cmake_cmd.append("-DQFP_NO_STRIP=1") + if OPTION['NO_OVERRIDE_OPTIMIZATION_FLAGS']: + cmake_cmd.append("-DQFP_NO_OVERRIDE_OPTIMIZATION_FLAGS=1") 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 + if sys.platform == 'win32' and self.debug: + cmake_cmd.append("-DFORCE_LIMITED_API=no") else: - raise DistutilsSetupError("option limited-api must be 'yes' or 'no' " - "(default yes if applicable, i.e. python version >= 3.5)") + raise SetupError("option limited-api must be 'yes' or 'no' " + "(default yes if applicable, i.e. Python " + "version >= 3.9 and release build if on Windows)") - if OPTION["VERBOSE_BUILD"]: + if OPTION["DISABLE_PYI"]: + cmake_cmd.append("-DDISABLE_PYI=yes") + + if OPTION["LOG_LEVEL"] == LogLevel.VERBOSE: cmake_cmd.append("-DCMAKE_VERBOSE_MAKEFILE:BOOL=ON") + else: + cmake_cmd.append("-DCMAKE_VERBOSE_MAKEFILE:BOOL=OFF") + + if OPTION['COMPILER_LAUNCHER']: + compiler_launcher = OPTION['COMPILER_LAUNCHER'] + cmake_cmd.append(f"-DCMAKE_C_COMPILER_LAUNCHER={compiler_launcher}") + cmake_cmd.append(f"-DCMAKE_CXX_COMPILER_LAUNCHER={compiler_launcher}") if OPTION["SANITIZE_ADDRESS"]: # Some simple sanity checking. Only use at your own risk. @@ -988,9 +711,9 @@ class PysideBuild(_build): or sys.platform.startswith('darwin')): cmake_cmd.append("-DSANITIZE_ADDRESS=ON") else: - raise DistutilsSetupError("Address sanitizer can only be used on Linux and macOS.") + raise SetupError("Address sanitizer can only be used on Linux and macOS.") - if extension.lower() == "pyside2": + if extension.lower() == PYSIDE: pyside_qt_conf_prefix = '' if OPTION["QT_CONF_PREFIX"]: pyside_qt_conf_prefix = OPTION["QT_CONF_PREFIX"] @@ -999,13 +722,15 @@ class PysideBuild(_build): 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)) + cmake_cmd.append(f"-DPYSIDE_QT_CONF_PREFIX={pyside_qt_conf_prefix}") + + if OPTION["STANDALONE"]: + cmake_cmd.append("-DSTANDALONE:BOOL=ON") # 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)) + cmake_cmd.append(f"-DPACKAGE_SETUP_PY_PACKAGE_VERSION={package_version}") # In case if this is a snapshot build, also pass the # timestamp as a separate value, because it is the only @@ -1013,17 +738,17 @@ class PysideBuild(_build): timestamp = '' if OPTION["SNAPSHOT_BUILD"]: timestamp = get_package_timestamp() - cmake_cmd.append("-DPACKAGE_SETUP_PY_PACKAGE_TIMESTAMP={}".format(timestamp)) + cmake_cmd.append(f"-DPACKAGE_SETUP_PY_PACKAGE_TIMESTAMP={timestamp}") + + if extension.lower() in [SHIBOKEN]: + cmake_cmd.append("-DUSE_PYTHON_VERSION=3.9") - 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") + cmake_cmd += platform_cmake_options() 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"])) + cmake_cmd.append(f"-DCMAKE_OSX_ARCHITECTURES:STRING={OPTION['MACOS_ARCH']}") if OPTION["MACOS_USE_LIBCPP"]: # Explicitly link the libc++ standard library (useful @@ -1037,60 +762,111 @@ class PysideBuild(_build): cmake_cmd.append("-DOSX_USE_LIBCPP=ON") if OPTION["MACOS_SYSROOT"]: - cmake_cmd.append("-DCMAKE_OSX_SYSROOT={}".format( - OPTION["MACOS_SYSROOT"])) + cmake_cmd.append(f"-DCMAKE_OSX_SYSROOT={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)) + cmake_cmd.append(f"-DCMAKE_OSX_SYSROOT={latest_sdk_path}") # Set macOS minimum deployment target (version). # This is required so that calling - # run_process -> distutils.spawn() + # run_process -> subprocess.call() # 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 = PysideBuild.macos_pyside_min_deployment_target() - cmake_cmd.append("-DCMAKE_OSX_DEPLOYMENT_TARGET={}".format(deployment_target)) + deployment_target = macos_pyside_min_deployment_target() + cmake_cmd.append(f"-DCMAKE_OSX_DEPLOYMENT_TARGET={deployment_target}") os.environ['MACOSX_DEPLOYMENT_TARGET'] = deployment_target + if OPTION["BUILD_DOCS"]: + # Build the whole documentation (Base + API) by default + cmake_cmd.append("-DFULLDOCSBUILD=1") + + 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") + else: + cmake_cmd.append("-DBUILD_DOCS=no") + if OPTION["DOC_BUILD_ONLINE"]: + log.info("Warning: Documentation build is disabled, " + "however --doc-build-online was passed. " + "Use '--build-docs' to enable the documentation build") + + if OPTION["PYSIDE_NUMPY_SUPPORT"]: + log.info("Warning: '--pyside-numpy-support' is deprecated and will be removed. " + "Use --enable-numpy-support/--disable-numpy-support.") + + target_qt_prefix_path = self.qtinfo.prefix_dir + cmake_cmd.append(f"-DQFP_QT_TARGET_PATH={target_qt_prefix_path}") + if self.qt_host_path: + cmake_cmd.append(f"-DQFP_QT_HOST_PATH={self.qt_host_path}") + + if self.is_cross_compile and (not OPTION["SHIBOKEN_HOST_PATH"] + or not Path(OPTION["SHIBOKEN_HOST_PATH"]).exists()): + raise SetupError("Please specify the location of host shiboken tools via " + "--shiboken-host-path=") + + if self.shiboken_host_path: + cmake_cmd.append(f"-DQFP_SHIBOKEN_HOST_PATH={self.shiboken_host_path}") + + if self.shiboken_target_path: + cmake_cmd.append(f"-DQFP_SHIBOKEN_TARGET_PATH={self.shiboken_target_path}") + elif self.cmake_toolchain_file and not extension.lower() == SHIBOKEN: + # Need to tell where to find target shiboken when + # cross-compiling pyside. + cmake_cmd.append(f"-DQFP_SHIBOKEN_TARGET_PATH={self.install_dir}") + + if self.cmake_toolchain_file: + cmake_cmd.append(f"-DCMAKE_TOOLCHAIN_FILE={self.cmake_toolchain_file}") + if not OPTION["SKIP_CMAKE"]: - log.info("Configuring module {} ({})...".format(extension, module_src_dir)) + log.info(f"Configuring module {extension} ({module_src_dir})...") if run_process(cmake_cmd) != 0: - raise DistutilsSetupError("Error configuring {}".format(extension)) + raise SetupError(f"Error configuring {extension}") else: - log.info("Reusing old configuration for module {} ({})...".format( - extension, module_src_dir)) + log.info(f"Reusing old configuration for module {extension} ({module_src_dir})...") - log.info("Compiling module {}...".format(extension)) - cmd_make = [self.make_path] + log.info(f"-- Compiling module {extension}...") + cmd_make = [str(self.make_path)] if OPTION["JOBS"]: cmd_make.append(OPTION["JOBS"]) + if OPTION["LOG_LEVEL"] == LogLevel.VERBOSE and self.make_generator == "Ninja": + cmd_make.append("-v") 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 - + raise SetupError(f"Error compiling {extension}") + + if sys.version_info == (3, 6) and sys.platform == "darwin": + # Python 3.6 has a Sphinx problem because of docutils 0.17 . + # Instead of pinning v0.16, setting the default encoding fixes that. + # Since other platforms are not affected, we restrict this to macOS. + if "UTF-8" not in os.environ.get("LC_ALL", ""): + os.environ["LC_ALL"] = "en_US.UTF-8" + + if OPTION["BUILD_DOCS"]: + if extension.lower() == SHIBOKEN: + found = importlib.util.find_spec("sphinx") + if found: log.info("Generating Shiboken documentation") - if run_process([self.make_path, "doc"]) != 0: - raise DistutilsSetupError("Error generating documentation " - "for {}".format(extension)) - except ImportError: + make_doc_cmd = [str(self.make_path), "doc"] + if OPTION["LOG_LEVEL"] == LogLevel.VERBOSE and self.make_generator == "Ninja": + make_doc_cmd.append("-v") + if run_process(make_doc_cmd) != 0: + raise SetupError(f"Error generating documentation for {extension}") + else: log.info("Sphinx not found, skipping documentation build") else: - log.info("Skipped documentation generation") + log.info("-- Skipped documentation generation. Enable with '--build-docs'") + cmake_cmd.append("-DBUILD_DOCS=no") if not OPTION["SKIP_MAKE_INSTALL"]: - log.info("Installing module {}...".format(extension)) + log.info(f"Installing module {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 @@ -1100,25 +876,24 @@ class PysideBuild(_build): 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)) + if run_process([str(self.make_path), target]) != 0: + raise SetupError(f"Error pseudo installing {extension}") else: - log.info("Skipped installing module {}".format(extension)) + log.info(f"Skipped installing module {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). + to the setup tools build dir (which is read from self.build_lib provided by setuptools). 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 = { + log.info("Preparing setup tools build directory.") + _vars = { "site_packages_dir": self.site_packages_dir, "sources_dir": self.sources_dir, "install_dir": self.install_dir, @@ -1131,42 +906,61 @@ class PysideBuild(_build): "py_version": self.py_version, "qt_version": self.qtinfo.version, "qt_bin_dir": self.qtinfo.bins_dir, + "qt_data_dir": self.qtinfo.data_dir, "qt_doc_dir": self.qtinfo.docs_dir, "qt_lib_dir": self.qtinfo.libs_dir, + "qt_module_json_files_dir": self.qtinfo.module_json_files_dir, + "qt_metatypes_dir": self.qtinfo.metatypes_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, + + # TODO: This is currently None when cross-compiling + # There doesn't seem to be any place where we can query + # it. Fortunately it's currently only used when + # packaging Windows vcredist. "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 + _vars['cmake_package_name'] = config.shiboken_module_option_name os.chdir(self.script_dir) + # Clean up the previous st_build_dir before files are copied + # into it again. That's the because the same dir is used + # when copying the files for each of the sub-projects and + # we don't want to accidentally install shiboken files + # as part of pyside-tools package. + if self.st_build_dir.is_dir(): + log.info(f"Removing {self.st_build_dir}") + try: + remove_tree(self.st_build_dir) + except Exception as e: + log.warning(f'problem removing "{self.st_build_dir}"') + log.warning(f'ignored error: {e}') + if sys.platform == "win32": - vars['dbg_postfix'] = OPTION["DEBUG"] and "_d" or "" - return prepare_packages_win32(self, vars) + _vars['dbg_postfix'] = OPTION["DEBUG"] and "_d" or "" + return prepare_packages_win32(self, _vars) else: - return prepare_packages_posix(self, vars) + return prepare_packages_posix(self, _vars, self.is_cross_compile) 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 + return Path(f"{self.qtinfo.headers_dir}/../lib/QtCore.framework").is_dir() - def get_built_pyside_config(self, vars): + 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") + st_build_dir = Path(_vars['st_build_dir']) + config_path = 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'] = [] @@ -1179,16 +973,16 @@ class PysideBuild(_build): def prepare_standalone_clang(self, is_win=False): """ - Copies the libclang library to the shiboken2-generator + Copies the libclang library to the shiboken6-generator package so that the shiboken executable works. """ log.info('Finding path to the libclang shared library.') cmake_cmd = [ - OPTION["CMAKE"], + str(OPTION["CMAKE"]), "-L", # Lists variables "-N", # Just inspects the cache (faster) - "--build", # Specifies the build dir - self.shiboken_build_dir + "-B", # Specifies the build dir + str(self.shiboken_build_dir) ] out = run_process_output(cmake_cmd) lines = [s.strip() for s in out] @@ -1209,11 +1003,12 @@ class PysideBuild(_build): # 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) + clang_lib_path = Path(re.sub(r'lib/libclang.lib$', + 'bin/libclang.dll', + clang_lib_path)) else: - # shiboken2 links against libclang.so.6 or a similarly + clang_lib_path = Path(clang_lib_path) + # shiboken6 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 @@ -1224,26 +1019,26 @@ class PysideBuild(_build): # 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): + if clang_lib_path.is_symlink(): + link_target = Path(os.readlink(clang_lib_path)) + if link_target.is_absolute(): 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) + clang_lib_path = clang_lib_path.parent / link_target + clang_lib_path = clang_lib_path.resolve() # 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) + _vars = {} + _vars['st_build_dir'] = self.st_build_dir + _vars['st_package_name'] = config.package_name() + destination_dir = Path("{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) + if clang_lib_path.exists(): + basename = clang_lib_path.name + log.info(f"Copying libclang shared library {clang_lib_path} to the package " + f"folder as {basename}.") + destination_path = destination_dir / basename # Need to modify permissions in case file is not writable # (a reinstall would cause a permission denied error). @@ -1253,12 +1048,51 @@ class PysideBuild(_build): 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): + f"from {clang_lib_path} to {destination_dir}. ") + + def get_shared_library_filters(self): + unix_filters = ["*.so", "*.so.*"] + darwin_filters = ["*.so", "*.dylib"] + filters = [] + if self.is_cross_compile: + if 'darwin' in self.plat_name or 'macos' in self.plat_name: + filters = darwin_filters + elif 'linux' in self.plat_name or 'android' in self.plat_name: + filters = unix_filters + else: + log.warning(f"No shared library filters found for platform {self.plat_name}. " + f"The package might miss Qt libraries and plugins.") + else: + if sys.platform == 'darwin': + filters = darwin_filters + else: + filters = unix_filters + return filters + + def _find_shared_libraries(self, path, recursive=False): + """Helper to find shared libraries in a path.""" + result = set() + for filter in self.get_shared_library_filters(): + glob_pattern = f"**/{filter}" if recursive else filter + for library in path.glob(glob_pattern): + result.add(library) + return list(result) + + def package_libraries(self, package_path): + """Returns the libraries of the Python module""" + return self._find_shared_libraries(package_path) + + def get_shared_libraries_in_path_recursively(self, initial_path): + """Returns shared library plugins in given path (collected + recursively)""" + return self._find_shared_libraries(initial_path, recursive=True) + + def update_rpath(self, executables, libexec=False, message=None): + ROOT = '@loader_path' if sys.platform == 'darwin' else '$ORIGIN' + QT_PATH = '/../lib' if libexec else '/Qt/lib' + + message = "Patched rpath to '$ORIGIN/' in" 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 = '' @@ -1271,14 +1105,13 @@ class PysideBuild(_build): # installed qt lib directory. final_rpath = self.qtinfo.libs_dir if OPTION["STANDALONE"]: - final_rpath = "$ORIGIN/Qt/lib" + final_rpath = f'{ROOT}{QT_PATH}' 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"])] + message = "Updated rpath in" def rpath_cmd(srcpath): final_rpath = '' @@ -1288,26 +1121,186 @@ class PysideBuild(_build): final_rpath = OPTION["RPATH_VALUES"] else: if OPTION["STANDALONE"]: - final_rpath = "@loader_path/Qt/lib" + final_rpath = f'{ROOT}{QT_PATH}' 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)) + raise RuntimeError(f"Not configured for platform {sys.platform}") + + # Update rpath + for executable in executables: + if executable.is_dir() or executable.is_symlink(): + continue + if not executable.exists(): + continue + rpath_cmd(executable) + log.debug(f"{message} {executable}.") + + def update_rpath_for_linux_plugins( + self, + plugin_paths, + qt_lib_dir=None, + is_qml_plugin=False): + + # If the linux sysroot (where the plugins are copied from) + # is from a mainline distribution, it might have a different + # directory layout than then one we expect to have in the + # wheel. + # We have to ensure that any plugins copied have rpath + # values that can find Qt libs in the newly assembled wheel + # dir layout. + if not (self.is_cross_compile and sys.platform.startswith('linux') and self.standalone): + return + + log.info("Patching rpath for Qt and QML plugins.") + for plugin in plugin_paths: + if plugin.is_dir() or plugin.is_symlink(): + continue + if not plugin.exists(): + continue - pyside_libs.extend(executables) + if is_qml_plugin: + plugin_dir = plugin.parent + # FIXME: there is no os.path.relpath equivalent on pathlib. + # The Path.relative_to is not equivalent and raises ValueError when the paths + # are not subpaths, so it doesn't generate "../../something". + rel_path_from_qml_plugin_qt_lib_dir = os.path.relpath(qt_lib_dir, plugin_dir) + rpath_value = Path("$ORIGIN") / rel_path_from_qml_plugin_qt_lib_dir + else: + rpath_value = "$ORIGIN/../../lib" + + linux_fix_rpaths_for_library(self._patchelf_path, plugin, rpath_value, + override=True) + log.debug(f"Patched rpath to '{rpath_value}' in {plugin}.") - # 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): + def update_rpath_for_linux_qt_libraries(self, qt_lib_dir): + # Ensure that Qt libs and ICU libs have $ORIGIN in their rpath. + # Especially important for ICU lib, so that they don't + # accidentally load dependencies from the system. + if not (self.is_cross_compile and sys.platform.startswith('linux') and self.standalone): + return + + qt_lib_dir = Path(qt_lib_dir) + rpath_value = "$ORIGIN" + log.info(f"Patching rpath for Qt and ICU libraries in {qt_lib_dir}.") + for library in self.package_libraries(qt_lib_dir): + if library.is_dir() or library.is_symlink(): continue - if not os.path.exists(srcpath): + if not library.exists(): continue - rpath_cmd(srcpath) - log.info("Patched rpath to '$ORIGIN/' (Linux) or " - "updated rpath (OS/X) in {}.".format(srcpath)) + + linux_fix_rpaths_for_library(self._patchelf_path, library, rpath_value, override=True) + log.debug(f"Patched rpath to '{rpath_value}' in {library}.") + + +class PysideBaseDocs(Command, CommandMixin): + description = "Build the base documentation only" + user_options = CommandMixin.mixin_user_options + + def __init__(self, *args, **kwargs): + self.command_name = "build_base_docs" + Command.__init__(self, *args, **kwargs) + CommandMixin.__init__(self) + + def initialize_options(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 = config.setup_script_dir / "sources" / self.name / "doc" + # Check if sphinx is installed to proceed. + found = importlib.util.find_spec("sphinx") + self.html_dir = Path("html") + if found: + if self.name == SHIBOKEN: + # Delete the 'html' directory since new docs will be generated anyway + if self.html_dir.is_dir(): + rmtree(self.html_dir) + log.info("-- Deleted old html directory") + log.info("-- Generating Shiboken documentation") + log.info(f"-- Documentation directory: 'html/{PYSIDE}/{SHIBOKEN}/'") + elif self.name == PYSIDE: + log.info("-- Generating PySide documentation") + log.info(f"-- Documentation directory: 'html/{PYSIDE}/'") + else: + raise SetupError("Sphinx not found - aborting") + + # creating directories html/pyside6/shiboken6 + try: + if not self.html_dir.is_dir(): + self.html_dir.mkdir(parents=True) + if self.name == SHIBOKEN: + out_pyside = self.html_dir / PYSIDE + if not out_pyside.is_dir(): + out_pyside.mkdir(parents=True) + out_shiboken = out_pyside / SHIBOKEN + if not out_shiboken.is_dir(): + out_shiboken.mkdir(parents=True) + self.out_dir = out_shiboken + # We know that on the shiboken step, we already created the + # 'pyside6' directory + elif self.name == PYSIDE: + self.out_dir = self.html_dir / PYSIDE + except (PermissionError, FileExistsError): + raise SetupError(f"Error while creating directories for {self.doc_dir}") + + def run(self): + if not self.skip: + cmake_cmd = [ + str(OPTION["CMAKE"]), + "-S", str(self.doc_dir), + "-B", str(self.out_dir), + "-DDOC_OUTPUT_FORMAT=html", + "-DFULLDOCSBUILD=0", + ] + + cmake_quiet_build = 1 + cmake_message_log_level = "STATUS" + + # Define log level + if OPTION["LOG_LEVEL"] == LogLevel.VERBOSE: + cmake_quiet_build = 0 + cmake_message_log_level = "VERBOSE" + elif OPTION["LOG_LEVEL"] == LogLevel.QUIET: + cmake_message_log_level = "ERROR" + + cmake_cmd.append(f"-DQUIET_BUILD={cmake_quiet_build}") + cmake_cmd.append(f"-DCMAKE_MESSAGE_LOG_LEVEL={cmake_message_log_level}") + + if run_process(cmake_cmd) != 0: + raise SetupError(f"Error running CMake for {self.doc_dir}") + + if self.name == PYSIDE: + self.sphinx_src = self.out_dir / "base" + example_gallery = config.setup_script_dir / "tools" / "example_gallery" / "main.py" + assert example_gallery.is_file() + example_gallery_cmd = [sys.executable, os.fspath(example_gallery)] + if OPTION["LOG_LEVEL"] == LogLevel.QUIET: + example_gallery_cmd.append("--quiet") + qt_src_dir = OPTION['QT_SRC'] + if qt_src_dir: + example_gallery_cmd.extend(["--qt-src-dir", qt_src_dir]) + if run_process(example_gallery_cmd) != 0: + raise SetupError(f"Error running example gallery for {self.doc_dir}") + elif self.name == SHIBOKEN: + self.sphinx_src = self.out_dir + + sphinx_cmd = ["sphinx-build", "-b", "html", "-j", "auto", "-c", + str(self.sphinx_src), str(self.doc_dir), + str(self.out_dir)] + if run_process(sphinx_cmd) != 0: + raise SetupError(f"Error running CMake for {self.doc_dir}") + # Last message + if not self.skip and self.name == PYSIDE: + log.info(f"-- The documentation was built. Check html/{PYSIDE}/index.html") + + def finalize_options(self): + CommandMixin.mixin_finalize_options(self) cmd_class_dict = { @@ -1317,14 +1310,10 @@ cmd_class_dict = { 'bdist_egg': PysideBdistEgg, 'develop': PysideDevelop, 'install': PysideInstall, - 'install_lib': PysideInstallLib + 'install_lib': PysideInstallLib, + 'build_base_docs': PysideBaseDocs, } if wheel_module_exists: - params = {} - params['qt_version'] = get_qt_version() - params['package_version'] = get_package_version() - if sys.platform == 'darwin': - params['macos_plat_name'] = PysideBuild.macos_plat_name() - pyside_bdist_wheel = get_bdist_wheel_override(params) + pyside_bdist_wheel = get_bdist_wheel_override() if pyside_bdist_wheel: cmd_class_dict['bdist_wheel'] = pyside_bdist_wheel diff --git a/build_scripts/options.py b/build_scripts/options.py index 4229a2c10..806d4a8a3 100644 --- a/build_scripts/options.py +++ b/build_scripts/options.py @@ -1,57 +1,42 @@ -############################################################################# -## -## 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 +# Copyright (C) 2018 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +from setuptools import Command + import sys -import os -import warnings +import logging +from pathlib import Path + +from .log import log, LogLevel +from .qtinfo import QtInfo +from .utils import memoize, which + +_AVAILABLE_MKSPECS = ["ninja", "msvc", "mingw"] if sys.platform == "win32" else ["ninja", "make"] + + +# Global options not which are not part of the commands +ADDITIONAL_OPTIONS = """ +Additional options: + --limited-api Use Limited API [yes/no] + --macos-use-libc++ Use libc++ on macOS + --snapshot-build Snapshot build + --package-timestamp Package Timestamp + --cmake-toolchain-file Path to CMake toolchain to enable cross-compiling + --shiboken-host-path Path to host shiboken package when cross-compiling + --qt-host-path Path to host Qt installation when cross-compiling + --disable-pyi Disable .pyi file generation +""" def _warn_multiple_option(option): - warnings.warn('Option "{}" occurs multiple times on the command line.'.format(option)) + log.warning(f'Option "{option}" occurs multiple times on the command line.') def _warn_deprecated_option(option, replacement=None): - w = 'Option "{}" is deprecated and may be removed in a future release.'.format(option) + w = f'Option "{option}" is deprecated and may be removed in a future release.' if replacement: - w = '{}\nUse "{}" instead.'.format(w, replacement) - warnings.warn(w) + w = f'{w}\nUse "{replacement}" instead.' + log.warning(w) class Options(object): @@ -63,12 +48,12 @@ class Options(object): def has_option(self, name, remove=True): """ Returns True if argument '--name' was passed on the command line. """ - option = '--{}'.format(name) + option = f"--{name}" count = sys.argv.count(option) remove_count = count if not remove and count > 0: remove_count -= 1 - for i in range(remove_count): + for _ in range(remove_count): sys.argv.remove(option) if count > 1: _warn_multiple_option(option) @@ -76,11 +61,9 @@ class Options(object): def option_value(self, name, short_option_name=None, remove=True): """ - Returns the value of a command line option or environment - variable. + Returns the value of a command line option. - :param name: The name of the command line option or environment - variable. + :param name: The name of the command line option. :param remove: Whether the option and its value should be removed from sys.argv. Useful when there's a need to query for @@ -88,9 +71,10 @@ class Options(object): :return: Either the option value or None. """ - option = '--' + name - short_option = '-' + short_option_name if short_option_name else None - single_option_prefix = option + '=' + + option = f"--{name}" + short_option = f"-{short_option_name}" if short_option_name else None + single_option_prefix = f"{option}=" value = None for index in reversed(range(len(sys.argv))): arg = sys.argv[index] @@ -99,7 +83,7 @@ class Options(object): _warn_multiple_option(option) else: if index + 1 >= len(sys.argv): - raise RuntimeError("The option {} requires a value".format(option)) + raise RuntimeError(f"The option {option} requires a value") value = sys.argv[index + 1] if remove: @@ -114,9 +98,6 @@ class Options(object): if remove: sys.argv[index:index + 1] = [] - if value is None: - value = os.getenv(name.upper().replace('-', '_')) - self.dict[name] = value return value @@ -132,58 +113,486 @@ def option_value(*args, **kwargs): return options.option_value(*args, **kwargs) -# Declare options +def _jobs_option_value(): + """Option value for parallel builds.""" + value = option_value('parallel', short_option_name='j') + if value: + return f"-j{value}" if not value.startswith('-j') else value + return '' + + +def find_qtpaths(): + # for these command --qtpaths should not be required + no_qtpaths_commands = ["--help", "--help-commands", "--qt-target-path", "build_base_docs"] + + for no_qtpaths_command in no_qtpaths_commands: + if any(no_qtpaths_command in argument for argument in sys.argv): + return None + + qtpaths = option_value("qtpaths") + if qtpaths: + return qtpaths + + # if qtpaths is not given as cli option, try to find it in PATH + qtpaths = which("qtpaths6") + if qtpaths: + return str(qtpaths.resolve()) + + qtpaths = which("qtpaths") + if qtpaths: + return str(qtpaths.resolve()) + + return qtpaths + + +# Declare options which need to be known when instantiating the setuptools +# commands or even earlier during SetupRunner.run(). OPTION = { "BUILD_TYPE": option_value("build-type"), "INTERNAL_BUILD_TYPE": option_value("internal-build-type"), - "DEBUG": has_option("debug"), - "RELWITHDEBINFO": has_option('relwithdebinfo'), - "QMAKE": option_value("qmake"), - "QT_VERSION": option_value("qt"), - "CMAKE": option_value("cmake"), - "OPENSSL": option_value("openssl"), - "SHIBOKEN_CONFIG_DIR": option_value("shiboken-config-dir"), - "ONLYPACKAGE": has_option("only-package"), - "STANDALONE": has_option("standalone"), - "MAKESPEC": option_value("make-spec"), - "IGNOREGIT": has_option("ignore-git"), - # don't generate documentation - "SKIP_DOCS": has_option("skip-docs"), - # don't include pyside2-examples - "NOEXAMPLES": has_option("no-examples"), # number of parallel build jobs - "JOBS": option_value('parallel', short_option_name='j'), + "JOBS": _jobs_option_value(), # Legacy, not used any more. "JOM": has_option('jom'), - # Do not use jom instead of nmake with msvc - "NO_JOM": has_option('no-jom'), - "BUILDTESTS": has_option("build-tests"), - "MACOS_ARCH": option_value("macos-arch"), "MACOS_USE_LIBCPP": has_option("macos-use-libc++"), - "MACOS_SYSROOT": option_value("macos-sysroot"), - "MACOS_DEPLOYMENT_TARGET": option_value("macos-deployment-target"), - "XVFB": has_option("use-xvfb"), - "REUSE_BUILD": has_option("reuse-build"), - "SKIP_CMAKE": has_option("skip-cmake"), - "SKIP_MAKE_INSTALL": has_option("skip-make-install"), - "SKIP_PACKAGING": has_option("skip-packaging"), - "SKIP_MODULES": option_value("skip-modules"), - "MODULE_SUBSET": option_value("module-subset"), - "RPATH_VALUES": option_value("rpath"), - "QT_CONF_PREFIX": option_value("qt-conf-prefix"), - "QT_SRC": option_value("qt-src-dir"), - "QUIET": has_option('quiet', remove=False), - "VERBOSE_BUILD": has_option("verbose-build"), - "SANITIZE_ADDRESS": has_option("sanitize-address"), + "LOG_LEVEL": option_value("log-level", remove=False), + "QUIET": has_option('quiet'), + "VERBOSE_BUILD": has_option('verbose-build'), "SNAPSHOT_BUILD": has_option("snapshot-build"), "LIMITED_API": option_value("limited-api"), + "DISABLE_PYI": has_option("disable-pyi"), "PACKAGE_TIMESTAMP": option_value("package-timestamp"), - "SHORTER_PATHS": has_option("shorter-paths"), - # This is used automatically by distutils.command.install object, to + # This is used automatically by setuptools.command.install object, to # specify the final installation location. "FINAL_INSTALL_PREFIX": option_value("prefix", remove=False), + "CMAKE_TOOLCHAIN_FILE": option_value("cmake-toolchain-file"), + "SHIBOKEN_HOST_PATH": option_value("shiboken-host-path"), + "SHIBOKEN_HOST_PATH_QUERY_FILE": option_value("internal-shiboken-host-path-query-file"), + "QT_HOST_PATH": option_value("qt-host-path"), + # This is used to identify the template for doc builds + "QTPATHS": find_qtpaths() + # This is an optional command line option. If --qtpaths is not provided via command-line, + # then qtpaths is checked inside PATH variable } + _deprecated_option_jobs = option_value('jobs') if _deprecated_option_jobs: _warn_deprecated_option('jobs', 'parallel') OPTION["JOBS"] = _deprecated_option_jobs + + +class CommandMixin(object): + """Mixin for the setuptools build/install commands handling the options.""" + + _static_class_finalized_once = False + + mixin_user_options = [ + ('avoid-protected-hack', None, 'Force --avoid-protected-hack'), + ('debug', None, 'Build with debug information'), + ('relwithdebinfo', None, 'Build in release mode with debug information'), + ('only-package', None, 'Package only'), + ('no-strip', None, 'Do not strip package libraries (release mode)'), + ('standalone', None, 'Standalone build'), + ('ignore-git', None, 'Do update subrepositories'), + ('skip-docs', None, 'Skip documentation build (deprecated)'), + ('build-docs', None, 'Build the API documentation'), + ('no-jom', None, 'Do not use jom (MSVC)'), + ('build-tests', None, 'Build tests'), + ('use-xvfb', None, 'Use Xvfb for testing'), + ('reuse-build', None, 'Reuse existing build'), + ('compiler-launcher=', None, 'Use a compiler launcher like ccache or sccache for builds'), + ('skip-cmake', None, 'Skip CMake step'), + ('skip-make-install', None, 'Skip install step'), + ('skip-packaging', None, 'Skip packaging step'), + ('log-level=', None, 'Log level of the build.'), + ('verbose-build', None, 'Verbose build'), + ('quiet', None, 'Quiet build'), + ('sanitize-address', None, 'Build with address sanitizer'), + ('shorter-paths', None, 'Use shorter paths'), + ('doc-build-online', None, 'Build online documentation'), + ('qtpaths=', None, 'Path to qtpaths'), + ('qmake=', None, 'Path to qmake (deprecated, use qtpaths)'), + ('qt=', None, 'Qt version'), + ('qt-target-path=', None, + 'Path to device Qt installation (use Qt libs when cross-compiling)'), + ('cmake=', None, 'Path to CMake'), + ('openssl=', None, 'Path to OpenSSL libraries'), + + # FIXME: Deprecated in favor of shiboken-target-path + ('shiboken-config-dir=', None, 'shiboken configuration directory'), + + ('shiboken-target-path=', None, 'Path to target shiboken package'), + ('python-target-path=', None, 'Path to target Python installation / prefix'), + ('make-spec=', None, 'Qt make-spec'), + ('macos-arch=', None, 'macOS architecture'), + ('macos-sysroot=', None, 'macOS sysroot'), + ('macos-deployment-target=', None, 'macOS deployment target'), + ('skip-modules=', None, 'Qt modules to be skipped'), + ('module-subset=', None, 'Qt modules to be built'), + ('rpath=', None, 'RPATH'), + ('qt-conf-prefix=', None, 'Qt configuration prefix'), + ('qt-src-dir=', None, 'Qt source directory'), + ('no-qt-tools', None, 'Do not copy the Qt tools'), + ('no-size-optimization', None, 'Turn off size optimization for PySide6 binaries'), + # Default is auto-detected by PysideBuild._enable_numpy() + ('pyside-numpy-support', None, 'libpyside: Add numpy support (deprecated)'), + ('enable-numpy-support', None, 'Enable numpy support'), + ('disable-numpy-support', None, 'Disable numpy support'), + ('internal-cmake-install-dir-query-file-path=', None, + 'Path to file where the CMake install path of the project will be saved'), + + # We redeclare plat-name as an option so it's recognized by the + # install command and doesn't throw an error. + ('plat-name=', None, 'The platform name for which we are cross-compiling'), + ('unity', None, 'Use CMake UNITY_BUILD_MODE (obsolete)'), + ('no-unity', None, 'Disable CMake UNITY_BUILD_MODE'), + ('unity-build-batch-size=', None, 'Value of CMAKE_UNITY_BUILD_BATCH_SIZE') + ] + + def __init__(self): + self.avoid_protected_hack = False + self.debug = False + self.relwithdebinfo = False + self.no_strip = False + self.only_package = False + self.standalone = False + self.ignore_git = False + self.skip_docs = False + self.build_docs = False + self.no_jom = False + self.build_tests = False + self.use_xvfb = False + self.reuse_build = False + self.compiler_launcher = None + self.skip_cmake = False + self.skip_make_install = False + self.skip_packaging = False + self.log_level = "info" + self.verbose_build = False + self.sanitize_address = False + self.snapshot_build = False + self.shorter_paths = False + self.doc_build_online = False + self.qtpaths = None + self.qmake = None + self.has_qmake_option = False + self.qt = '5' + self.qt_host_path = None + self.qt_target_path = None + self.cmake = None + self.openssl = None + self.shiboken_config_dir = None + self.shiboken_host_path = None + self.shiboken_host_path_query_file = None + self.shiboken_target_path = None + self.python_target_path = None + self.is_cross_compile = False + self.cmake_toolchain_file = None + self.make_spec = None + self.macos_arch = None + self.macos_sysroot = None + self.macos_deployment_target = None + self.skip_modules = None + self.module_subset = None + self.rpath = None + self.qt_conf_prefix = None + self.qt_src_dir = None + self.no_qt_tools = False + self.no_size_optimization = False + self.pyside_numpy_support = False + self.enable_numpy_support = False + self.disable_numpy_support = False + self.plat_name = None + self.internal_cmake_install_dir_query_file_path = None + self._per_command_mixin_options_finalized = False + self.unity = False + self.no_unity = False + self.unity_build_batch_size = "16" + + # When initializing a command other than the main one (so the + # first one), we need to copy the user options from the main + # command to the new command options dict. Then + # Distribution.get_command_obj will pick up the copied options + # ensuring that all commands that inherit from + # the mixin, get our custom properties set by the time + # finalize_options is called. + if CommandMixin._static_class_finalized_once: + current_command: Command = self + dist = current_command.distribution + main_command_name = dist.commands[0] + main_command_opts = dist.get_option_dict(main_command_name) + current_command_name = current_command.get_command_name() + current_command_opts = dist.get_option_dict(current_command_name) + mixin_options_set = self.get_mixin_options_set() + for key, value in main_command_opts.items(): + if key not in current_command_opts and key in mixin_options_set: + current_command_opts[key] = value + + # qtpaths is already known before running SetupRunner + self.qtpaths = OPTION["QTPATHS"] + + @staticmethod + @memoize + def get_mixin_options_set(): + keys = set() + for (name, _, _) in CommandMixin.mixin_user_options: + keys.add(name.rstrip("=").replace("-", "_")) + return keys + + def mixin_finalize_options(self): + # The very first we finalize options, record that. + if not CommandMixin._static_class_finalized_once: + CommandMixin._static_class_finalized_once = True + + # Ensure we finalize once per command object, rather than per + # setup.py invocation. We want to have the option values + # available in all commands that derive from the mixin. + if not self._per_command_mixin_options_finalized: + self._per_command_mixin_options_finalized = True + self._do_finalize() + + def _do_finalize(self): + # is_cross_compile must be set before checking for qtpaths/qmake + # because we DON'T want those to be found when cross compiling. + # Currently when cross compiling, qt-target-path MUST be used. + using_cmake_toolchain_file = False + cmake_toolchain_file = None + if OPTION["CMAKE_TOOLCHAIN_FILE"]: + self.is_cross_compile = True + using_cmake_toolchain_file = True + cmake_toolchain_file = OPTION["CMAKE_TOOLCHAIN_FILE"] + self.cmake_toolchain_file = cmake_toolchain_file + + if not self._determine_defaults_and_check(): + sys.exit(-1) + OPTION['AVOID_PROTECTED_HACK'] = self.avoid_protected_hack + OPTION['DEBUG'] = self.debug + OPTION['RELWITHDEBINFO'] = self.relwithdebinfo + OPTION['NO_STRIP'] = self.no_strip + OPTION['ONLYPACKAGE'] = self.only_package + OPTION['STANDALONE'] = self.standalone + if self.ignore_git: + _warn_deprecated_option('ignore_git') + OPTION['SKIP_DOCS'] = self.skip_docs + OPTION['BUILD_DOCS'] = self.build_docs + OPTION['BUILDTESTS'] = self.build_tests + + OPTION['NO_JOM'] = self.no_jom + OPTION['XVFB'] = self.use_xvfb + OPTION['REUSE_BUILD'] = self.reuse_build + OPTION['COMPILER_LAUNCHER'] = self.compiler_launcher + OPTION['SKIP_CMAKE'] = self.skip_cmake + OPTION['SKIP_MAKE_INSTALL'] = self.skip_make_install + OPTION['SKIP_PACKAGING'] = self.skip_packaging + # Logging options: + # 'quiet' and 'verbose-build' are deprecated, + # log-level has higher priority when used. + OPTION['LOG_LEVEL'] = self.log_level + OPTION['VERBOSE_BUILD'] = self.verbose_build + # The OPTION["QUIET"] doesn't need to be initialized with a value + # because is an argument that it will not be removed due to being + # a setuptools argument as well. + + # By default they are False, so we check if they changed with xor + if bool(OPTION["QUIET"]) != bool(OPTION["VERBOSE_BUILD"]): + log.warning("Using --quiet and --verbose-build is deprecated. " + "Please use --log-level=quiet or --log-level=verbose instead.") + # We assign a string value instead of the enum + # because is what we get from the command line. + # Later we assign the enum + if OPTION["QUIET"]: + OPTION["LOG_LEVEL"] = "quiet" + elif OPTION["VERBOSE_BUILD"]: + OPTION["LOG_LEVEL"] = "verbose" + + if OPTION["LOG_LEVEL"] not in ("quiet", "info", "verbose"): + log.error(f"Invalid value for log level: '--log-level={OPTION['LOG_LEVEL']}'. " + "Use 'quiet', 'info', or 'verbose'.") + sys.exit(-1) + else: + if OPTION["LOG_LEVEL"] == "quiet": + OPTION["LOG_LEVEL"] = LogLevel.QUIET + log.setLevel(logging.ERROR) + elif OPTION["LOG_LEVEL"] == "info": + OPTION["LOG_LEVEL"] = LogLevel.INFO + log.setLevel(logging.INFO) + elif OPTION["LOG_LEVEL"] == "verbose": + OPTION["LOG_LEVEL"] = LogLevel.VERBOSE + log.setLevel(logging.DEBUG) + + OPTION['SANITIZE_ADDRESS'] = self.sanitize_address + OPTION['SHORTER_PATHS'] = self.shorter_paths + OPTION['DOC_BUILD_ONLINE'] = self.doc_build_online + if self.unity: + log.warning("Using --unity no longer has any effect, " + "Unity build mode is now the default.") + OPTION['UNITY'] = not self.no_unity + OPTION['UNITY_BUILD_BATCH_SIZE'] = self.unity_build_batch_size + + qtpaths_abs_path = None + if self.qtpaths and Path(self.qtpaths).exists(): + qtpaths_abs_path = Path(self.qtpaths).resolve() + + # FIXME PYSIDE7: Remove qmake handling + # make qtinfo.py independent of relative paths. + qmake_abs_path = None + if self.qmake: + qmake_abs_path = Path(self.qmake).resolve() + OPTION['QMAKE'] = qmake_abs_path + OPTION['HAS_QMAKE_OPTION'] = self.has_qmake_option + OPTION['QT_VERSION'] = self.qt + self.qt_host_path = OPTION['QT_HOST_PATH'] + OPTION['QT_TARGET_PATH'] = self.qt_target_path + + qt_target_path = self.qt_target_path + + # We use the CMake project to find host Qt if neither qmake or + # qtpaths is available. This happens when building the host + # tools in the overall cross-building process. + use_cmake = False + if (using_cmake_toolchain_file or (not self.qmake + and not self.qtpaths and self.qt_target_path)): + use_cmake = True + + QtInfo().setup(qtpaths_abs_path, self.cmake, qmake_abs_path, + self.has_qmake_option, + use_cmake=use_cmake, + qt_target_path=qt_target_path, + cmake_toolchain_file=cmake_toolchain_file) + + if 'build_base_docs' not in sys.argv: + try: + QtInfo().prefix_dir + except Exception as e: + if not self.qt_target_path: + log.error( + "\nCould not find Qt. You can pass the --qt-target-path=<qt-dir> option " + "as a hint where to find Qt. Error was:\n\n\n") + else: + log.error( + f"\nCould not find Qt via provided option --qt-target-path={qt_target_path}" + "Error was:\n\n\n") + raise e + + OPTION['CMAKE'] = self.cmake.resolve() + OPTION['OPENSSL'] = self.openssl + OPTION['SHIBOKEN_CONFIG_DIR'] = self.shiboken_config_dir + if self.shiboken_config_dir: + _warn_deprecated_option('shiboken-config-dir', 'shiboken-target-path') + + self.shiboken_host_path = OPTION['SHIBOKEN_HOST_PATH'] + self.shiboken_host_path_query_file = OPTION['SHIBOKEN_HOST_PATH_QUERY_FILE'] + + if not self.shiboken_host_path and self.shiboken_host_path_query_file: + try: + queried_shiboken_host_path = Path(self.shiboken_host_path_query_file).read_text() + self.shiboken_host_path = queried_shiboken_host_path + OPTION['SHIBOKEN_HOST_PATH'] = queried_shiboken_host_path + except Exception as e: + log.error( + f"\n Could not find shiboken host tools via the query file: " + f"{self.shiboken_host_path_query_file:} Error was:\n\n\n") + raise e + + OPTION['SHIBOKEN_TARGET_PATH'] = self.shiboken_target_path + OPTION['PYTHON_TARGET_PATH'] = self.python_target_path + OPTION['MAKESPEC'] = self.make_spec + OPTION['MACOS_ARCH'] = self.macos_arch + OPTION['MACOS_SYSROOT'] = self.macos_sysroot + OPTION['MACOS_DEPLOYMENT_TARGET'] = self.macos_deployment_target + OPTION['SKIP_MODULES'] = self.skip_modules + OPTION['MODULE_SUBSET'] = self.module_subset + OPTION['RPATH_VALUES'] = self.rpath + OPTION['QT_CONF_PREFIX'] = self.qt_conf_prefix + OPTION['QT_SRC'] = self.qt_src_dir + OPTION['NO_QT_TOOLS'] = self.no_qt_tools + OPTION['NO_OVERRIDE_OPTIMIZATION_FLAGS'] = self.no_size_optimization + OPTION['DISABLE_NUMPY_SUPPORT'] = self.disable_numpy_support + OPTION['ENABLE_NUMPY_SUPPORT'] = self.enable_numpy_support + OPTION['PYSIDE_NUMPY_SUPPORT'] = self.pyside_numpy_support + + if not self._extra_checks(): + sys.exit(-1) + + OPTION['PLAT_NAME'] = self.plat_name + + def _extra_checks(self): + if self.is_cross_compile and not self.plat_name: + log.error("No value provided to --plat-name while cross-compiling.") + return False + return True + + def _determine_defaults_and_check(self): + if not self.cmake: + self.cmake = Path(which("cmake")) + elif isinstance(self.cmake, str): # command line option + self.cmake = Path(self.cmake) + if not self.cmake: + log.error("cmake could not be found.") + return False + if not self.cmake.exists(): + log.error(f"'{self.cmake}' does not exist.") + return False + + # Setting up the Paths when passing via command line + if isinstance(self.qtpaths, str): + self.qtpaths = Path(self.qtpaths) + if isinstance(self.qmake, str): + self.qmake = Path(self.qmake) + if self.qt_target_path and isinstance(self.qt_target_path, str): + self.qt_target_path = Path(self.qt_target_path) + + # When cross-compiling, we only accept the qt-target-path + # option and don't rely on auto-searching in PATH or the other + # qtpaths / qmake options. + # We also don't do auto-searching if qt-target-path is passed + # explicitly. This is to help with the building of host tools + # while cross-compiling. + # Skip this process for the 'build_base_docs' command + if (not self.is_cross_compile + and not self.qt_target_path + and 'build_base_docs' not in sys.argv): + # Enforce usage of qmake in QtInfo if it was given explicitly. + if self.qmake: + self.has_qmake_option = True + _warn_deprecated_option('qmake', 'qtpaths') + + # If no tool was specified and qtpaths was not found in PATH, + # ask to provide a path to qtpaths. + if not self.qtpaths and not self.qmake and not self.qt_target_path: + log.error("No value provided to --qtpaths option. Please provide one to find Qt.") + return False + + # Validate that the given tool path exists. + if self.qtpaths and not self.qtpaths.exists(): + log.error(f"The specified qtpaths path '{self.qtpaths}' does not exist.") + return False + + if self.qmake and not self.qmake.exists(): + log.error(f"The specified qmake path '{self.qmake}' does not exist.") + return False + else: + # Check for existence, but don't require if it's not set. A + # check later will be done to see if it's needed. + if self.qt_target_path and not self.qt_target_path.exists(): + log.error(f"Provided --qt-target-path='{self.qt_target_path}' " + "path does not exist.") + return False + + if not self.make_spec: + self.make_spec = _AVAILABLE_MKSPECS[0] + if self.make_spec not in _AVAILABLE_MKSPECS: + log.error(f'Invalid option --make-spec "{self.make_spec}". ' + f'Available values are {_AVAILABLE_MKSPECS}') + return False + + if OPTION["JOBS"] and sys.platform == 'win32' and self.no_jom: + log.error("Option --jobs can only be used with jom on Windows.") + return False + + if sys.platform == 'win32' and OPTION["LIMITED_API"] == "yes" and self.debug: + log.error("It is not possible to make a debug build of PySide6 with limited API. " + "Please select a release build or disable limited API.") + return False + + return True diff --git a/build_scripts/platforms/__init__.py b/build_scripts/platforms/__init__.py index 571d37492..853aaad7b 100644 --- a/build_scripts/platforms/__init__.py +++ b/build_scripts/platforms/__init__.py @@ -1,38 +1,2 @@ -############################################################################# -## -## 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$ -## -############################################################################# +# Copyright (C) 2018 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only diff --git a/build_scripts/platforms/linux.py b/build_scripts/platforms/linux.py index 712739e05..b4c66d94e 100644 --- a/build_scripts/platforms/linux.py +++ b/build_scripts/platforms/linux.py @@ -1,57 +1,26 @@ -############################################################################# -## -## 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 ..utils import (copydir, copyfile, copy_icu_libs, find_files_using_glob, - linux_set_rpaths, linux_run_read_elf, linux_get_rpaths, - rpaths_has_origin) +# Copyright (C) 2018 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +from pathlib import Path + +from ..log import log from ..config import config +from ..options import OPTION +from ..utils import (copy_icu_libs, copydir, copyfile, find_files_using_glob, + linux_patch_executable) +from .. import PYSIDE, PYSIDE_UNIX_BUNDLED_TOOLS -def prepare_standalone_package_linux(self, vars): - built_modules = vars['built_modules'] +def prepare_standalone_package_linux(pyside_build, _vars, cross_build=False, is_android=False): + built_modules = _vars['built_modules'] constrain_modules = None copy_plugins = True copy_qml = True copy_translations = True copy_qt_conf = True - should_copy_icu_libs = True + + log.info("Copying files...") if config.is_internal_shiboken_generator_build(): constrain_modules = ["Core", "Network", "Xml", "XmlPatterns"] @@ -59,88 +28,116 @@ def prepare_standalone_package_linux(self, vars): copy_qml = False copy_translations = False copy_qt_conf = False - should_copy_icu_libs = False # <qt>/lib/* -> <setup>/{st_package_name}/Qt/lib - destination_lib_dir = "{st_build_dir}/{st_package_name}/Qt/lib" + destination_dir = Path("{st_build_dir}/{st_package_name}".format(**_vars)) + destination_qt_dir = destination_dir / "Qt" + destination_qt_lib_dir = destination_qt_dir / "lib" + + # android libs does not have the Qt major version + if is_android: + lib_regex = 'libQt6*.so*' + else: + lib_regex = 'libQt6*.so.?' - accepted_modules = ['libQt5*.so.?'] + accepted_modules = [lib_regex] if constrain_modules: - accepted_modules = ["libQt5" + module + "*.so.?" for module in constrain_modules] + accepted_modules = [f"libQt6{module}*.so.?" if not is_android else f"libQt6{module}*.so*" + for module in constrain_modules] accepted_modules.append("libicu*.so.??") - copydir("{qt_lib_dir}", destination_lib_dir, - filter=accepted_modules, - recursive=False, vars=vars, force_copy_symlinks=True) + if is_android: + accepted_modules.append("*-android-dependencies.xml") - if should_copy_icu_libs: + copydir("{qt_lib_dir}", destination_qt_lib_dir, + _filter=accepted_modules, + recursive=False, _vars=_vars, force_copy_symlinks=True) + + if not cross_build and not is_android: # Check if ICU libraries were copied over to the destination # Qt libdir. - resolved_destination_lib_dir = destination_lib_dir.format(**vars) - maybe_icu_libs = find_files_using_glob(resolved_destination_lib_dir, "libicu*") + maybe_icu_libs = find_files_using_glob(destination_qt_lib_dir, "libicu*") # If no ICU libraries are present in the Qt libdir (like when # Qt is built against system ICU, or in the Coin CI where ICU # libs are in a different directory) try to find out / resolve # which ICU libs are used by QtCore (if used at all) using a - # custom written ldd, and copy the ICU libs to the Pyside Qt - # dir if necessary. We choose the QtCore lib to inspect, by - # checking which QtCore library the shiboken2 executable uses. + # custom written ldd (non-cross build only), and copy the ICU + # libs to the Pyside Qt dir if necessary. + # We choose the QtCore lib to inspect, by + # checking which QtCore library the shiboken6 executable uses. if not maybe_icu_libs: - copy_icu_libs(self._patchelf_path, resolved_destination_lib_dir) + copy_icu_libs(pyside_build._patchelf_path, destination_qt_lib_dir) + + # Set RPATH for Qt libs. + if not is_android: + pyside_build.update_rpath_for_linux_qt_libraries(destination_qt_lib_dir) # Patching designer to use the Qt libraries provided in the wheel - if config.is_internal_pyside_build(): - designer_path = "{st_build_dir}/{st_package_name}/designer".format(**vars) - rpaths = linux_get_rpaths(designer_path) - if not rpaths or not rpaths_has_origin(rpaths): - rpaths.insert(0, '$ORIGIN/../lib') - new_rpaths_string = ":".join(rpaths) - linux_set_rpaths(self._patchelf_path, designer_path, new_rpaths_string) - - if self.is_webengine_built(built_modules): - copydir("{qt_lib_execs_dir}", - "{st_build_dir}/{st_package_name}/Qt/libexec", - filter=None, - recursive=False, - vars=vars) + if config.is_internal_pyside_build() and not OPTION['NO_QT_TOOLS']: + + for tool in PYSIDE_UNIX_BUNDLED_TOOLS: + tool_path = destination_dir / tool + linux_patch_executable(pyside_build._patchelf_path, tool_path) - copydir("{qt_prefix_dir}/resources", - "{st_build_dir}/{st_package_name}/Qt/resources", - filter=None, + if pyside_build.is_webengine_built(built_modules): + copydir("{qt_data_dir}/resources", + destination_qt_dir / "resources", + _filter=None, recursive=False, - vars=vars) + _vars=_vars) if copy_plugins: + is_pypy = "pypy" in pyside_build.build_classifiers # <qt>/plugins/* -> <setup>/{st_package_name}/Qt/plugins - copydir("{qt_plugins_dir}", - "{st_build_dir}/{st_package_name}/Qt/plugins", - filter=["*.so"], + plugins_target = destination_qt_dir / "plugins" + copydir("{qt_plugins_dir}", plugins_target, + _filter=["*.so"], recursive=True, - vars=vars) + _vars=_vars) + if not is_pypy and not is_android: + copydir("{install_dir}/plugins/designer", + plugins_target / "designer", + _filter=["*.so"], + recursive=False, + _vars=_vars) + + copied_plugins = pyside_build.get_shared_libraries_in_path_recursively( + plugins_target) + if not is_android: + pyside_build.update_rpath_for_linux_plugins(copied_plugins) if copy_qml: # <qt>/qml/* -> <setup>/{st_package_name}/Qt/qml + qml_plugins_target = destination_qt_dir / "qml" copydir("{qt_qml_dir}", - "{st_build_dir}/{st_package_name}/Qt/qml", - filter=None, + qml_plugins_target, + _filter=None, force=False, recursive=True, - ignore=["*.so.debug"], - vars=vars) + ignore=["*.debug"], + _vars=_vars) + copied_plugins = pyside_build.get_shared_libraries_in_path_recursively( + qml_plugins_target) + if not is_android: + pyside_build.update_rpath_for_linux_plugins( + copied_plugins, + qt_lib_dir=destination_qt_lib_dir, + is_qml_plugin=True) if copy_translations: # <qt>/translations/* -> # <setup>/{st_package_name}/Qt/translations copydir("{qt_translations_dir}", - "{st_build_dir}/{st_package_name}/Qt/translations", - filter=["*.qm", "*.pak"], + destination_qt_dir / "translations", + _filter=["*.qm", "*.pak"], force=False, - vars=vars) + _vars=_vars) if copy_qt_conf: # Copy the qt.conf file to libexec. - copyfile( - "{build_dir}/pyside2/{st_package_name}/qt.conf", - "{st_build_dir}/{st_package_name}/Qt/libexec", - vars=vars) + qt_libexec_path = destination_qt_dir / "libexec" + if not qt_libexec_path.is_dir(): + qt_libexec_path.mkdir(parents=True) + copyfile(f"{{build_dir}}/{PYSIDE}/{{st_package_name}}/qt.conf", + qt_libexec_path, _vars=_vars) diff --git a/build_scripts/platforms/macos.py b/build_scripts/platforms/macos.py index 7932db337..dbe60d343 100644 --- a/build_scripts/platforms/macos.py +++ b/build_scripts/platforms/macos.py @@ -1,50 +1,28 @@ -############################################################################# -## -## 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$ -## -############################################################################# +# Copyright (C) 2018 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only import fnmatch -import os -from ..utils import copydir, copyfile, macos_fix_rpaths_for_library, macos_add_rpath +from pathlib import Path + +from ..log import log from ..config import config +from ..options import OPTION +from ..utils import (copydir, copyfile, macos_add_rpath, + macos_fix_rpaths_for_library) +from .. import PYSIDE, PYSIDE_UNIX_BUNDLED_TOOLS + +def _macos_patch_executable(name, _vars=None): + """ Patch an executable to run with the Qt libraries. """ + upper_name = name.capitalize() + bundle = f"{{st_build_dir}}/{{st_package_name}}/{upper_name}.app".format(**_vars) + binary = f"{bundle}/Contents/MacOS/{upper_name}" + rpath = "@loader_path/../../../Qt/lib" + macos_add_rpath(rpath, binary) -def prepare_standalone_package_macos(self, vars): - built_modules = vars['built_modules'] + +def prepare_standalone_package_macos(pyside_build, _vars): + built_modules = _vars['built_modules'] constrain_modules = None copy_plugins = True @@ -52,9 +30,14 @@ def prepare_standalone_package_macos(self, vars): copy_translations = True copy_qt_conf = True + destination_dir = Path("{st_build_dir}/{st_package_name}".format(**_vars)) + destination_qt_dir = destination_dir / "Qt" + destination_qt_lib_dir = destination_qt_dir / "lib" + log.info("Copying files...") + if config.is_internal_shiboken_generator_build(): constrain_modules = ["Core", "Network", "Xml", "XmlPatterns"] - constrain_frameworks = ['Qt' + name + '.framework' for name in constrain_modules] + constrain_frameworks = [f"Qt{name}.framework" for name in constrain_modules] copy_plugins = False copy_qml = False copy_translations = False @@ -71,36 +54,34 @@ def prepare_standalone_package_macos(self, vars): no_copy_debug = True def file_variant_filter(file_name, file_full_path): - if self.qtinfo.build_type != 'debug_and_release': + if pyside_build.qtinfo.build_type != 'debug_and_release': return True if file_name.endswith('_debug.dylib') and no_copy_debug: return False return True # Patching designer to use the Qt libraries provided in the wheel - if config.is_internal_pyside_build(): - designer_bundle = "{st_build_dir}/{st_package_name}/Designer.app".format(**vars) - designer_binary = "{}/Contents/MacOS/Designer".format(designer_bundle) - rpath = "@loader_path/../../../Qt/lib" - macos_add_rpath(rpath, designer_binary) + if config.is_internal_pyside_build() and not OPTION['NO_QT_TOOLS']: + for tool in PYSIDE_UNIX_BUNDLED_TOOLS: + _macos_patch_executable(tool, _vars) # <qt>/lib/* -> <setup>/{st_package_name}/Qt/lib - if self.qt_is_framework_build(): + if pyside_build.qt_is_framework_build(): def framework_dir_filter(dir_name, parent_full_path, dir_full_path): if '.framework' in dir_name: if (dir_name.startswith('QtWebEngine') - and not self.is_webengine_built(built_modules)): + and not pyside_build.is_webengine_built(built_modules)): return False if constrain_modules and dir_name not in constrain_frameworks: return False if dir_name in ['Headers', 'fonts']: return False - if dir_full_path.endswith('Versions/Current'): + if str(dir_full_path).endswith('Versions/Current'): return False - if dir_full_path.endswith('Versions/5/Resources'): + if str(dir_full_path).endswith('Versions/5/Resources'): return False - if dir_full_path.endswith('Versions/5/Helpers'): + if str(dir_full_path).endswith('Versions/5/Helpers'): return False return general_dir_filter(dir_name, parent_full_path, dir_full_path) @@ -109,16 +90,16 @@ def prepare_standalone_package_macos(self, vars): no_copy_debug = True def framework_variant_filter(file_name, file_full_path): - if self.qtinfo.build_type != 'debug_and_release': + if pyside_build.qtinfo.build_type != 'debug_and_release': return True - dir_path = os.path.dirname(file_full_path) + dir_path = Path(file_full_path).parent in_framework = dir_path.endswith("Versions/5") if file_name.endswith('_debug') and in_framework and no_copy_debug: return False return True - copydir("{qt_lib_dir}", "{st_build_dir}/{st_package_name}/Qt/lib", - recursive=True, vars=vars, + copydir("{qt_lib_dir}", destination_qt_lib_dir, + recursive=True, _vars=_vars, ignore=["*.la", "*.a", "*.cmake", "*.pc", "*.prl"], dir_filter_function=framework_dir_filter, file_filter_function=framework_variant_filter) @@ -127,85 +108,81 @@ def prepare_standalone_package_macos(self, vars): # present rpath does not work because it assumes a symlink # from Versions/5/Helpers, thus adding two more levels of # directory hierarchy. - if self.is_webengine_built(built_modules): - qt_lib_path = "{st_build_dir}/{st_package_name}/Qt/lib".format(**vars) - bundle = "QtWebEngineCore.framework/Helpers/" - bundle += "QtWebEngineProcess.app" + if pyside_build.is_webengine_built(built_modules): + bundle = Path("QtWebEngineCore.framework/Helpers/") / "QtWebEngineProcess.app" binary = "Contents/MacOS/QtWebEngineProcess" - webengine_process_path = os.path.join(bundle, binary) - final_path = os.path.join(qt_lib_path, webengine_process_path) + webengine_process_path = bundle / binary + final_path = destination_qt_lib_dir / webengine_process_path rpath = "@loader_path/../../../../../" macos_fix_rpaths_for_library(final_path, rpath) else: ignored_modules = [] - if not self.is_webengine_built(built_modules): - ignored_modules.extend(['libQt5WebEngine*.dylib']) - if 'WebKit' not in built_modules: - ignored_modules.extend(['libQt5WebKit*.dylib']) - accepted_modules = ['libQt5*.5.dylib'] + if not pyside_build.is_webengine_built(built_modules): + ignored_modules.extend(['libQt6WebEngine*.dylib']) + accepted_modules = ['libQt6*.6.dylib'] if constrain_modules: - accepted_modules = ["libQt5" + module + "*.5.dylib" for module in constrain_modules] + accepted_modules = [f"libQt6{module}*.6.dylib" for module in constrain_modules] - copydir("{qt_lib_dir}", - "{st_build_dir}/{st_package_name}/Qt/lib", - filter=accepted_modules, + copydir("{qt_lib_dir}", destination_qt_lib_dir, + _filter=accepted_modules, ignore=ignored_modules, file_filter_function=file_variant_filter, - recursive=True, vars=vars, force_copy_symlinks=True) + recursive=True, _vars=_vars, force_copy_symlinks=True) - if self.is_webengine_built(built_modules): - copydir("{qt_lib_execs_dir}", - "{st_build_dir}/{st_package_name}/Qt/libexec", - filter=None, + if pyside_build.is_webengine_built(built_modules): + copydir("{qt_data_dir}/resources", + destination_qt_dir / "resources", + _filter=None, recursive=False, - vars=vars) - - copydir("{qt_prefix_dir}/resources", - "{st_build_dir}/{st_package_name}/Qt/resources", - filter=None, - recursive=False, - vars=vars) + _vars=_vars) # Fix rpath for WebEngine process executable. - qt_libexec_path = "{st_build_dir}/{st_package_name}/Qt/libexec".format(**vars) + qt_libexec_path = Path(destination_qt_dir) / "libexec" binary = "QtWebEngineProcess" - final_path = os.path.join(qt_libexec_path, binary) + final_path = qt_libexec_path / binary rpath = "@loader_path/../lib" macos_fix_rpaths_for_library(final_path, rpath) if copy_qt_conf: # Copy the qt.conf file to libexec. + if not qt_libexec_path.is_dir(): + qt_libexec_path.mkdir(parents=True) copyfile( - "{build_dir}/pyside2/{st_package_name}/qt.conf", - "{st_build_dir}/{st_package_name}/Qt/libexec", - vars=vars) + f"{{build_dir}}/{PYSIDE}/{{st_package_name}}/qt.conf", + qt_libexec_path, _vars=_vars) if copy_plugins: + is_pypy = "pypy" in pyside_build.build_classifiers # <qt>/plugins/* -> <setup>/{st_package_name}/Qt/plugins - copydir("{qt_plugins_dir}", - "{st_build_dir}/{st_package_name}/Qt/plugins", - filter=["*.dylib"], + plugins_target = destination_qt_dir / "plugins" + filters = ["*.dylib"] + copydir("{qt_plugins_dir}", plugins_target, + _filter=filters, recursive=True, dir_filter_function=general_dir_filter, file_filter_function=file_variant_filter, - vars=vars) + _vars=_vars) + if not is_pypy: + copydir("{install_dir}/plugins/designer", + plugins_target / "designer", + _filter=filters, + recursive=False, + _vars=_vars) if copy_qml: # <qt>/qml/* -> <setup>/{st_package_name}/Qt/qml - copydir("{qt_qml_dir}", - "{st_build_dir}/{st_package_name}/Qt/qml", - filter=None, + copydir("{qt_qml_dir}", destination_qt_dir / "qml", + _filter=None, recursive=True, force=False, dir_filter_function=general_dir_filter, file_filter_function=file_variant_filter, - vars=vars) + _vars=_vars) if copy_translations: # <qt>/translations/* -> # <setup>/{st_package_name}/Qt/translations - copydir("{qt_translations_dir}", - "{st_build_dir}/{st_package_name}/Qt/translations", - filter=["*.qm", "*.pak"], + copydir("{qt_translations_dir}", destination_qt_dir / "translations", + _filter=["*.qm", "*.pak"], force=False, - vars=vars) + _vars=_vars) diff --git a/build_scripts/platforms/unix.py b/build_scripts/platforms/unix.py index b842510ff..3333f5f96 100644 --- a/build_scripts/platforms/unix.py +++ b/build_scripts/platforms/unix.py @@ -1,227 +1,253 @@ -############################################################################# -## -## 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$ -## -############################################################################# - -import os +# Copyright (C) 2018 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + import sys -import fnmatch -from .linux import prepare_standalone_package_linux -from .macos import prepare_standalone_package_macos +from pathlib import Path +from ..log import log from ..config import config from ..options import OPTION -from ..utils import copydir, copyfile, makefile -from ..utils import regenerate_qt_resources +from ..utils import copydir, copyfile, copy_qt_metatypes, makefile +from .. import PYSIDE, SHIBOKEN +from .linux import prepare_standalone_package_linux +from .macos import prepare_standalone_package_macos +from .. import PYSIDE_UNIX_BIN_TOOLS, PYSIDE_UNIX_LIBEXEC_TOOLS, PYSIDE_UNIX_BUNDLED_TOOLS + + +def _macos_copy_gui_executable(name, _vars=None): + """macOS helper: Copy a GUI executable from the .app folder and return the + files""" + app_name = f"{name.capitalize()}.app" + return copydir(f"{{install_dir}}/bin/{app_name}", + f"{{st_build_dir}}/{{st_package_name}}/{app_name}", + _filter=None, recursive=True, + force=False, _vars=_vars) -def prepare_packages_posix(self, vars): +def _unix_copy_gui_executable(name, _vars=None): + """UNIX helper: Copy a GUI executable and return the files""" + return copydir("{install_dir}/bin/", + "{st_build_dir}/{st_package_name}/", + _filter=[name], + force=False, _vars=_vars) + + +def _copy_gui_executable(name, _vars=None): + """Copy a GUI executable and return the files""" + if sys.platform == 'darwin': + return _macos_copy_gui_executable(name, _vars) + return _unix_copy_gui_executable(name, _vars) + + +def prepare_packages_posix(pyside_build, _vars, cross_build=False): + is_android = False + if str(OPTION['PLAT_NAME']).startswith('android'): + is_android = True + executables = [] + libexec_executables = [] + log.info("Copying files...") + + destination_dir = Path("{st_build_dir}/{st_package_name}".format(**_vars)) + destination_qt_dir = destination_dir / "Qt" # <install>/lib/site-packages/{st_package_name}/* -> # <setup>/{st_package_name} # This copies the module .so/.dylib files and various .py files # (__init__, config, git version, etc.) copydir( - "{site_packages_dir}/{st_package_name}", - "{st_build_dir}/{st_package_name}", - vars=vars) + "{site_packages_dir}/{st_package_name}", destination_dir, + _vars=_vars) - generated_config = self.get_built_pyside_config(vars) + generated_config = pyside_build.get_built_pyside_config(_vars) def adjusted_lib_name(name, version): postfix = '' - if sys.platform.startswith('linux'): - postfix = '.so.' + version + if config.is_cross_compile() and is_android: + postfix = ".so" + elif sys.platform.startswith('linux'): + postfix = f".so.{version}" elif sys.platform == 'darwin': - postfix = '.' + version + '.dylib' + postfix = f".{version}.dylib" return name + postfix if config.is_internal_shiboken_module_build(): - # <build>/shiboken2/doc/html/* -> - # <setup>/{st_package_name}/docs/shiboken2 + # <build>/shiboken6/doc/html/* -> + # <setup>/{st_package_name}/docs/shiboken6 copydir( - "{build_dir}/shiboken2/doc/html", - "{st_build_dir}/{st_package_name}/docs/shiboken2", - force=False, vars=vars) + f"{{build_dir}}/{SHIBOKEN}/doc/html", + f"{{st_build_dir}}/{{st_package_name}}/docs/{SHIBOKEN}", + force=False, _vars=_vars) # <install>/lib/lib* -> {st_package_name}/ copydir( - "{install_dir}/lib/", - "{st_build_dir}/{st_package_name}", - filter=[ + "{install_dir}/lib/", destination_dir, + _filter=[ adjusted_lib_name("libshiboken*", generated_config['shiboken_library_soversion']), ], - recursive=False, vars=vars, force_copy_symlinks=True) + recursive=False, _vars=_vars, force_copy_symlinks=True) if config.is_internal_shiboken_generator_build(): # <install>/bin/* -> {st_package_name}/ - executables.extend(copydir( - "{install_dir}/bin/", - "{st_build_dir}/{st_package_name}", - filter=[ - "shiboken2", - ], - recursive=False, vars=vars)) + copydir( + "{install_dir}/bin/", destination_dir, + _filter=[SHIBOKEN], + recursive=False, _vars=_vars) # Used to create scripts directory. makefile( "{st_build_dir}/{st_package_name}/scripts/shiboken_tool.py", - vars=vars) + _vars=_vars) # For setting up setuptools entry points. copyfile( "{install_dir}/bin/shiboken_tool.py", "{st_build_dir}/{st_package_name}/scripts/shiboken_tool.py", - force=False, vars=vars) + force=False, _vars=_vars) if config.is_internal_shiboken_generator_build() or config.is_internal_pyside_build(): # <install>/include/* -> <setup>/{st_package_name}/include copydir( "{install_dir}/include/{cmake_package_name}", "{st_build_dir}/{st_package_name}/include", - vars=vars) + _vars=_vars) if config.is_internal_pyside_build(): - makefile( - "{st_build_dir}/{st_package_name}/scripts/__init__.py", - vars=vars) - - # For setting up setuptools entry points - copyfile( - "{install_dir}/bin/pyside_tool.py", - "{st_build_dir}/{st_package_name}/scripts/pyside_tool.py", - force=False, vars=vars) - - # <install>/bin/* -> {st_package_name}/ - executables.extend(copydir( - "{install_dir}/bin/", - "{st_build_dir}/{st_package_name}", - filter=[ - "pyside2-lupdate", - "uic", - "rcc", - ], - recursive=False, vars=vars)) - - # Copying designer - if sys.platform == "darwin": + if not is_android: + makefile( + "{st_build_dir}/{st_package_name}/scripts/__init__.py", + _vars=_vars) + + scripts = ["pyside_tool.py", "metaobjectdump.py", "project.py", "qml.py", + "qtpy2cpp.py", "deploy.py"] + + script_dirs = ["qtpy2cpp_lib", "deploy_lib", "project"] + + if sys.platform.startswith("linux"): + scripts.append("android_deploy.py") + scripts.append("requirements-android.txt") + script_dirs.extend(["deploy_lib/android", + "deploy_lib/android/recipes/PySide6", + "deploy_lib/android/recipes/shiboken6",]) + + # For setting up setuptools entry points + for script in scripts: + src = f"{{install_dir}}/bin/{script}" + target = f"{{st_build_dir}}/{{st_package_name}}/scripts/{script}" + copyfile(src, target, force=False, _vars=_vars) + + for script_dir in script_dirs: + src = f"{{install_dir}}/bin/{script_dir}" + target = f"{{st_build_dir}}/{{st_package_name}}/scripts/{script_dir}" + # Exclude subdirectory tests + copydir(src, target, _filter=["*.py", "*.spec", "*.jpg", "*.icns", "*.ico"], + recursive=False, _vars=_vars) + + # <install>/bin/* -> {st_package_name}/ executables.extend(copydir( - "{install_dir}/bin/Designer.app", - "{st_build_dir}/{st_package_name}/Designer.app", - filter=None, recursive=True, - force=False, vars=vars)) - else: - copyfile( - "{install_dir}/bin/designer", - "{st_build_dir}/{st_package_name}/designer", - force=False, vars=vars) + "{install_dir}/bin/", destination_dir, + _filter=[f"{PYSIDE}-lupdate"], + recursive=False, _vars=_vars)) + + lib_exec_filters = [] + if not OPTION['NO_QT_TOOLS']: + lib_exec_filters.extend(PYSIDE_UNIX_LIBEXEC_TOOLS) + executables.extend(copydir( + "{install_dir}/bin/", destination_dir, + _filter=PYSIDE_UNIX_BIN_TOOLS, + recursive=False, _vars=_vars)) + + # Copying assistant/designer/linguist + for tool in PYSIDE_UNIX_BUNDLED_TOOLS: + executables.extend(_copy_gui_executable(tool, _vars=_vars)) + + copy_qt_metatypes(destination_qt_dir, _vars) + + # Copy libexec + built_modules = pyside_build.get_built_pyside_config(_vars)['built_modules'] + if pyside_build.is_webengine_built(built_modules): + lib_exec_filters.append('QtWebEngineProcess') + if lib_exec_filters: + libexec_executables.extend(copydir("{qt_lib_execs_dir}", + destination_qt_dir / "libexec", + _filter=lib_exec_filters, + recursive=False, + _vars=_vars)) # <install>/lib/lib* -> {st_package_name}/ copydir( - "{install_dir}/lib/", - "{st_build_dir}/{st_package_name}", - filter=[ + "{install_dir}/lib", destination_dir, + _filter=[ adjusted_lib_name("libpyside*", generated_config['pyside_library_soversion']), ], - recursive=False, vars=vars, force_copy_symlinks=True) - - # <install>/share/{st_package_name}/typesystems/* -> - # <setup>/{st_package_name}/typesystems - copydir( - "{install_dir}/share/{st_package_name}/typesystems", - "{st_build_dir}/{st_package_name}/typesystems", - vars=vars) - - # <install>/share/{st_package_name}/glue/* -> - # <setup>/{st_package_name}/glue + recursive=False, _vars=_vars, force_copy_symlinks=True) + + copydir("{qt_module_json_files_dir}", + destination_qt_dir / "modules", + _filter=["*.json"], _vars=_vars) + + if not config.is_cross_compile(): + # <install>/share/{st_package_name}/typesystems/* -> + # <setup>/{st_package_name}/typesystems + copydir( + "{install_dir}/share/{st_package_name}/typesystems", + "{st_build_dir}/{st_package_name}/typesystems", + _vars=_vars) + + # <install>/share/{st_package_name}/glue/* -> + # <setup>/{st_package_name}/glue + copydir( + "{install_dir}/share/{st_package_name}/glue", + "{st_build_dir}/{st_package_name}/glue", + _vars=_vars) + + if not is_android: + # <source>/pyside6/{st_package_name}/support/* -> + # <setup>/{st_package_name}/support/* + copydir( + f"{{build_dir}}/{PYSIDE}/{{st_package_name}}/support", + "{st_build_dir}/{st_package_name}/support", + _vars=_vars) + + # <source>/pyside6/{st_package_name}/QtAsyncio/* -> + # <setup>/{st_package_name}/QtAsyncio/* copydir( - "{install_dir}/share/{st_package_name}/glue", - "{st_build_dir}/{st_package_name}/glue", - vars=vars) + "{site_packages_dir}/{st_package_name}/QtAsyncio", + "{st_build_dir}/{st_package_name}/QtAsyncio", + _vars=_vars) - # <source>/pyside2/{st_package_name}/support/* -> - # <setup>/{st_package_name}/support/* - copydir( - "{build_dir}/pyside2/{st_package_name}/support", - "{st_build_dir}/{st_package_name}/support", - vars=vars) - - # <source>/pyside2/{st_package_name}/*.pyi -> + # <source>/pyside6/{st_package_name}/*.pyi -> # <setup>/{st_package_name}/*.pyi copydir( - "{build_dir}/pyside2/{st_package_name}", - "{st_build_dir}/{st_package_name}", - filter=["*.pyi", "py.typed"], - vars=vars) - - if not OPTION["NOEXAMPLES"]: - def pycache_dir_filter(dir_name, parent_full_path, dir_full_path): - if fnmatch.fnmatch(dir_name, "__pycache__"): - return False - return True - # examples/* -> <setup>/{st_package_name}/examples - copydir(os.path.join(self.script_dir, "examples"), - "{st_build_dir}/{st_package_name}/examples", - force=False, vars=vars, dir_filter_function=pycache_dir_filter) - # Re-generate examples Qt resource files for Python 3 - # compatibility - if sys.version_info[0] == 3: - examples_path = "{st_build_dir}/{st_package_name}/examples".format(**vars) - pyside_rcc_path = "{install_dir}/bin/rcc".format(**vars) - pyside_rcc_options = ['-g', 'python'] - regenerate_qt_resources(examples_path, pyside_rcc_path, pyside_rcc_options) + f"{{build_dir}}/{PYSIDE}/{{st_package_name}}", destination_dir, + _filter=["*.pyi", "py.typed"], + _vars=_vars) + + # copy the jar files + if is_android: + copydir( + "{install_dir}/lib/jar", + "{st_build_dir}/{st_package_name}/jar", + _vars=_vars) # Copy Qt libs to package if OPTION["STANDALONE"]: if config.is_internal_pyside_build() or config.is_internal_shiboken_generator_build(): - vars['built_modules'] = generated_config['built_modules'] + _vars['built_modules'] = generated_config['built_modules'] if sys.platform == 'darwin': - prepare_standalone_package_macos(self, vars) + prepare_standalone_package_macos(pyside_build, _vars) else: - prepare_standalone_package_linux(self, vars) + prepare_standalone_package_linux(pyside_build, _vars, cross_build, + is_android=is_android) if config.is_internal_shiboken_generator_build(): # Copy over clang before rpath patching. - self.prepare_standalone_clang(is_win=False) + pyside_build.prepare_standalone_clang(is_win=False) # Update rpath to $ORIGIN - if sys.platform.startswith('linux') or sys.platform.startswith('darwin'): - rpath_path = "{st_build_dir}/{st_package_name}".format(**vars) - self.update_rpath(rpath_path, executables) + if (sys.platform.startswith('linux') or sys.platform.startswith('darwin')) and not is_android: + pyside_build.update_rpath(executables) + if libexec_executables: + pyside_build.update_rpath(libexec_executables, libexec=True) diff --git a/build_scripts/platforms/windows_desktop.py b/build_scripts/platforms/windows_desktop.py index 750a064b4..9c29953be 100644 --- a/build_scripts/platforms/windows_desktop.py +++ b/build_scripts/platforms/windows_desktop.py @@ -1,281 +1,273 @@ -############################################################################# -## -## 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$ -## -############################################################################# +# Copyright (C) 2018 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only import functools import os -import sys -import fnmatch +import tempfile +from pathlib import Path + +from ..log import log from ..config import config from ..options import OPTION -from ..utils import copydir, copyfile, makefile -from ..utils import regenerate_qt_resources, filter_match -from ..utils import download_and_extract_7z +from ..utils import (copydir, copyfile, copy_qt_metatypes, + download_and_extract_7z, filter_match, makefile) +from .. import PYSIDE, SHIBOKEN, PYSIDE_WINDOWS_BIN_TOOLS -def prepare_packages_win32(self, vars): +def prepare_packages_win32(pyside_build, _vars): # For now, debug symbols will not be shipped into the package. copy_pdbs = False pdbs = [] - if (self.debug or self.build_type == 'RelWithDebInfo') and copy_pdbs: + if (pyside_build.debug or pyside_build.build_type == 'RelWithDebInfo') and copy_pdbs: pdbs = ['*.pdb'] + destination_dir = Path("{st_build_dir}/{st_package_name}".format(**_vars)) + destination_qt_dir = destination_dir + log.info("Copying files...") + # <install>/lib/site-packages/{st_package_name}/* -> # <setup>/{st_package_name} # This copies the module .pyd files and various .py files # (__init__, config, git version, etc.) copydir( - "{site_packages_dir}/{st_package_name}", - "{st_build_dir}/{st_package_name}", - vars=vars) + "{site_packages_dir}/{st_package_name}", destination_dir, + _vars=_vars) if config.is_internal_shiboken_module_build(): - # <build>/shiboken2/doc/html/* -> - # <setup>/{st_package_name}/docs/shiboken2 + # <build>/shiboken6/doc/html/* -> + # <setup>/{st_package_name}/docs/shiboken6 copydir( - "{build_dir}/shiboken2/doc/html", - "{st_build_dir}/{st_package_name}/docs/shiboken2", - force=False, vars=vars) + f"{{build_dir}}/{SHIBOKEN}/doc/html", + f"{{st_build_dir}}/{{st_package_name}}/docs/{SHIBOKEN}", + force=False, _vars=_vars) # <install>/bin/*.dll -> {st_package_name}/ copydir( - "{install_dir}/bin/", - "{st_build_dir}/{st_package_name}", - filter=["shiboken*.dll"], - recursive=False, vars=vars) + "{install_dir}/bin/", destination_qt_dir, + _filter=["shiboken*.dll"], + recursive=False, _vars=_vars) # <install>/lib/*.lib -> {st_package_name}/ copydir( - "{install_dir}/lib/", - "{st_build_dir}/{st_package_name}", - filter=["shiboken*.lib"], - recursive=False, vars=vars) + "{install_dir}/lib/", destination_qt_dir, + _filter=["shiboken*.lib"], + recursive=False, _vars=_vars) # @TODO: Fix this .pdb file not to overwrite release # {shibokengenerator}.pdb file. # Task-number: PYSIDE-615 copydir( - "{build_dir}/shiboken2/shibokenmodule", - "{st_build_dir}/{st_package_name}", - filter=pdbs, - recursive=False, vars=vars) + f"{{build_dir}}/{SHIBOKEN}/shibokenmodule", destination_dir, + _filter=pdbs, + recursive=False, _vars=_vars) # pdb files for libshiboken and libpyside copydir( - "{build_dir}/shiboken2/libshiboken", - "{st_build_dir}/{st_package_name}", - filter=pdbs, - recursive=False, vars=vars) + f"{{build_dir}}/{SHIBOKEN}/libshiboken", destination_dir, + _filter=pdbs, + recursive=False, _vars=_vars) if config.is_internal_shiboken_generator_build(): # <install>/bin/*.dll -> {st_package_name}/ copydir( - "{install_dir}/bin/", - "{st_build_dir}/{st_package_name}", - filter=["shiboken*.exe"], - recursive=False, vars=vars) + "{install_dir}/bin/", destination_dir, + _filter=["shiboken*.exe"], + recursive=False, _vars=_vars) # Used to create scripts directory. - makefile( - "{st_build_dir}/{st_package_name}/scripts/shiboken_tool.py", - vars=vars) + makefile(f"{destination_dir}/scripts/shiboken_tool.py", _vars=_vars) # For setting up setuptools entry points. copyfile( "{install_dir}/bin/shiboken_tool.py", - "{st_build_dir}/{st_package_name}/scripts/shiboken_tool.py", - force=False, vars=vars) + f"{destination_dir}/scripts/shiboken_tool.py", + force=False, _vars=_vars) # @TODO: Fix this .pdb file not to overwrite release # {shibokenmodule}.pdb file. # Task-number: PYSIDE-615 copydir( - "{build_dir}/shiboken2/generator", - "{st_build_dir}/{st_package_name}", - filter=pdbs, - recursive=False, vars=vars) + f"{{build_dir}}/{SHIBOKEN}/generator", destination_dir, + _filter=pdbs, + recursive=False, _vars=_vars) if config.is_internal_shiboken_generator_build() or config.is_internal_pyside_build(): # <install>/include/* -> <setup>/{st_package_name}/include copydir( "{install_dir}/include/{cmake_package_name}", - "{st_build_dir}/{st_package_name}/include", - vars=vars) + destination_dir / "include", + _vars=_vars) if config.is_internal_pyside_build(): - # <build>/pyside2/{st_package_name}/*.pdb -> + # <build>/pyside6/{st_package_name}/*.pdb -> # <setup>/{st_package_name} copydir( - "{build_dir}/pyside2/{st_package_name}", - "{st_build_dir}/{st_package_name}", - filter=pdbs, - recursive=False, vars=vars) + f"{{build_dir}}/{PYSIDE}/{{st_package_name}}", destination_dir, + _filter=pdbs, + recursive=False, _vars=_vars) - makefile( - "{st_build_dir}/{st_package_name}/scripts/__init__.py", - vars=vars) + makefile(f"{destination_dir}/scripts/__init__.py", _vars=_vars) # For setting up setuptools entry points - copyfile( - "{install_dir}/bin/pyside_tool.py", - "{st_build_dir}/{st_package_name}/scripts/pyside_tool.py", - force=False, vars=vars) + for script in ("pyside_tool.py", "metaobjectdump.py", "project.py", "qml.py", + "qtpy2cpp.py", "deploy.py"): + src = f"{{install_dir}}/bin/{script}" + target = f"{{st_build_dir}}/{{st_package_name}}/scripts/{script}" + copyfile(src, target, force=False, _vars=_vars) + + for script_dir in ("qtpy2cpp_lib", "deploy_lib", "project"): + src = f"{{install_dir}}/bin/{script_dir}" + target = f"{{st_build_dir}}/{{st_package_name}}/scripts/{script_dir}" + # Exclude subdirectory tests + copydir(src, target, _filter=["*.py", "*.spec", "*.jpg", "*.icns", "*.ico"], + recursive=False, _vars=_vars) # <install>/bin/*.exe,*.dll -> {st_package_name}/ - copydir( - "{install_dir}/bin/", - "{st_build_dir}/{st_package_name}", - filter=["pyside*.exe", "pyside*.dll", "uic.exe", "rcc.exe", "designer.exe"], - recursive=False, vars=vars) + filters = ["pyside*.exe", "pyside*.dll"] + if not OPTION['NO_QT_TOOLS']: + filters.extend([f"{tool}.exe" for tool in PYSIDE_WINDOWS_BIN_TOOLS]) + copydir("{install_dir}/bin/", destination_qt_dir, + _filter=filters, + recursive=False, _vars=_vars) + + copy_qt_metatypes(destination_qt_dir, _vars) # <install>/lib/*.lib -> {st_package_name}/ copydir( - "{install_dir}/lib/", - "{st_build_dir}/{st_package_name}", - filter=["pyside*.lib"], - recursive=False, vars=vars) + "{install_dir}/lib/", destination_dir, + _filter=["pyside*.lib"], + recursive=False, _vars=_vars) + + copydir("{qt_module_json_files_dir}", + destination_qt_dir / "modules", + _filter=["*.json"], _vars=_vars) # <install>/share/{st_package_name}/typesystems/* -> # <setup>/{st_package_name}/typesystems copydir( "{install_dir}/share/{st_package_name}/typesystems", - "{st_build_dir}/{st_package_name}/typesystems", - vars=vars) + destination_dir / "typesystems", + _vars=_vars) # <install>/share/{st_package_name}/glue/* -> # <setup>/{st_package_name}/glue copydir( "{install_dir}/share/{st_package_name}/glue", - "{st_build_dir}/{st_package_name}/glue", - vars=vars) + destination_dir / "glue", + _vars=_vars) - # <source>/pyside2/{st_package_name}/support/* -> + # <source>/pyside6/{st_package_name}/support/* -> # <setup>/{st_package_name}/support/* copydir( - "{build_dir}/pyside2/{st_package_name}/support", - "{st_build_dir}/{st_package_name}/support", - vars=vars) + f"{{build_dir}}/{PYSIDE}/{{st_package_name}}/support", + destination_dir / "support", + _vars=_vars) + + # <source>/pyside6/{st_package_name}/QtAsyncio/* -> + # <setup>/{st_package_name}/QtAsyncio/* + copydir( + "{site_packages_dir}/{st_package_name}/QtAsyncio", + "{st_build_dir}/{st_package_name}/QtAsyncio", + _vars=_vars) - # <source>/pyside2/{st_package_name}/*.pyi -> + # <source>/pyside6/{st_package_name}/*.pyi -> # <setup>/{st_package_name}/*.pyi copydir( - "{build_dir}/pyside2/{st_package_name}", - "{st_build_dir}/{st_package_name}", - filter=["*.pyi", "py.typed"], - vars=vars) + f"{{build_dir}}/{PYSIDE}/{{st_package_name}}", destination_dir, + _filter=["*.pyi", "py.typed"], + _vars=_vars) copydir( - "{build_dir}/pyside2/libpyside", - "{st_build_dir}/{st_package_name}", - filter=pdbs, - recursive=False, vars=vars) - - if not OPTION["NOEXAMPLES"]: - def pycache_dir_filter(dir_name, parent_full_path, dir_full_path): - if fnmatch.fnmatch(dir_name, "__pycache__"): - return False - return True - # examples/* -> <setup>/{st_package_name}/examples - copydir(os.path.join(self.script_dir, "examples"), - "{st_build_dir}/{st_package_name}/examples", - force=False, vars=vars, dir_filter_function=pycache_dir_filter) - # Re-generate examples Qt resource files for Python 3 - # compatibility - if sys.version_info[0] == 3: - examples_path = "{st_build_dir}/{st_package_name}/examples".format( - **vars) - pyside_rcc_path = "{install_dir}/bin/rcc.exe".format( - **vars) - pyside_rcc_options = ['-g', 'python'] - regenerate_qt_resources(examples_path, pyside_rcc_path, pyside_rcc_options) - - if vars['ssl_libs_dir']: + f"{{build_dir}}/{PYSIDE}/libpyside", destination_dir, + _filter=pdbs, + recursive=False, _vars=_vars) + + if _vars['ssl_libs_dir']: # <ssl_libs>/* -> <setup>/{st_package_name}/openssl - copydir("{ssl_libs_dir}", "{st_build_dir}/{st_package_name}/openssl", - filter=[ + copydir("{ssl_libs_dir}", destination_dir / "openssl", + _filter=[ "libeay32.dll", "ssleay32.dll"], - force=False, vars=vars) + force=False, _vars=_vars) if config.is_internal_shiboken_module_build(): # The C++ std library dlls need to be packaged with the # shiboken module, because libshiboken uses C++ code. - copy_msvc_redist_files(vars, "{build_dir}/msvc_redist".format(**vars)) + copy_msvc_redist_files(destination_dir) if config.is_internal_pyside_build() or config.is_internal_shiboken_generator_build(): - copy_qt_artifacts(self, copy_pdbs, vars) - - -def copy_msvc_redist_files(vars, redist_target_path): - # MSVC redistributable file list. - msvc_redist = [ - "concrt140.dll", - "msvcp140.dll", - "ucrtbase.dll", - "vcamp140.dll", - "vccorlib140.dll", - "vcomp140.dll", - "vcruntime140.dll" - ] + copy_qt_artifacts(pyside_build, destination_qt_dir, copy_pdbs, _vars) + copy_msvc_redist_files(destination_dir) + + +# MSVC redistributable file list. +msvc_redist = [ + "concrt140.dll", + "msvcp140.dll", + "vcamp140.dll", + "vccorlib140.dll", + "vcomp140.dll", + "vcruntime140.dll", + "vcruntime140_1.dll", + "msvcp140_1.dll", + "msvcp140_2.dll", + "msvcp140_codecvt_ids.dll" +] + + +def copy_msvc_redist_files(destination_dir): + in_coin = os.environ.get('COIN_LAUNCH_PARAMETERS', None) + if in_coin is None: + log.info("Qt dependency DLLs (MSVC redist) will not be copied.") + return # Make a directory where the files should be extracted. - if not os.path.exists(redist_target_path): - os.makedirs(redist_target_path) - + if not destination_dir.exists(): + destination_dir.mkdir(parents=True) + + # Copy Qt dependency DLLs (MSVC) from PATH when building on Qt CI. + paths = os.environ["PATH"].split(os.pathsep) + for path in paths: + try: + for f in Path(path).glob("*140*.dll"): + if f.name in msvc_redist: + copyfile(f, Path(destination_dir) / f.name) + msvc_redist.remove(f.name) + if not msvc_redist: + break + except WindowsError: + continue + + if msvc_redist: + msg = "The following Qt dependency DLLs (MSVC redist) were not found: {msvc_redist}" + raise FileNotFoundError(msg) + + +def copy_qt_dependency_dlls(_vars, destination_qt_dir, artifacts): # Extract Qt dependency dlls when building on Qt CI. in_coin = os.environ.get('COIN_LAUNCH_PARAMETERS', None) - if in_coin is not None: - redist_url = "http://download.qt.io/development_releases/prebuilt/vcredist/" - zip_file = "pyside_qt_deps_64.7z" - if "{target_arch}".format(**vars) == "32": - zip_file = "pyside_qt_deps_32.7z" - download_and_extract_7z(redist_url + zip_file, redist_target_path) - else: - print("Qt dependency DLLs (MSVC redist) will not be downloaded and extracted.") - - copydir(redist_target_path, - "{st_build_dir}/{st_package_name}", - filter=msvc_redist, recursive=False, vars=vars) - - -def copy_qt_artifacts(self, copy_pdbs, vars): - built_modules = self.get_built_pyside_config(vars)['built_modules'] + if in_coin is None: + log.info("Qt dependency DLLs will not be downloaded and extracted.") + return + + with tempfile.TemporaryDirectory() as temp_path: + redist_url = "https://download.qt.io/development_releases/prebuilt/vcredist/" + zip_file = "pyside_qt_deps_64_2019.7z" + if "{target_arch}".format(**_vars) == "32": + zip_file = "pyside_qt_deps_32_2019.7z" + try: + download_and_extract_7z(redist_url + zip_file, temp_path) + except Exception as e: + log.warning(f"Download failed: {type(e).__name__}: {e}") + log.warning("download.qt.io is down, try with mirror") + redist_url = "https://master.qt.io/development_releases/prebuilt/vcredist/" + download_and_extract_7z(redist_url + zip_file, temp_path) + copydir(temp_path, destination_qt_dir, _filter=artifacts, recursive=False, _vars=_vars) + + +def copy_qt_artifacts(pyside_build, destination_qt_dir, copy_pdbs, _vars): + built_modules = pyside_build.get_built_pyside_config(_vars)['built_modules'] constrain_modules = None copy_plugins = True @@ -283,7 +275,6 @@ def copy_qt_artifacts(self, copy_pdbs, vars): copy_translations = True copy_qt_conf = True copy_qt_permanent_artifacts = True - copy_msvc_redist = False copy_clang = False if config.is_internal_shiboken_generator_build(): @@ -293,13 +284,16 @@ def copy_qt_artifacts(self, copy_pdbs, vars): copy_translations = False copy_qt_conf = False copy_qt_permanent_artifacts = False - copy_msvc_redist = True copy_clang = True # <qt>/bin/*.dll and Qt *.exe -> <setup>/{st_package_name} qt_artifacts_permanent = [ + "avcodec-60.dll", + "avformat-60.dll", + "avutil-58.dll", + "swresample-4.dll", + "swscale-7.dll", "opengl*.dll", - "d3d*.dll", "designer.exe", "linguist.exe", "lrelease.exe", @@ -313,41 +307,28 @@ def copy_qt_artifacts(self, copy_pdbs, vars): "libEGL{}.dll", "libGLESv2{}.dll" ] - if self.qtinfo.build_type != 'debug_and_release': + if pyside_build.qtinfo.build_type != 'debug_and_release': egl_suffix = '*' - elif self.debug: + elif pyside_build.debug: egl_suffix = 'd' else: egl_suffix = '' qt_artifacts_egl = [a.format(egl_suffix) for a in qt_artifacts_egl] - artifacts = [] if copy_qt_permanent_artifacts: - artifacts += qt_artifacts_permanent - artifacts += qt_artifacts_egl - - if copy_msvc_redist: - # The target path has to be qt_bin_dir at the moment, - # because the extracted archive also contains the opengl32sw - # and the d3dcompiler dlls, which are copied not by this - # function, but by the copydir below. - copy_msvc_redist_files(vars, "{qt_bin_dir}".format(**vars)) - - if artifacts: - copydir("{qt_bin_dir}", - "{st_build_dir}/{st_package_name}", - filter=artifacts, recursive=False, vars=vars) + artifacts = qt_artifacts_permanent + qt_artifacts_egl + copy_qt_dependency_dlls(_vars, destination_qt_dir, artifacts) # <qt>/bin/*.dll and Qt *.pdbs -> <setup>/{st_package_name} part two # File filter to copy only debug or only release files. if constrain_modules: - qt_dll_patterns = ["Qt5" + x + "{}.dll" for x in constrain_modules] + qt_dll_patterns = [f"Qt6{x}{{}}.dll" for x in constrain_modules] if copy_pdbs: - qt_dll_patterns += ["Qt5" + x + "{}.pdb" for x in constrain_modules] + qt_dll_patterns += [f"Qt6{x}{{}}.pdb" for x in constrain_modules] else: - qt_dll_patterns = ["Qt5*{}.dll", "lib*{}.dll"] + qt_dll_patterns = ["Qt6*{}.dll", "lib*{}.dll"] if copy_pdbs: - qt_dll_patterns += ["Qt5*{}.pdb", "lib*{}.pdb"] + qt_dll_patterns += ["Qt6*{}.pdb", "lib*{}.pdb"] def qt_build_config_filter(patterns, file_name, file_full_path): release = [a.format('') for a in patterns] @@ -356,74 +337,83 @@ def copy_qt_artifacts(self, copy_pdbs, vars): # If qt is not a debug_and_release build, that means there # is only one set of shared libraries, so we can just copy # them. - if self.qtinfo.build_type != 'debug_and_release': + if pyside_build.qtinfo.build_type != 'debug_and_release': if filter_match(file_name, release): return True return False + # Setup Paths + file_name = Path(file_name) + file_full_path = Path(file_full_path) + # In debug_and_release case, choosing which files to copy # is more difficult. We want to copy only the files that - # match the PySide2 build type. So if PySide2 is built in + # match the PySide6 build type. So if PySide6 is built in # debug mode, we want to copy only Qt debug libraries # (ending with "d.dll"). Or vice versa. The problem is that # some libraries have "d" as the last character of the - # actual library name (for example Qt5Gamepad.dll and - # Qt5Gamepadd.dll). So we can't just match a pattern ending + # actual library name (for example Qt6Gamepad.dll and + # Qt6Gamepadd.dll). So we can't just match a pattern ending # in "d". Instead we check if there exists a file with the # same name plus an additional "d" at the end, and using # that information we can judge if the currently processed # file is a debug or release file. - # e.g. ["Qt5Cored", ".dll"] - file_split = os.path.splitext(file_name) - file_base_name = file_split[0] - file_ext = file_split[1] + # e.g. ["Qt6Cored", ".dll"] + file_base_name = file_name.stem + file_ext = file_name.suffix # e.g. "/home/work/qt/qtbase/bin" - file_path_dir_name = os.path.dirname(file_full_path) - # e.g. "Qt5Coredd" - maybe_debug_name = "{}d".format(file_base_name) - if self.debug: - filter = debug + file_path_dir_name = file_full_path.parent + # e.g. "Qt6Coredd" + maybe_debug_name = f"{file_base_name}d" + if pyside_build.debug: + _filter = debug def predicate(path): - return not os.path.exists(path) + return not path.exists() else: - filter = release + _filter = release def predicate(path): - return os.path.exists(path) - # e.g. "/home/work/qt/qtbase/bin/Qt5Coredd.dll" - other_config_path = os.path.join(file_path_dir_name, maybe_debug_name + file_ext) + return path.exists() + # e.g. "/home/work/qt/qtbase/bin/Qt6Coredd.dll" + other_config_path = file_path_dir_name / (maybe_debug_name + file_ext) - if (filter_match(file_name, filter) and predicate(other_config_path)): + if (filter_match(file_name, _filter) and predicate(other_config_path)): return True return False qt_dll_filter = functools.partial(qt_build_config_filter, qt_dll_patterns) - copydir("{qt_bin_dir}", - "{st_build_dir}/{st_package_name}", + copydir("{qt_bin_dir}", destination_qt_dir, file_filter_function=qt_dll_filter, - recursive=False, vars=vars) + recursive=False, _vars=_vars) if copy_plugins: + is_pypy = "pypy" in pyside_build.build_classifiers # <qt>/plugins/* -> <setup>/{st_package_name}/plugins + plugins_target = f"{destination_qt_dir}/plugins" plugin_dll_patterns = ["*{}.dll"] pdb_pattern = "*{}.pdb" if copy_pdbs: plugin_dll_patterns += [pdb_pattern] plugin_dll_filter = functools.partial(qt_build_config_filter, plugin_dll_patterns) - copydir("{qt_plugins_dir}", "{st_build_dir}/{st_package_name}/plugins", + copydir("{qt_plugins_dir}", plugins_target, file_filter_function=plugin_dll_filter, - vars=vars) + _vars=_vars) + if not is_pypy: + copydir("{install_dir}/plugins/designer", + f"{plugins_target}/designer", + _filter=["*.dll"], + recursive=False, + _vars=_vars) if copy_translations: # <qt>/translations/* -> <setup>/{st_package_name}/translations - copydir("{qt_translations_dir}", - "{st_build_dir}/{st_package_name}/translations", - filter=["*.qm", "*.pak"], + copydir("{qt_translations_dir}", f"{destination_qt_dir}/translations", + _filter=["*.qm", "*.pak"], force=False, - vars=vars) + _vars=_vars) if copy_qml: # <qt>/qml/* -> <setup>/{st_package_name}/qml @@ -432,42 +422,40 @@ def copy_qt_artifacts(self, copy_pdbs, vars): qml_ignore = [a.format('') for a in qml_ignore_patterns] # Copy all files that are not dlls and pdbs (.qml, qmldir). - copydir("{qt_qml_dir}", "{st_build_dir}/{st_package_name}/qml", + copydir("{qt_qml_dir}", f"{destination_qt_dir}/qml", ignore=qml_ignore, force=False, recursive=True, - vars=vars) + _vars=_vars) if copy_pdbs: qml_dll_patterns += [pdb_pattern] qml_dll_filter = functools.partial(qt_build_config_filter, qml_dll_patterns) # Copy all dlls (and possibly pdbs). - copydir("{qt_qml_dir}", "{st_build_dir}/{st_package_name}/qml", + copydir("{qt_qml_dir}", f"{destination_qt_dir}/qml", file_filter_function=qml_dll_filter, force=False, recursive=True, - vars=vars) + _vars=_vars) - if self.is_webengine_built(built_modules): - copydir("{qt_prefix_dir}/resources", - "{st_build_dir}/{st_package_name}/resources", - filter=None, + if pyside_build.is_webengine_built(built_modules): + copydir("{qt_data_dir}/resources", f"{destination_qt_dir}/resources", + _filter=None, recursive=False, - vars=vars) + _vars=_vars) - filter = 'QtWebEngineProcess{}.exe'.format( - 'd' if self.debug else '') - copydir("{qt_bin_dir}", - "{st_build_dir}/{st_package_name}", - filter=[filter], - recursive=False, vars=vars) + _ext = "d" if pyside_build.debug else "" + _filter = [f"QtWebEngineProcess{_ext}.exe"] + copydir("{qt_bin_dir}", destination_qt_dir, + _filter=_filter, + recursive=False, _vars=_vars) if copy_qt_conf: # Copy the qt.conf file to prefix dir. - copyfile("{build_dir}/pyside2/{st_package_name}/qt.conf", - "{st_build_dir}/{st_package_name}", - vars=vars) + copyfile(f"{{build_dir}}/{PYSIDE}/{{st_package_name}}/qt.conf", + destination_qt_dir, + _vars=_vars) if copy_clang: - self.prepare_standalone_clang(is_win=True) + pyside_build.prepare_standalone_clang(is_win=True) diff --git a/build_scripts/qp5_tool.py b/build_scripts/qfp_tool.py index 9fc37a99b..abaf48fc8 100644 --- a/build_scripts/qp5_tool.py +++ b/build_scripts/qfp_tool.py @@ -1,54 +1,17 @@ -############################################################################# -## -## Copyright (C) 2019 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 +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only -from argparse import ArgumentParser, RawTextHelpFormatter import datetime -from enum import Enum import os import re import subprocess import sys import time import warnings - +from argparse import ArgumentParser, RawTextHelpFormatter +from enum import Enum, auto +from pathlib import Path +from typing import List DESC = """ Utility script for working with Qt for Python. @@ -56,9 +19,9 @@ Utility script for working with Qt for Python. Feel free to extend! Typical Usage: -Update and build a repository: python qp5_tool -p -b +Update and build a repository: python qfp_tool -p -b -qp5_tool.py uses a configuration file "%CONFIGFILE%" +qfp_tool.py uses a configuration file "%CONFIGFILE%" in the format key=value. It is possible to use repository-specific values by adding a key postfixed by @@ -68,6 +31,8 @@ Modules-pyside-setup512=Core,Gui,Widgets,Network,Test Configuration keys: Acceleration Incredibuild or unset BuildArguments Arguments to setup.py +Generator Generator to be used for CMake. Currently, only Ninja is + supported. Jobs Number of jobs to be run simultaneously Modules Comma separated list of modules to be built (for --module-subset=) @@ -93,54 +58,65 @@ class BuildMode(Enum): MAKE = 3 -DEFAULT_BUILD_ARGS = ['--build-tests', '--skip-docs', '--quiet'] +class UnityMode(Enum): + DEFAULT = auto() + ENABLE = auto() + DISABLE = auto() + + +DISABLE_UNITY_OPTION = "--no-unity" +LOG_LEVEL_OPTION = "--log-level" +DEFAULT_BUILD_ARGS = ['--build-tests', '--skip-docs', LOG_LEVEL_OPTION, "quiet"] IS_WINDOWS = sys.platform == 'win32' INCREDIBUILD_CONSOLE = 'BuildConsole' if IS_WINDOWS else '/opt/incredibuild/bin/ib_console' # Config file keys ACCELERATION_KEY = 'Acceleration' BUILDARGUMENTS_KEY = 'BuildArguments' +GENERATOR_KEY = 'Generator' JOBS_KEY = 'Jobs' MODULES_KEY = 'Modules' PYTHON_KEY = 'Python' DEFAULT_MODULES = "Core,Gui,Widgets,Network,Test,Qml,Quick,Multimedia,MultimediaWidgets" -DEFAULT_CONFIG_FILE = "Modules={}\n".format(DEFAULT_MODULES) +DEFAULT_CONFIG_FILE = f"Modules={DEFAULT_MODULES}\n" build_mode = BuildMode.NONE opt_dry_run = False +opt_verbose = False +opt_unity_mode = UnityMode.DEFAULT -def which(needle): +def which(needle: str): """Perform a path search""" needles = [needle] if IS_WINDOWS: for ext in ("exe", "bat", "cmd"): - needles.append("{}.{}".format(needle, ext)) + needles.append(f"{needle}.{ext}") for path in os.environ.get("PATH", "").split(os.pathsep): for n in needles: - binary = os.path.join(path, n) - if os.path.isfile(binary): + binary = Path(path) / n + if binary.is_file(): return binary return None -def command_log_string(args, dir): - result = '[{}]'.format(os.path.basename(dir)) +def command_log_string(args: List[str], directory: Path): + result = f'[{directory.name}]' for arg in args: - result += ' "{}"'.format(arg) if ' ' in arg else ' {}'.format(arg) + result += f' "{arg}"' if ' ' in arg else f' {arg}' return result -def execute(args): +def execute(args: List[str]): """Execute a command and print to log""" - log_string = command_log_string(args, os.getcwd()) + log_string = command_log_string(args, Path.cwd()) print(log_string) if opt_dry_run: return exit_code = subprocess.call(args) if exit_code != 0: - raise RuntimeError('FAIL({}): {}'.format(exit_code, log_string)) + raise RuntimeError(f'FAIL({exit_code}): {log_string}') def run_process_output(args): @@ -156,9 +132,6 @@ def run_git(args): """Run git in the current directory and its submodules""" args.insert(0, git) # run in repo execute(args) # run for submodules - module_args = [git, "submodule", "foreach"] - module_args.extend(args) - execute(module_args) def expand_reference(cache_dict, value): @@ -192,7 +165,7 @@ def edit_config_file(): exit_code = subprocess.call([editor(), config_file]) except Exception as e: reason = str(e) - print('Unable to launch: {}: {}'.format(editor(), reason)) + print(f'Unable to launch: {editor()}: {reason}') return exit_code @@ -208,9 +181,10 @@ def read_config_file(file_name): keyPattern = re.compile(r'^\s*([A-Za-z0-9\_\-]+)\s*=\s*(.*)$') with open(file_name) as f: while True: - line = f.readline().rstrip() + line = f.readline() if not line: break + line = line.rstrip() match = keyPattern.match(line) if match: key = match.group(1) @@ -223,13 +197,13 @@ def read_config_file(file_name): def read_config(key): """ - Read a value from the '$HOME/.qp5_tool' configuration file. When given + Read a value from the '$HOME/.qfp_tool' configuration file. When given a key 'key' for the repository directory '/foo/qt-5', check for the repo-specific value 'key-qt5' and then for the general 'key'. """ if not config_dict: read_config_file(config_file) - repo_value = config_dict.get(key + '-' + base_dir) + repo_value = config_dict.get(f"{key}-{base_dir}") return repo_value if repo_value else config_dict.get(key) @@ -262,18 +236,33 @@ def read_config_build_arguments(): def read_config_modules_argument(): value = read_config(MODULES_KEY) if value and value != '' and value != 'all': - return '--module-subset=' + value + return f"--module-subset={value}" return None -def read_config_python_binary(): +def read_config_python_binary() -> str: binary = read_config(PYTHON_KEY) - if binary: - return binary - return 'python3' if which('python3') else 'python' + virtual_env = os.environ.get('VIRTUAL_ENV') + if not binary: + # Use 'python3' unless virtualenv is set + use_py3 = not virtual_env and which('python3') + binary = 'python3' if use_py3 else 'python' + binary = Path(binary) + if not binary.is_absolute(): + abs_path = which(str(binary)) + if abs_path: + binary = abs_path + else: + warnings.warn(f'Unable to find "{binary}"', RuntimeWarning) + if virtual_env: + if not str(binary).startswith(virtual_env): + w = f'Python "{binary}" is not under VIRTUAL_ENV "{virtual_env}"' + warnings.warn(w, RuntimeWarning) + return str(binary) -def get_config_file(base_name): +def get_config_file(base_name) -> Path: + global user home = os.getenv('HOME') if IS_WINDOWS: # Set a HOME variable on Windows such that scp. etc. @@ -282,18 +271,18 @@ def get_config_file(base_name): home = os.getenv('HOMEDRIVE') + os.getenv('HOMEPATH') os.environ['HOME'] = home user = os.getenv('USERNAME') - config_file = os.path.join(os.getenv('APPDATA'), base_name) + config_file = Path(os.getenv('APPDATA')) / base_name else: user = os.getenv('USER') - config_dir = os.path.join(home, '.config') - if os.path.exists(config_dir): - config_file = os.path.join(config_dir, base_name) + config_dir = Path(home) / '.config' + if config_dir.exists(): + config_file = config_dir / base_name else: - config_file = os.path.join(home, '.' + base_name) + config_file = Path(home) / f".{base_name}" return config_file -def build(target): +def build(target: str): """Run configure and build steps""" start_time = time.time() @@ -301,14 +290,28 @@ def build(target): acceleration = read_acceleration_config() if not IS_WINDOWS and acceleration == Acceleration.INCREDIBUILD: arguments.append(INCREDIBUILD_CONSOLE) - arguments.append('--avoid') # caching, v0.96.74 + arguments.appendh('--avoid') # caching, v0.96.74 arguments.extend([read_config_python_binary(), 'setup.py', target]) - arguments.extend(read_config_build_arguments()) + build_arguments = read_config_build_arguments() + if opt_verbose and LOG_LEVEL_OPTION in build_arguments: + i = build_arguments.index(LOG_LEVEL_OPTION) + del build_arguments[i] + del build_arguments[i] + arguments.extend(build_arguments) + if opt_unity_mode != UnityMode.DEFAULT: + unity_disabled = DISABLE_UNITY_OPTION in build_arguments + if opt_unity_mode == UnityMode.ENABLE and unity_disabled: + arguments.remove(DISABLE_UNITY_OPTION) + elif opt_unity_mode == UnityMode.DISABLE and not unity_disabled: + arguments.append(DISABLE_UNITY_OPTION) + generator = read_config(GENERATOR_KEY) + if generator != 'Ninja': + arguments.extend(['--make-spec', 'ninja']) jobs = read_int_config(JOBS_KEY) if jobs > 1: arguments.extend(['-j', str(jobs)]) if build_mode != BuildMode.BUILD: - arguments.extend(['--reuse-build', '--ignore-git']) + arguments.append('--reuse-build') if build_mode != BuildMode.RECONFIGURE: arguments.append('--skip-cmake') modules = read_config_modules_argument() @@ -316,24 +319,34 @@ def build(target): arguments.append(modules) if IS_WINDOWS and acceleration == Acceleration.INCREDIBUILD: arg_string = ' '.join(arguments) - arguments = [INCREDIBUILD_CONSOLE, '/command={}'.format(arg_string)] + arguments = [INCREDIBUILD_CONSOLE, f'/command={arg_string}'] execute(arguments) elapsed_time = int(time.time() - start_time) - print('--- Done({}s) ---'.format(elapsed_time)) + print(f'--- Done({elapsed_time}s) ---') + + +def build_base_docs(): + arguments = [read_config_python_binary(), "setup.py", "build_base_docs", "--log-level", + "quiet"] + for build_arg in read_config_build_arguments(): + if build_arg.startswith("--qt-src-dir="): + arguments.append(build_arg) + break + execute(arguments) def run_tests(): """Run tests redirected into a log file with a time stamp""" logfile_name = datetime.datetime.today().strftime("test_%Y%m%d_%H%M.txt") binary = sys.executable - command = '"{}" testrunner.py test > {}'.format(binary, logfile_name) - print(command_log_string([command], os.getcwd())) + command = f'"{binary}" testrunner.py test > {logfile_name}' + print(command_log_string([command], Path.cwd())) start_time = time.time() result = 0 if opt_dry_run else os.system(command) elapsed_time = int(time.time() - start_time) - print('--- Done({}s) ---'.format(elapsed_time)) + print(f'--- Done({elapsed_time}s) ---') return result @@ -358,7 +371,15 @@ def create_argument_parser(desc): help='cmake + Make (continue broken build)') parser.add_argument('--test', '-t', action='store_true', help='Run tests') + parser.add_argument('--Documentation', '-D', action='store_true', + help='Run build_base_docs') parser.add_argument('--version', '-v', action='version', version='%(prog)s 1.0') + parser.add_argument('--verbose', '-V', action='store_true', + help='Turn off --quiet specified in build arguments') + parser.add_argument('--unity', '-u', action='store_true', + help='Force unity build') + parser.add_argument('--no-unity', action='store_true', + help='Turn off --unity specified in build arguments') return parser @@ -368,10 +389,16 @@ if __name__ == '__main__': config_file = None user = None - config_file = get_config_file('qp5_tool.conf') - argument_parser = create_argument_parser(DESC.replace('%CONFIGFILE%', config_file)) + config_file = get_config_file('qfp_tool.conf') + argument_parser = create_argument_parser(DESC.replace('%CONFIGFILE%', str(config_file))) options = argument_parser.parse_args() opt_dry_run = options.dry_run + opt_verbose = options.verbose + + if options.unity: + opt_unity_mode = UnityMode.ENABLE + elif options.no_unity: + opt_unity_mode = UnityMode.DISABLE if options.edit: sys.exit(edit_config_file()) @@ -383,29 +410,30 @@ if __name__ == '__main__': elif options.Make: build_mode = BuildMode.RECONFIGURE - if build_mode == BuildMode.NONE and not (options.clean or options.reset - or options.pull or options.test): + if build_mode == BuildMode.NONE and not (options.clean or options.reset or options.pull + or options.Documentation or options.test): argument_parser.print_help() sys.exit(0) - git = which('git') - if git is None: + git = 'git' + if which(git) is None: warnings.warn('Unable to find git', RuntimeWarning) sys.exit(-1) - if not os.path.exists(config_file): + if not config_file.exists(): print('Create initial config file ', config_file, " ..") with open(config_file, 'w') as f: f.write(DEFAULT_CONFIG_FILE.format(' '.join(DEFAULT_BUILD_ARGS))) - while not os.path.exists('.gitmodules'): - cwd = os.getcwd() - if cwd == '/' or (IS_WINDOWS and len(cwd) < 4): + while not Path(".git").exists(): + cwd = Path.cwd() + cwd_s = os.fspath(cwd) + if cwd_s == '/' or (IS_WINDOWS and len(cwd_s) < 4): warnings.warn('Unable to find git root', RuntimeWarning) sys.exit(-1) - os.chdir(os.path.dirname(cwd)) + os.chdir(cwd.parent) - base_dir = os.path.basename(os.getcwd()) + base_dir = Path.cwd().name if options.clean: run_git(['clean', '-dxf']) @@ -420,6 +448,9 @@ if __name__ == '__main__': target = 'build' if options.no_install else 'install' build(target) + if options.Documentation: + build_base_docs() + if options.test: sys.exit(run_tests()) diff --git a/build_scripts/qtinfo.py b/build_scripts/qtinfo.py index 4dc976360..1eb7c4909 100644 --- a/build_scripts/qtinfo.py +++ b/build_scripts/qtinfo.py @@ -1,241 +1,261 @@ -############################################################################# -## -## 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$ -## -############################################################################# +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only import os -import sys -import re import subprocess -from distutils.spawn import find_executable +from pathlib import Path + +from .utils import (configure_cmake_project, parse_cmake_project_message_info, + platform_cmake_options) class QtInfo(object): - def __init__(self, qmake_command=None): - self.initialized = False - - if qmake_command: - self._qmake_command = qmake_command - else: - self._qmake_command = [find_executable("qmake"), ] - - # Dict to cache qmake values. - self._query_dict = {} - # Dict to cache mkspecs variables. - self._mkspecs_dict = {} - # Initialize the properties. - self._init_properties() - - def get_qmake_command(self): - qmake_command_string = self._qmake_command[0] - for entry in self._qmake_command[1:]: - qmake_command_string += " {}".format(entry) - return qmake_command_string - - def get_version(self): - return self.get_property("QT_VERSION") - - def get_bins_path(self): - return self.get_property("QT_INSTALL_BINS") - - def get_libs_path(self): - return self.get_property("QT_INSTALL_LIBS") - - def get_libs_execs_path(self): - return self.get_property("QT_INSTALL_LIBEXECS") - - def get_plugins_path(self): - return self.get_property("QT_INSTALL_PLUGINS") - - def get_prefix_path(self): - return self.get_property("QT_INSTALL_PREFIX") - - def get_imports_path(self): - return self.get_property("QT_INSTALL_IMPORTS") - - def get_translations_path(self): - return self.get_property("QT_INSTALL_TRANSLATIONS") - - def get_headers_path(self): - return self.get_property("QT_INSTALL_HEADERS") - - def get_docs_path(self): - return self.get_property("QT_INSTALL_DOCS") - - def get_qml_path(self): - return self.get_property("QT_INSTALL_QML") - - def get_macos_deployment_target(self): - """ Return value is a macOS version or None. """ - return self.get_property("QMAKE_MACOSX_DEPLOYMENT_TARGET") - - def get_build_type(self): - """ - Return value is either debug, release, debug_release, or None. - """ - return self.get_property("BUILD_TYPE") - - def get_src_dir(self): - """ Return path to Qt src dir or None.. """ - return self.get_property("QT_INSTALL_PREFIX/src") - - def get_property(self, prop_name): - if prop_name not in self._query_dict: - return None - return self._query_dict[prop_name] - - def get_properties(self): - return self._query_dict - - def get_mkspecs_variables(self): - return self._mkspecs_dict - - def _get_qmake_output(self, args_list=[]): - cmd = self._qmake_command + args_list - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=False) - output = proc.communicate()[0] - proc.wait() - if proc.returncode != 0: - return "" - if sys.version_info >= (3,): - output = str(output, 'ascii').strip() - else: - output = output.strip() - return output - - def _parse_query_properties(self, process_output): - props = {} - if not process_output: + _instance = None # singleton helpers + + def __new__(cls): # __new__ always a classmethod + if not QtInfo._instance: + QtInfo._instance = QtInfo.__QtInfo() + return QtInfo._instance + + def __getattr__(self, name): + return getattr(self._instance, name) + + def __setattr__(self, name): + return setattr(self._instance, name) + + class __QtInfo: # Python singleton + def __init__(self): + self._qtpaths_command = None + self._cmake_command = None + self._qmake_command = None + self._force_qmake = False + self._use_cmake = False + self._qt_target_path = None + self._cmake_toolchain_file = None + # Dict to cache qmake values. + self._query_dict = {} + + def setup(self, qtpaths, cmake, qmake, force_qmake, use_cmake, qt_target_path, + cmake_toolchain_file): + self._qtpaths_command = qtpaths + self._cmake_command = cmake + self._qmake_command = qmake + self._force_qmake = force_qmake + self._use_cmake = use_cmake + self._qt_target_path = qt_target_path + self._cmake_toolchain_file = cmake_toolchain_file + + @property + def qmake_command(self): + return self._qmake_command + + @property + def qtpaths_command(self): + return self._qtpaths_command + + @property + def version(self): + return self.get_property("QT_VERSION") + + @property + def version_tuple(self): + return tuple(map(int, self.version.split("."))) + + @property + def bins_dir(self): + return self.get_property("QT_INSTALL_BINS") + + @property + def data_dir(self): + return self.get_property("QT_INSTALL_DATA") + + @property + def libs_dir(self): + return self.get_property("QT_INSTALL_LIBS") + + @property + def module_json_files_dir(self): + # FIXME: Use INSTALL_DESCRIPTIONSDIR once QTBUG-116983 is done. + result = Path(self.arch_data) / "modules" + return os.fspath(result) + + @property + def metatypes_dir(self): + parent = self.arch_data if self.version_tuple >= (6, 5, 0) else self.libs_dir + return os.fspath(Path(parent) / "metatypes") + + @property + def lib_execs_dir(self): + return self.get_property("QT_INSTALL_LIBEXECS") + + @property + def plugins_dir(self): + return self.get_property("QT_INSTALL_PLUGINS") + + @property + def prefix_dir(self): + return self.get_property("QT_INSTALL_PREFIX") + + @property + def arch_data(self): + return self.get_property("QT_INSTALL_ARCHDATA") + + @property + def imports_dir(self): + return self.get_property("QT_INSTALL_IMPORTS") + + @property + def translations_dir(self): + return self.get_property("QT_INSTALL_TRANSLATIONS") + + @property + def headers_dir(self): + return self.get_property("QT_INSTALL_HEADERS") + + @property + def docs_dir(self): + return self.get_property("QT_INSTALL_DOCS") + + @property + def qml_dir(self): + return self.get_property("QT_INSTALL_QML") + + @property + def macos_min_deployment_target(self): + """ Return value is a macOS version or None. """ + return self.get_property("QMAKE_MACOSX_DEPLOYMENT_TARGET") + + @property + def build_type(self): + """ + Return value is either debug, release, debug_release, or None. + """ + return self.get_property("BUILD_TYPE") + + @property + def src_dir(self): + """ Return path to Qt src dir or None.. """ + return self.get_property("QT_INSTALL_PREFIX/src") + + def get_property(self, prop_name): + if not self._query_dict: + self._get_query_properties() + self._get_other_properties() + if prop_name not in self._query_dict: + return None + return self._query_dict[prop_name] + + def _get_qtpaths_output(self, args_list=None, cwd=None): + if args_list is None: + args_list = [] + assert self._qtpaths_command + cmd = [str(self._qtpaths_command)] + cmd.extend(args_list) + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=False, + cwd=cwd, universal_newlines=True) + output, error = proc.communicate() + proc.wait() + if proc.returncode != 0: + raise RuntimeError(f"Could not run {self._qtpaths_command}: {error}") + return output + + # FIXME PYSIDE7: Remove qmake handling + def _get_qmake_output(self, args_list=None, cwd=None): + if args_list is None: + args_list = [] + assert self._qmake_command + cmd = [self._qmake_command] + cmd.extend(args_list) + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=False, + cwd=cwd) + output = proc.communicate()[0] + proc.wait() + if proc.returncode != 0: + return "" + output = str(output, "ascii").strip() + return output + + def _parse_query_properties(self, process_output): + props = {} + if not process_output: + return props + lines = [s.strip() for s in process_output.splitlines()] + for line in lines: + if line and (":" in line): + key, value = line.split(":", 1) + props[key] = value return props - lines = [s.strip() for s in process_output.splitlines()] - for line in lines: - if line and ':' in line: - key, value = line.split(':', 1) - props[key] = value - return props - - def _get_query_properties(self): - output = self._get_qmake_output(['-query']) - self._query_dict = self._parse_query_properties(output) - - def _parse_qt_build_type(self): - key = 'QT_CONFIG' - if key not in self._mkspecs_dict: - return None - - qt_config = self._mkspecs_dict[key] - if 'debug_and_release' in qt_config: - return 'debug_and_release' - - split = qt_config.split(' ') - if 'release' in split and 'debug' in split: - return 'debug_and_release' - - if 'release' in split: - return 'release' - - if 'debug' in split: - return 'debug' - - return None - - def _get_other_properties(self): - # Get the src property separately, because it is not returned by - # qmake unless explicitly specified. - key = 'QT_INSTALL_PREFIX/src' - result = self._get_qmake_output(['-query', key]) - self._query_dict[key] = result - - # Get mkspecs variables and cache them. - self._get_qmake_mkspecs_variables() - - # Get macOS minimum deployment target. - key = 'QMAKE_MACOSX_DEPLOYMENT_TARGET' - if key in self._mkspecs_dict: - self._query_dict[key] = self._mkspecs_dict[key] - - # Figure out how Qt was built: - # debug mode, release mode, or both. - build_type = self._parse_qt_build_type() - if build_type: - self._query_dict['BUILD_TYPE'] = build_type - - def _init_properties(self): - self._get_query_properties() - self._get_other_properties() - - def _get_qmake_mkspecs_variables(self): - # Create empty temporary qmake project file. - temp_file_name = 'qmake_fake_empty_project.txt' - open(temp_file_name, 'a').close() - - # Query qmake for all of its mkspecs variables. - qmake_output = self._get_qmake_output(['-E', temp_file_name]) - lines = [s.strip() for s in qmake_output.splitlines()] - pattern = re.compile(r"^(.+?)=(.+?)$") - for line in lines: - found = pattern.search(line) - if found: - key = found.group(1).strip() - value = found.group(2).strip() - self._mkspecs_dict[key] = value - - # We need to clean up after qmake, which always creates a - # .qmake.stash file after a -E invocation. - qmake_stash_file = os.path.join(os.getcwd(), ".qmake.stash") - if os.path.exists(qmake_stash_file): - os.remove(qmake_stash_file) - - # Also clean up the temporary empty project file. - if os.path.exists(temp_file_name): - os.remove(temp_file_name) - - version = property(get_version) - bins_dir = property(get_bins_path) - libs_dir = property(get_libs_path) - lib_execs_dir = property(get_libs_execs_path) - plugins_dir = property(get_plugins_path) - prefix_dir = property(get_prefix_path) - qmake_command = property(get_qmake_command) - imports_dir = property(get_imports_path) - translations_dir = property(get_translations_path) - headers_dir = property(get_headers_path) - docs_dir = property(get_docs_path) - qml_dir = property(get_qml_path) - macos_min_deployment_target = property(get_macos_deployment_target) - build_type = property(get_build_type) - src_dir = property(get_src_dir) + + def _get_query_properties(self): + if self._use_cmake: + setup_script_dir = Path.cwd() + sources_dir = setup_script_dir / "sources" + qt_target_info_dir = sources_dir / "shiboken6" / "config.tests" / "target_qt_info" + qt_target_info_dir = os.fspath(qt_target_info_dir) + config_tests_dir = setup_script_dir / "build" / "config.tests" + config_tests_dir = os.fspath(config_tests_dir) + + cmake_cache_args = [] + if self._cmake_toolchain_file: + cmake_cache_args.append(("CMAKE_TOOLCHAIN_FILE", self._cmake_toolchain_file)) + + if self._qt_target_path: + cmake_cache_args.append(("QFP_QT_TARGET_PATH", self._qt_target_path)) + qt_target_info_output = configure_cmake_project( + qt_target_info_dir, + self._cmake_command, + temp_prefix_build_path=config_tests_dir, + cmake_cache_args=cmake_cache_args) + qt_target_info = parse_cmake_project_message_info(qt_target_info_output) + self._query_dict = qt_target_info['qt_info'] + else: + if self._force_qmake: + output = self._get_qmake_output(["-query"]) + else: + output = self._get_qtpaths_output(["--qt-query"]) + self._query_dict = self._parse_query_properties(output) + + def _get_other_properties(self): + # Get the src property separately, because it is not returned by + # qmake unless explicitly specified. + key = "QT_INSTALL_PREFIX/src" + if not self._use_cmake: + if self._force_qmake: + result = self._get_qmake_output(["-query", key]) + else: + result = self._get_qtpaths_output(["--qt-query", key]) + self._query_dict[key] = result + + # Get mkspecs variables and cache them. + # FIXME Python 3.9 self._query_dict |= other_dict + for key, value in self._get_cmake_mkspecs_variables().items(): + self._query_dict[key] = value + + def _get_cmake_mkspecs_variables(self): + setup_script_dir = Path.cwd() + sources_dir = setup_script_dir / "sources" + qt_target_mkspec_dir = sources_dir / "shiboken6" / "config.tests" / "target_qt_mkspec" + qt_target_mkspec_dir = qt_target_mkspec_dir.as_posix() + config_tests_dir = setup_script_dir / "build" / "config.tests" + config_tests_dir = config_tests_dir.as_posix() + + cmake_cache_args = [] + if self._cmake_toolchain_file: + cmake_cache_args.append(("CMAKE_TOOLCHAIN_FILE", self._cmake_toolchain_file)) + if self._qt_target_path: + cmake_cache_args.append(("QFP_QT_TARGET_PATH", self._qt_target_path)) + else: + qt_prefix = Path(self.prefix_dir).as_posix() + cmake_cache_args.append(("CMAKE_PREFIX_PATH", qt_prefix)) + + cmake_cache_args.extend(platform_cmake_options(as_tuple_list=True)) + qt_target_mkspec_output = configure_cmake_project( + qt_target_mkspec_dir, + self._cmake_command, + temp_prefix_build_path=config_tests_dir, + cmake_cache_args=cmake_cache_args) + + qt_target_mkspec_info = parse_cmake_project_message_info(qt_target_mkspec_output) + qt_target_mkspec_info = qt_target_mkspec_info['qt_info'] + + return qt_target_mkspec_info diff --git a/build_scripts/setup_runner.py b/build_scripts/setup_runner.py index 1a7317e4d..5d0466247 100644 --- a/build_scripts/setup_runner.py +++ b/build_scripts/setup_runner.py @@ -1,53 +1,21 @@ -############################################################################# -## -## 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$ -## -############################################################################# +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only -import sys import os +import sys +import tempfile import textwrap +import logging + +from pathlib import Path +from setuptools import setup from build_scripts.config import config -from build_scripts.main import get_package_version, get_setuptools_extension_modules -from build_scripts.main import cmd_class_dict -from build_scripts.options import OPTION +from build_scripts.main import (cmd_class_dict, get_package_version, + get_setuptools_extension_modules) +from build_scripts.options import ADDITIONAL_OPTIONS, OPTION from build_scripts.utils import run_process - -from setuptools import setup +from build_scripts.log import log, LogLevel class SetupRunner(object): @@ -59,38 +27,140 @@ class SetupRunner(object): self.orig_argv = orig_argv self.sub_argv = list(orig_argv) - self.setup_script_dir = os.getcwd() + self.setup_script_dir = Path.cwd() @staticmethod def cmd_line_argument_is_in_args(argument, args): """ Check if command line argument was passed in args. """ - return any(arg for arg in list(args) if "--" + argument in arg) + return any(arg for arg in list(args) if f"--{argument}" in arg) + + @staticmethod + def get_cmd_line_argument_in_args(argument, args): + """ Gets the value of a cmd line argument passed in args. """ + for arg in list(args): + if f"--{argument}" in arg: + prefix = f"--{argument}" + prefix_len = len(prefix) + 1 + return arg[prefix_len:] + return None @staticmethod def remove_cmd_line_argument_in_args(argument, args): """ Remove command line argument from args. """ - return [arg for arg in list(args) if "--" + argument not in arg] + return [arg for arg in list(args) if f"--{argument}" not in arg] @staticmethod def construct_cmd_line_argument(name, value=None): """ Constructs a command line argument given name and value. """ if not value: - return "--{}".format(name) - return "--{}={}".format(name, value) + return f"--{name}" + return f"--{name}={value}" @staticmethod def construct_internal_build_type_cmd_line_argument(internal_build_type): return SetupRunner.construct_cmd_line_argument("internal-build-type", internal_build_type) - def add_setup_internal_invocation(self, build_type, reuse_build=False): - """ Enqueues a script sub-invocation to be executed later. """ + def enqueue_setup_internal_invocation(self, setup_cmd): + self.invocations_list.append(setup_cmd) + + def add_setup_internal_invocation(self, build_type, reuse_build=False, extra_args=None): + setup_cmd = self.new_setup_internal_invocation(build_type, reuse_build, extra_args) + self.enqueue_setup_internal_invocation(setup_cmd) + + def new_setup_internal_invocation(self, build_type, + reuse_build=False, + extra_args=None, + replace_command_with=None): + """ Creates a script sub-invocation to be executed later. """ internal_build_type_arg = self.construct_internal_build_type_cmd_line_argument(build_type) - setup_cmd = [sys.executable] + self.sub_argv + [internal_build_type_arg] + + command_index = 0 + command = self.sub_argv[command_index] + if command == 'setup.py' and len(self.sub_argv) > 1: + command_index = 1 + command = self.sub_argv[command_index] + + # Make a copy + modified_argv = list(self.sub_argv) + + if replace_command_with: + modified_argv[command_index] = replace_command_with + + setup_cmd = [sys.executable] + modified_argv + [internal_build_type_arg] + + if extra_args: + for (name, value) in extra_args: + setup_cmd.append(self.construct_cmd_line_argument(name, value)) # Add --reuse-build option if requested and not already present. - if reuse_build and not self.cmd_line_argument_is_in_args("reuse-build", self.sub_argv): + if (reuse_build and command in ('bdist_wheel', 'build', 'build_base_docs', 'install') + and not self.cmd_line_argument_is_in_args("reuse-build", modified_argv)): setup_cmd.append(self.construct_cmd_line_argument("reuse-build")) - self.invocations_list.append(setup_cmd) + return setup_cmd + + def add_host_tools_setup_internal_invocation(self, initialized_config): + extra_args = [] + extra_host_args = [] + + # When cross-compiling, build the host shiboken generator tool + # only if a path to an existing one was not provided. + if not self.cmd_line_argument_is_in_args("shiboken-host-path", self.sub_argv): + handle, initialized_config.shiboken_host_query_path = tempfile.mkstemp() + os.close(handle) + + # Tell the setup process to create a file with the location + # of the installed host shiboken as its contents. + extra_host_args.append( + ("internal-cmake-install-dir-query-file-path", + initialized_config.shiboken_host_query_path)) + + # Tell the other setup invocations to read that file and use + # the read path as the location of the host shiboken. + extra_args.append( + ("internal-shiboken-host-path-query-file", + initialized_config.shiboken_host_query_path) + ) + + # This is specifying shiboken_module_option_name + # instead of shiboken_generator_option_name, but it will + # actually build the generator. + host_cmd = self.new_setup_internal_invocation( + initialized_config.shiboken_module_option_name, + extra_args=extra_host_args, + replace_command_with="build") + + # To build the host tools, we reuse the initial target + # command line arguments, but we remove some options that + # don't make sense for the host build. + + # Drop the toolchain arg. + host_cmd = self.remove_cmd_line_argument_in_args("cmake-toolchain-file", + host_cmd) + + # Drop the target plat-name arg if there is one. + if self.cmd_line_argument_is_in_args("plat-name", host_cmd): + host_cmd = self.remove_cmd_line_argument_in_args("plat-name", host_cmd) + + # Drop the python-target-path arg if there is one. + if self.cmd_line_argument_is_in_args("python-target-path", host_cmd): + host_cmd = self.remove_cmd_line_argument_in_args("python-target-path", host_cmd) + + # Drop the target build-tests arg if there is one. + if self.cmd_line_argument_is_in_args("build-tests", host_cmd): + host_cmd = self.remove_cmd_line_argument_in_args("build-tests", host_cmd) + + # Make sure to pass the qt host path as the target path + # when doing the host build. And make sure to remove any + # existing qt target path. + if self.cmd_line_argument_is_in_args("qt-host-path", host_cmd): + qt_host_path = self.get_cmd_line_argument_in_args("qt-host-path", host_cmd) + host_cmd = self.remove_cmd_line_argument_in_args("qt-host-path", host_cmd) + host_cmd = self.remove_cmd_line_argument_in_args("qt-target-path", host_cmd) + host_cmd.append(self.construct_cmd_line_argument("qt-target-path", + qt_host_path)) + + self.enqueue_setup_internal_invocation(host_cmd) + return extra_args def run_setup(self): """ @@ -101,6 +171,13 @@ class SetupRunner(object): will run setuptools.setup(). """ + # PYSIDE-1746: We prevent the generation of .pyc/.pyo files during installation. + # These files are generated anyway on their import. + sys.dont_write_bytecode = True + qt_install_path = OPTION["QTPATHS"] + if qt_install_path: + qt_install_path = Path(qt_install_path).parents[1] + # Prepare initial config. config.init_config(build_type=OPTION["BUILD_TYPE"], internal_build_type=OPTION["INTERNAL_BUILD_TYPE"], @@ -108,14 +185,25 @@ class SetupRunner(object): package_version=get_package_version(), ext_modules=get_setuptools_extension_modules(), setup_script_dir=self.setup_script_dir, - quiet=OPTION["QUIET"]) + cmake_toolchain_file=OPTION["CMAKE_TOOLCHAIN_FILE"], + log_level=OPTION["LOG_LEVEL"], + qt_install_path=qt_install_path) + + # Enable logging for both the top-level invocation of setup.py + # as well as for child invocations. We we now use + if OPTION["LOG_LEVEL"] == LogLevel.VERBOSE: + log.setLevel(logging.DEBUG) + elif OPTION["LOG_LEVEL"] == LogLevel.QUIET: + log.setLevel(logging.ERROR) + elif OPTION["LOG_LEVEL"] == LogLevel.INFO: + log.setLevel(logging.INFO) # This is an internal invocation of setup.py, so start actual # build. if config.is_internal_invocation(): if config.internal_build_type not in config.get_allowed_internal_build_values(): - raise RuntimeError("Invalid '{}' option given to --internal-build-type. " - .format(config.internal_build_type)) + raise RuntimeError(f"Invalid '{config.internal_build_type}' option given to " + "--internal-build-type. ") self.run_setuptools_setup() return @@ -123,19 +211,37 @@ class SetupRunner(object): # modules we will build and depending on that, call setup.py # multiple times with different arguments. if config.build_type not in config.get_allowed_top_level_build_values(): - raise RuntimeError("Invalid '{}' option given to --build-type. " - .format(config.build_type)) + raise RuntimeError(f"Invalid '{config.build_type}' option given to --build-type. ") - # Build everything: shiboken2, shiboken2-generator and PySide2. - if config.is_top_level_build_all(): - self.add_setup_internal_invocation(config.shiboken_module_option_name) + # Build everything: shiboken6, shiboken6-generator and PySide6. + help_requested = '--help' in self.sub_argv or '-h' in self.sub_argv + + if help_requested: + self.add_setup_internal_invocation(config.pyside_option_name) + + elif config.is_top_level_build_all(): + extra_args = [] + + # extra_args might contain the location of the built host + # shiboken, which needs to be passed to the other + # target invocations. + if config.is_cross_compile(): + extra_args = self.add_host_tools_setup_internal_invocation(config) + + self.add_setup_internal_invocation( + config.shiboken_module_option_name, + extra_args=extra_args) # Reuse the shiboken build for the generator package instead # of rebuilding it again. - self.add_setup_internal_invocation(config.shiboken_generator_option_name, - reuse_build=True) + # Don't build it in a cross-build though. + if not config.is_cross_compile(): + self.add_setup_internal_invocation( + config.shiboken_generator_option_name, + reuse_build=True) - self.add_setup_internal_invocation(config.pyside_option_name) + self.add_setup_internal_invocation(config.pyside_option_name, + extra_args=extra_args) elif config.is_top_level_build_shiboken_module(): self.add_setup_internal_invocation(config.shiboken_module_option_name) @@ -148,15 +254,21 @@ class SetupRunner(object): for cmd in self.invocations_list: cmd_as_string = " ".join(cmd) - print("\nRunning process: {}\n".format(cmd_as_string)) exit_code = run_process(cmd) if exit_code != 0: - msg = textwrap.dedent(""" - setup.py invocation failed with exit code: {}.\n\n - setup.py invocation was: {} - """).format(exit_code, cmd_as_string) + msg = textwrap.dedent(f""" + setup.py invocation failed with exit code: {exit_code}.\n\n + setup.py invocation was: {cmd_as_string} + """) raise RuntimeError(msg) + if help_requested: + print(ADDITIONAL_OPTIONS) + + # Cleanup temp query file. + if config.shiboken_host_query_path: + os.remove(config.shiboken_host_query_path) + @staticmethod def run_setuptools_setup(): """ diff --git a/build_scripts/utils.py b/build_scripts/utils.py index d1bc780dc..74d9e6fc5 100644 --- a/build_scripts/utils.py +++ b/build_scripts/utils.py @@ -1,61 +1,25 @@ -############################################################################# -## -## 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$ -## -############################################################################# +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only -import sys +import errno +import fnmatch +import glob import os import re -import stat -import errno import shutil +import stat import subprocess -import fnmatch -import itertools -import glob +import sys +import tempfile +import urllib.request as urllib +from collections import defaultdict +from pathlib import Path +from textwrap import dedent, indent -# There is no urllib.request in Python2 -try: - import urllib.request as urllib -except ImportError: - import urllib +from .log import log +from . import (PYSIDE_PYTHON_TOOLS, PYSIDE_LINUX_BIN_TOOLS, PYSIDE_UNIX_LIBEXEC_TOOLS, + PYSIDE_WINDOWS_BIN_TOOLS, PYSIDE_UNIX_BIN_TOOLS, PYSIDE_UNIX_BUNDLED_TOOLS) -import distutils.log as log -from distutils.errors import DistutilsSetupError try: WindowsError @@ -63,6 +27,27 @@ except NameError: WindowsError = None +def which(name): + """ + Like shutil.which, but accepts a string or a PathLike and returns a Path + """ + path = None + try: + if isinstance(name, Path): + name = str(name) + path = shutil.which(name) + if path is None: + raise TypeError("None was returned") + path = Path(path) + except TypeError as e: + log.error(f"{name} was not found in PATH: {e}") + return path + + +def is_64bit(): + return sys.maxsize > 2147483647 + + def filter_match(name, patterns): for pattern in patterns: if pattern is None: @@ -75,281 +60,172 @@ def filter_match(name, patterns): 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)) + if str(path).lower() not in paths: + log.info(f"Inserting path '{path}' to environment") paths.insert(0, path) - os.environ['PATH'] = "{}{}{}".format(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("{}\\{}".format(WINSDK_BASE, sdk_version), - "installationfolder") - # productversion = Reg.get_value("{}\\{}".format(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 sdk_version not 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 - vsbase = VS_BASE % version - try: - productdir = Reg.get_value(r"{}\Setup\VC".format(vsbase), "productdir") - except KeyError: - productdir = None + os.environ['PATH'] = f"{path}{os.pathsep}{os.environ['PATH']}" - # 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.0f}0COMNTOOLS".format(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 get_numpy_location(): + for p in sys.path: + if 'site-' in p: + numpy = Path(p).resolve() / 'numpy' + if numpy.is_dir(): + return os.fspath(numpy / 'core' / 'include') + return None -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.".format(MSVC_VERSION)) - else: - log.info("Found {}".format(vcdir_path)) +def platform_cmake_options(as_tuple_list=False): + result = [] + if sys.platform == 'win32': + # Prevent cmake from auto-detecting clang if it is in path. + if as_tuple_list: + result.append(("CMAKE_C_COMPILER", "cl.exe")) + result.append(("CMAKE_CXX_COMPILER", "cl.exe")) + else: + result.append("-DCMAKE_C_COMPILER=cl.exe") + result.append("-DCMAKE_CXX_COMPILER=cl.exe") + return result - 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, - make_writable_by_owner=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)) +def copyfile(src, dst, force=True, _vars=None, force_copy_symlink=False, + make_writable_by_owner=False): + if isinstance(src, str): + src = Path(src.format(**_vars)) if _vars else Path(src) + if isinstance(dst, str): + dst = Path(dst.format(**_vars)) if _vars else Path(dst) + assert (isinstance(src, Path)) + assert (isinstance(dst, Path)) + + if not src.exists() and not force: + log.info(f"**Skipping copy file\n {src} to\n {dst}\n Source does not exist") return - if not os.path.islink(src) or force_copy_symlink: - log.info("Copying file {} to {}.".format(src, dst)) + if not src.is_symlink() or force_copy_symlink: + if dst.is_file(): + src_stat = os.stat(src) + dst_stat = os.stat(dst) + if (src_stat.st_size == dst_stat.st_size + and src_stat.st_mtime <= dst_stat.st_mtime): + log.info(f"{dst} is up to date.") + return dst + + log.debug(f"Copying file\n {src} to\n {dst}.") shutil.copy2(src, dst) if make_writable_by_owner: make_file_writable_by_owner(dst) + return dst + + # We use 'strict=False' to mimic os.path.realpath in case + # the directory doesn't exist. + link_target_path = src.resolve(strict=False) + if link_target_path.parent == src.parent: + link_target = Path(link_target_path.name) + link_name = Path(src.name) + current_directory = Path.cwd() + try: + target_dir = dst if dst.is_dir() else dst.parent + os.chdir(target_dir) + if link_name.exists(): + if (link_name.is_symlink() + and os.readlink(link_name) == link_target): + log.info(f"Symlink already exists\n {link_name} ->\n {link_target}") + return dst + os.remove(link_name) + log.info(f"Symlinking\n {link_name} ->\n {link_target} in\n {target_dir}") + os.symlink(link_target, link_name) + except OSError: + log.error(f"Error creating symlink\n {link_name} ->\n {link_target}") + finally: + os.chdir(current_directory) else: - link_target_path = os.path.realpath(src) - if os.path.dirname(link_target_path) == os.path.dirname(src): - link_target = os.path.basename(link_target_path) - link_name = os.path.basename(src) - current_directory = os.getcwd() - try: - target_dir = dst if os.path.isdir(dst) else os.path.dirname(dst) - os.chdir(target_dir) - if os.path.exists(link_name): - os.remove(link_name) - log.info("Symlinking {} -> {} in {}.".format(link_name, link_target, target_dir)) - os.symlink(link_target, link_name) - except OSError: - log.error("{} -> {}: Error creating symlink".format(link_name, link_target)) - finally: - os.chdir(current_directory) - else: - log.error("{} -> {}: Can only create symlinks within the same " - "directory".format(src, link_target_path)) + log.error(f"{src} -> {link_target_path}: Can only create symlinks within the same " + "directory") return dst -def makefile(dst, content=None, vars=None): - if vars is not None: +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) + content = content.format(**_vars) + dst = Path(dst.format(**_vars)) - log.info("Making file {}.".format(dst)) + log.info(f"Making file {dst}.") - dstdir = os.path.dirname(dst) - if not os.path.exists(dstdir): - os.makedirs(dstdir) + dstdir = dst.parent + if not dstdir.exists(): + dstdir.mkdir(parents=True) with open(dst, "wt") as f: if content is not None: f.write(content) -def copydir(src, dst, filter=None, ignore=None, force=True, recursive=True, vars=None, +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 isinstance(src, str): + src = Path(src.format(**_vars)) if _vars else Path(src) + if isinstance(dst, str): + dst = Path(dst.format(**_vars)) if _vars else Path(dst) + assert (isinstance(src, Path)) + assert (isinstance(dst, Path)) + + if _vars is not None: + if _filter is not None: + _filter = [i.format(**_vars) for i in _filter] if ignore is not None: - for i in range(len(ignore)): - ignore[i] = ignore[i].format(**vars) + ignore = [i.format(**_vars) for i in ignore] - 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)) + if not src.exists() and not force: + log.info(f"**Skipping copy tree\n {src} to\n {dst}\n Source does not exist. " + f"filter={_filter}. ignore={ignore}.") return [] - log.info("Copying tree {} to {}. filter={}. ignore={}.".format(src, dst, filter, ignore)) + log.debug(f"Copying tree\n {src} to\n {dst}. filter={_filter}. ignore={ignore}.") names = os.listdir(src) results = [] - errors = [] + copy_errors = [] for name in names: - srcname = os.path.join(src, name) - dstname = os.path.join(dst, name) + srcname = src / name + dstname = dst / name try: - if os.path.isdir(srcname): + if srcname.is_dir(): 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, + 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 (_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)) + if not dst.is_dir(): + dst.mkdir(parents=True) + 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]) + copy_errors.extend(err.args[0]) except EnvironmentError as why: - errors.append((srcname, dstname, str(why))) + copy_errors.append((srcname, dstname, str(why))) try: - if os.path.exists(dst): - shutil.copystat(src, dst) + if dst.exists(): + shutil.copystat(str(src), str(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) + copy_errors.extend((src, dst, str(why))) + if copy_errors: + raise EnvironmentError(copy_errors) return results @@ -358,9 +234,10 @@ def make_file_writable_by_owner(path): os.chmod(path, current_permissions | stat.S_IWUSR) -def rmtree(dirname, ignore=False): +def remove_tree(dirname, ignore=False): def handle_remove_readonly(func, path, exc): - excvalue = exc[1] + # exc returns like 'sys.exc_info()': type, value, traceback + _, excvalue, _ = exc 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) @@ -372,13 +249,12 @@ def rmtree(dirname, ignore=False): def run_process_output(args, initial_env=None): if initial_env is None: initial_env = os.environ - std_out = subprocess.Popen(args, env=initial_env, universal_newlines=1, - stdout=subprocess.PIPE).stdout result = [] - for raw_line in std_out.readlines(): - line = raw_line if sys.version_info >= (3,) else raw_line.decode('utf-8') - result.append(line.rstrip()) - std_out.close() + with subprocess.Popen(args, env=initial_env, universal_newlines=1, + stdout=subprocess.PIPE) as p: + for raw_line in p.stdout.readlines(): + result.append(raw_line.rstrip()) + p.stdout.close() return result @@ -387,8 +263,8 @@ def run_process(args, initial_env=None): Run process until completion and return the process exit code. No output is captured. """ - command = " ".join([(" " in x and '"{}"'.format(x) or x) for x in args]) - log.info("Running process in directory {}: command {}".format(os.getcwd(), command)) + command = " ".join([(" " in x and f'"{x}"' or x) for x in args]) + log.debug(f"In directory {Path.cwd()}:\n\tRunning command: {command}") if initial_env is None: initial_env = os.environ @@ -400,83 +276,10 @@ def run_process(args, initial_env=None): return exit_code -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 - lines = map(lambda s: s.decode(), 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 - # parse key/values into pairs - pairs = map(lambda l: l.rstrip().split('=', 1), 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 + Run command `cmd`, return stdout, or (stdout, stderr, + return_code) if `ret_err` is True. Parameters ---------- @@ -500,23 +303,20 @@ def back_tick(cmd, ret_err=False): 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("{} process did not terminate".format(cmd)) - if retcode != 0 and not ret_err: - raise RuntimeError("{} process returned code {}\n*** {}".format( - (cmd, retcode, err))) - out = out.strip() + with subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, shell=True) as proc: + out_bytes, err_bytes = proc.communicate() + out = out_bytes.decode().strip() + err = err_bytes.decode().strip() + retcode = proc.returncode + if retcode is None and not ret_err: + proc.terminate() + raise RuntimeError(f"{cmd} process did not terminate") + if retcode != 0 and not ret_err: + raise RuntimeError(f"{cmd} process returned code {retcode}\n*** {err}") if not ret_err: return out - return out, err.strip(), retcode + return out, err, retcode MACOS_OUTNAME_RE = re.compile(r'\(compatibility version [\d.]+, current version [\d.]+\)') @@ -536,7 +336,7 @@ def macos_get_install_names(libpath): install_names : list of str install names in library `libpath` """ - out = back_tick("otool -L {}".format(libpath)) + out = back_tick(f"otool -L {libpath}") libs = [line for line in out.split('\n')][1:] return [MACOS_OUTNAME_RE.sub('', lib).strip() for lib in libs] @@ -561,7 +361,7 @@ def macos_get_rpaths(libpath): ----- See ``man dyld`` for more information on rpaths in libraries """ - lines = back_tick('otool -l {}'.format(libpath)).split('\n') + lines = back_tick(f"otool -l {libpath}").split('\n') ctr = 0 rpaths = [] while ctr < len(lines): @@ -573,14 +373,17 @@ def macos_get_rpaths(libpath): rpath_line = lines[ctr + 2].strip() match = MACOS_RPATH_RE.match(rpath_line) if match is None: - raise RuntimeError("Unexpected path line: {}".format(rpath_line)) + raise RuntimeError(f"Unexpected path line: {rpath_line}") rpaths.append(match.groups()[0]) ctr += 3 return rpaths def macos_add_rpath(rpath, library_path): - back_tick("install_name_tool -add_rpath {} {}".format(rpath, library_path)) + try: + back_tick(f"install_name_tool -add_rpath {rpath} {library_path}") + except RuntimeError as e: + print(f"Exception {type(e).__name__}: {e}") def macos_fix_rpaths_for_library(library_path, qt_lib_dir): @@ -627,8 +430,8 @@ def macos_fix_rpaths_for_library(library_path, qt_lib_dir): 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=[]): +def macos_add_qt_rpath(library_path, qt_lib_dir, existing_rpath_commands=None, + library_dependencies=None): """ Adds an rpath load command to the Qt lib directory if necessary @@ -636,6 +439,12 @@ def macos_add_qt_rpath(library_path, qt_lib_dir, existing_rpath_commands=[], and adds an rpath load command that points to the Qt lib directory (qt_lib_dir). """ + if existing_rpath_commands is None: + existing_rpath_commands = [] + + if library_dependencies is None: + library_dependencies = [] + if not existing_rpath_commands: existing_rpath_commands = macos_get_rpaths(library_path) @@ -664,31 +473,19 @@ def find_glob_in_path(pattern): pattern += '.exe' for path in os.environ.get('PATH', '').split(os.pathsep): - for match in glob.glob(os.path.join(path, pattern)): + for match in glob.glob(str(Path(path) / pattern)): result.append(match) return result -# Locate the most recent version of llvm_config in the path. -def find_llvm_config(): - version_re = re.compile(r'(\d+)\.(\d+)\.(\d+)') - result = None - last_version_string = '000000' - for llvm_config in find_glob_in_path('llvm-config*'): - try: - output = run_process_output([llvm_config, '--version']) - if output: - match = version_re.match(output[0]) - if match: - version_string = "{:02d}{:02d}{:02d}".format(int(match.group(1)), - int(match.group(2)), - int(match.group(3))) - if (version_string > last_version_string): - result = llvm_config - last_version_string = version_string - except OSError: - pass - return result +# Expand the __ARCH_ place holder in the CLANG environment variables +def expand_clang_variables(target_arch): + for var in 'LLVM_INSTALL_DIR', 'CLANG_INSTALL_DIR': + value = os.environ.get(var) + if value and '_ARCH_' in value: + value = value.replace('_ARCH_', target_arch) + os.environ[var] = value + print(f"{var} = {value}") # Add Clang to path for Windows for the shiboken ApiExtractor tests. @@ -700,18 +497,8 @@ def detect_clang(): source = 'CLANG_INSTALL_DIR' clang_dir = os.environ.get(source, None) if not clang_dir: - source = find_llvm_config() - try: - if source is not None: - output = run_process_output([source, '--prefix']) - if output: - clang_dir = output[0] - except OSError: - pass - if clang_dir: - arch = '64' if sys.maxsize > 2 ** 31 - 1 else '32' - clang_dir = clang_dir.replace('_ARCH_', arch) - return (clang_dir, source) + raise OSError("clang not found") + return (Path(clang_dir), source) _7z_binary = None @@ -723,29 +510,29 @@ def download_and_extract_7z(fileurl, target): localfile = None for i in range(1, 10): try: - print("Downloading fileUrl {}, attempt #{}".format(fileurl, i)) + log.info(f"Downloading fileUrl {fileurl}, attempt #{i}") localfile, info = urllib.urlretrieve(fileurl) break - except: + except urllib.URLError: pass if not localfile: - print("Error downloading {} : {}".format(fileurl, info)) - raise RuntimeError(' Error downloading {}'.format(fileurl)) + log.error(f"Error downloading {fileurl} : {info}") + raise RuntimeError(f" Error downloading {fileurl}") try: global _7z_binary - outputDir = "-o{}".format(target) + outputDir = f"-o{target}" if not _7z_binary: - if sys.platform == 'win32': - candidate = 'c:\\Program Files\\7-Zip\\7z.exe' - if os.path.exists(candidate): + if sys.platform == "win32": + candidate = Path("c:\\Program Files\\7-Zip\\7z.exe") + if candidate.exists(): _7z_binary = candidate if not _7z_binary: _7z_binary = '7z' - print("calling {} x {} {}".format(_7z_binary, localfile, outputDir)) + log.info(f"calling {_7z_binary} x {localfile} {outputDir}") subprocess.call([_7z_binary, "x", "-y", localfile, outputDir]) - except: - raise RuntimeError(' Error extracting {}'.format(localfile)) + except (subprocess.CalledProcessError, OSError): + raise RuntimeError(f"Error extracting {localfile}") def split_and_strip(sinput): @@ -803,18 +590,49 @@ def ldd_get_paths_for_dependencies(dependencies_regex, executable_path=None, dep return paths -def ldd(executable_path): +def _ldd_ldd(executable_path): + """Helper for ldd(): + Returns ldd output of shared library dependencies for given + `executable_path`. + + Parameters + ---------- + executable_path : str + path to executable or shared library. + + Returns + ------- + output : str + the raw output retrieved from the dynamic linker. """ + + output = '' + error = '' + try: + output_lines = run_process_output(['ldd', executable_path]) + output = '\n'.join(output_lines) + except Exception as e: + error = str(e) + if not output: + message = (f"ldd failed to query for dependent shared libraries of {executable_path}: " + f"{error}") + raise RuntimeError(message) + return output + + +def _ldd_ldso(executable_path): + """ + Helper for ldd(): 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. + This is a partial port of /usr/bin/ldd from bash to Python for + systems that do not have ldd. 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. + Only works on Linux. This is because ldd (on Ubuntu) is shipped in the libc-bin package that, which might have a minuscule percentage of not being installed. @@ -837,12 +655,13 @@ def ldd(executable_path): # Choose appropriate runtime dynamic linker. for rtld in rtld_list: - if os.path.isfile(rtld) and os.access(rtld, os.X_OK): + rtld = Path(rtld) + if rtld.is_file() 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) + (_, _, code) = back_tick(f"{rtld} --verify {executable_path}", True) # Codes 0 and 2 mean given executable_path can be # understood by ld.so. if code in [0, 2]: @@ -854,25 +673,51 @@ def ldd(executable_path): # Query for shared library dependencies. rtld_env = "LD_TRACE_LOADED_OBJECTS=1" - rtld_cmd = "{} {} {}".format(rtld_env, chosen_rtld, executable_path) + rtld_cmd = f"{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)) + f"libraries of {executable_path}") + + +def ldd(executable_path): + """ + Returns ldd output of shared library dependencies for given `executable_path`, + using either ldd or ld.so depending on availability. + + Parameters + ---------- + executable_path : str + path to executable or shared library. + + Returns + ------- + output : str + the raw output retrieved from the dynamic linker. + """ + result = '' + try: + result = _ldd_ldd(executable_path) + except RuntimeError as e: + message = f"ldd: Falling back to ld.so ({str(e)})" + log.warning(message) + if not result: + result = _ldd_ldso(executable_path) + return result 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) + final_pattern = Path(path) / pattern + maybe_files = glob.glob(str(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.?") + maybe_file = find_files_using_glob(lib_dir, "libQt6Core.so.?") if len(maybe_file) == 1: return maybe_file[0] return None @@ -883,16 +728,18 @@ def find_qt_core_library_glob(lib_dir): # ldd for the specified platforms. # This has less priority because ICU libs are not used in the default # Qt configuration build. +# Note: Uses ldd to query shared library dependencies and thus does not +# work for cross builds. def copy_icu_libs(patchelf, 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) + qt_core_library_path = 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)) + if not qt_core_library_path or not qt_core_library_path.exists(): + raise RuntimeError(f"QtCore library does not exist at path: {qt_core_library_path}. " + "Failed to copy ICU libraries.") dependencies = ldd_get_dependencies(qt_core_library_path) @@ -909,14 +756,15 @@ def copy_icu_libs(patchelf, destination_lib_dir): 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.') + log.debug('Copying the detected ICU libraries required by QtCore.') - if not os.path.exists(destination_lib_dir): - os.makedirs(destination_lib_dir) + destination_lib_dir = Path(destination_lib_dir) + if not destination_lib_dir.exists(): + destination_lib_dir.mkdir(parents=True) for path in paths: - basename = os.path.basename(path) - destination = os.path.join(destination_lib_dir, basename) + basename = Path(path).name + destination = 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. @@ -925,20 +773,15 @@ def copy_icu_libs(patchelf, destination_lib_dir): # 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 = linux_get_rpaths(qt_core_library_path) - if not rpaths or not rpaths_has_origin(rpaths): - log.info('Patching QtCore library to contain $ORIGIN rpath.') - rpaths.insert(0, '$ORIGIN') - new_rpaths_string = ":".join(rpaths) - linux_set_rpaths(patchelf, qt_core_library_path, new_rpaths_string) + linux_prepend_rpath(patchelf, qt_core_library_path, '$ORIGIN') def linux_run_read_elf(executable_path): - cmd = "readelf -d {}".format(executable_path) + cmd = f"readelf -d {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)) + raise RuntimeError(f"Running `readelf -d {executable_path}` failed with error " + f"output:\n {err}. ") lines = split_and_strip(out) return lines @@ -946,10 +789,24 @@ def linux_run_read_elf(executable_path): def linux_set_rpaths(patchelf, executable_path, rpath_string): """ Patches the `executable_path` with a new rpath string. """ - cmd = [patchelf, '--set-rpath', rpath_string, executable_path] + cmd = [str(patchelf), '--set-rpath', str(rpath_string), str(executable_path)] if run_process(cmd) != 0: - raise RuntimeError("Error patching rpath in {}".format(executable_path)) + raise RuntimeError(f"Error patching rpath in {executable_path}") + + +def linux_prepend_rpath(patchelf, executable_path, new_path): + """ Prepends a path to the rpaths of the executable unless it has ORIGIN. """ + rpaths = linux_get_rpaths(executable_path) + if not rpaths or not rpaths_has_origin(rpaths): + rpaths.insert(0, new_path) + new_rpaths_string = ":".join(rpaths) + linux_set_rpaths(patchelf, executable_path, new_rpaths_string) + + +def linux_patch_executable(patchelf, executable_path): + """ Patch an executable to run with the Qt libraries. """ + linux_prepend_rpath(patchelf, executable_path, '$ORIGIN/../lib') def linux_get_dependent_libraries(executable_path): @@ -1036,6 +893,7 @@ def linux_fix_rpaths_for_library(patchelf, executable_path, qt_rpath, override=F existing_rpaths = linux_get_rpaths(executable_path) rpaths.extend(existing_rpaths) + qt_rpath = str(qt_rpath) if linux_needs_qt_rpath(executable_path) and qt_rpath not in existing_rpaths: rpaths.append(qt_rpath) @@ -1069,86 +927,237 @@ def get_python_dict(python_script_path): 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)) + print(f"get_python_dict: Couldn't get dict from python " + f"file: {python_script_path}. {e}") raise -def install_pip_package_from_url_specifier(env_pip, url, upgrade=True): - args = [env_pip, "install", url] - if upgrade: - args.append("--upgrade") - args.append(url) - run_instruction(args, "Failed to install {}".format(url)) - - -def install_pip_dependencies(env_pip, packages, upgrade=True): - for p in packages: - args = [env_pip, "install"] - if upgrade: - args.append("--upgrade") - args.append(p) - run_instruction(args, "Failed to install {}".format(p)) - - def get_qtci_virtualEnv(python_ver, host, hostArch, targetArch): _pExe = "python" - _env = "env{}".format(str(python_ver)) - env_python = _env + "/bin/python" - env_pip = _env + "/bin/pip" + _env = f"{os.environ.get('PYSIDE_VIRTUALENV') or 'env'+python_ver}" + env_python = f"{_env}/bin/python" + env_pip = f"{_env}/bin/pip" if host == "Windows": - print("New virtualenv to build {} in {} host".format(targetArch, hostArch)) + log.info("New virtualenv to build {targetArch} in {hostArch} host") _pExe = "python.exe" # With windows we are creating building 32-bit target in 64-bit host if hostArch == "X86_64" and targetArch == "X86": - if python_ver == "3": - _pExe = os.path.join(os.getenv("PYTHON3_32_PATH"), "python.exe") + if python_ver.startswith("3"): + var = f"PYTHON{python_ver}-32_PATH" + log.info(f"Try to find python from {var} env variable") + _path = Path(os.getenv(var, "")) + _pExe = _path / "python.exe" + if not _pExe.is_file(): + log.warning(f"Can't find python.exe from {_pExe}, using default python3") + _pExe = Path(os.getenv("PYTHON3_32_PATH")) / "python.exe" else: - _pExe = os.path.join(os.getenv("PYTHON2_32_PATH"), "python.exe") + _pExe = Path(os.getenv("PYTHON2_32_PATH")) / "python.exe" else: - if python_ver == "3": - _pExe = os.path.join(os.getenv("PYTHON3_PATH"), "python.exe") - env_python = _env + "\\Scripts\\python.exe" - env_pip = _env + "\\Scripts\\pip.exe" + if python_ver.startswith("3"): + var = f"PYTHON{python_ver}-64_PATH" + log.info(f"Try to find python from {var} env variable") + _path = Path(os.getenv(var, "")) + _pExe = _path / "python.exe" + if not _pExe.is_file(): + log.warning(f"Can't find python.exe from {_pExe}, using default python3") + _pExe = Path(os.getenv("PYTHON3_PATH")) / "python.exe" + env_python = f"{_env}\\Scripts\\python.exe" + env_pip = f"{_env}\\Scripts\\pip.exe" else: - if python_ver == "3": + _pExe = f"python{python_ver}" + try: + run_instruction([_pExe, "--version"], f"Failed to guess python version {_pExe}") + except Exception as e: + print(f"Exception {type(e).__name__}: {e}") _pExe = "python3" - return(_pExe, _env, env_pip, env_python) + return (_pExe, _env, env_pip, env_python) def run_instruction(instruction, error, initial_env=None): if initial_env is None: initial_env = os.environ - print("Running Coin instruction: {}".format(' '.join(str(e) for e in instruction))) + log.info(f"Running Coin instruction: {' '.join(str(e) for e in instruction)}") result = subprocess.call(instruction, env=initial_env) if result != 0: - print("ERROR : {}".format(error)) + log.error(f"ERROR : {error}") exit(result) -def acceptCITestConfiguration(hostOS, hostOSVer, targetArch, compiler): - # Disable unsupported CI configs for now - # NOTE: String must match with QT CI's storagestruct thrift - if (hostOSVer in ["WinRT_10", "WebAssembly", "Ubuntu_18_04", "Android_ANY"] - or hostOSVer.startswith("SLES_")): - print("Disabled {} from Coin configuration".format(hostOSVer)) - return False - # With 5.11 CI will create two sets of release binaries, - # one with msvc 2015 and one with msvc 2017 - # we shouldn't release the 2015 version. - # BUT, 32 bit build is done only on msvc 2015... - if compiler in ["MSVC2015"] and targetArch in ["X86_64"]: - print("Disabled {} to {} from Coin configuration".format(compiler, targetArch)) - return False - return True +def get_ci_qtpaths_path(ci_install_dir, ci_host_os): + qtpaths_path = f"--qtpaths={ci_install_dir}" + if ci_host_os == "MacOS": + return f"{qtpaths_path}/bin/qtpaths" + elif ci_host_os == "Windows": + return f"{qtpaths_path}\\bin\\qtpaths.exe" + else: + return f"{qtpaths_path}/bin/qtpaths" def get_ci_qmake_path(ci_install_dir, ci_host_os): - qmake_path = "--qmake={}".format(ci_install_dir) + qmake_path = f"--qmake={ci_install_dir}" if ci_host_os == "MacOS": - return qmake_path + "/bin/qmake" + return f"{qmake_path}/bin/qmake" elif ci_host_os == "Windows": - return qmake_path + "\\bin\\qmake.exe" + return f"{qmake_path}\\bin\\qmake.exe" else: - return qmake_path + "/bin/qmake" + return f"{qmake_path}/bin/qmake" + + +def parse_cmake_conf_assignments_by_key(source_dir): + """ + Parses a .cmake.conf file that contains set(foo "bar") assignments + and returns a dict with those assignments transformed to keys and + values. + """ + + contents = (Path(source_dir) / ".cmake.conf").read_text() + matches = re.findall(r'set\((.+?) "(.*?)"\)', contents) + d = {key: value for key, value in matches} + return d + + +def _configure_failure_message(project_path, cmd, return_code, output, error, env): + """Format a verbose message about configure_cmake_project() failures.""" + cmd_string = ' '.join(cmd) + error_text = indent(error.strip(), " ") + output_text = indent(output.strip(), " ") + result = dedent(f""" + Failed to configure CMake project: '{project_path}' + Configure args were: + {cmd_string} + Return code: {return_code} + """) + + first = True + for k, v in env.items(): + if k.startswith("CMAKE"): + if first: + result += "Environment:\n" + first = False + result += f" {k}={v}\n" + + result += f"\nwith error:\n{error_text}\n" + + CMAKE_CMAKEOUTPUT_LOG_PATTERN = r'See also "([^"]+CMakeOutput\.log)"\.' + cmakeoutput_log_match = re.search(CMAKE_CMAKEOUTPUT_LOG_PATTERN, output) + if cmakeoutput_log_match: + cmakeoutput_log = Path(cmakeoutput_log_match.group(1)) + if cmakeoutput_log.is_file(): + log = indent(cmakeoutput_log.read_text().strip(), " ") + result += f"CMakeOutput.log:\n{log}\n" + + result += f"Output:\n{output_text}\n" + return result + + +def configure_cmake_project(project_path, + cmake_path, + build_path=None, + temp_prefix_build_path=None, + cmake_args=None, + cmake_cache_args=None, + ): + clean_temp_dir = False + if not build_path: + # Ensure parent dir exists. + if temp_prefix_build_path: + os.makedirs(temp_prefix_build_path, exist_ok=True) + + project_name = Path(project_path).name + build_path = tempfile.mkdtemp(prefix=f"{project_name}_", dir=temp_prefix_build_path) + + if 'QFP_SETUP_KEEP_TEMP_FILES' not in os.environ: + clean_temp_dir = True + + cmd = [cmake_path, '-G', 'Ninja', '-S', project_path, '-B', build_path] + + if cmake_args: + cmd.extend(cmake_args) + + for arg, value in cmake_cache_args: + cmd.extend([f'-D{arg}={value}']) + + cmd = [str(i) for i in cmd] + + proc = subprocess.run(cmd, shell=False, cwd=build_path, + capture_output=True, universal_newlines=True) + return_code = proc.returncode + output = proc.stdout + error = proc.stderr + + if return_code != 0: + m = _configure_failure_message(project_path, cmd, return_code, + output, error, os.environ) + raise RuntimeError(m) + + if clean_temp_dir: + remove_tree(build_path) + + return output + + +def parse_cmake_project_message_info(output): + # Parse the output for anything prefixed + # '-- qfp:<category>:<key>: <value>' as created by the message() + # calls in a given CMake project and store it in a python dict. + result = defaultdict(lambda: defaultdict(str)) + pattern = re.compile(r"^-- qfp:(.+?):(.+?):(.*)$") + for line in output.splitlines(): + found = pattern.search(line) + if found: + category = found.group(1).strip() + key = found.group(2).strip() + value = found.group(3).strip() + result[category][key] = str(value) + return result + + +def available_pyside_tools(qt_tools_path: Path, package_for_wheels: bool = False): + pyside_tools = PYSIDE_PYTHON_TOOLS.copy() + + if package_for_wheels: + # Qt wrappers in build/{python_env_name}/package_for_wheels/PySide6 + bin_path = qt_tools_path + else: + bin_path = qt_tools_path / "bin" + + def tool_exist(tool_path: Path): + if tool_path.exists(): + return True + else: + log.warning(f"{tool_path} not found. pyside-{tool_path.name} not included.") + return False + + if sys.platform == 'win32': + pyside_tools.extend([tool for tool in PYSIDE_WINDOWS_BIN_TOOLS + if tool_exist(bin_path / f"{tool}.exe")]) + else: + lib_exec_path = qt_tools_path / "Qt" / "libexec" if package_for_wheels \ + else qt_tools_path / "libexec" + pyside_tools.extend([tool for tool in PYSIDE_UNIX_LIBEXEC_TOOLS + if tool_exist(lib_exec_path / tool)]) + if sys.platform == 'darwin': + def name_to_path(name): + return f"{name.capitalize()}.app/Contents/MacOS/{name.capitalize()}" + + pyside_tools.extend([tool for tool in PYSIDE_UNIX_BIN_TOOLS + if tool_exist(bin_path / tool)]) + pyside_tools.extend([tool for tool in PYSIDE_UNIX_BUNDLED_TOOLS + if tool_exist(bin_path / name_to_path(tool))]) + else: + pyside_tools.extend([tool for tool in PYSIDE_LINUX_BIN_TOOLS + if tool_exist(bin_path / tool)]) + + return pyside_tools + + +def copy_qt_metatypes(destination_qt_dir, _vars): + """Copy the Qt metatypes files which changed location in 6.5""" + # <qt>/[lib]?/metatypes/* -> <setup>/{st_package_name}/Qt/[lib]?/metatypes + qt_meta_types_dir = "{qt_metatypes_dir}".format(**_vars) + qt_prefix_dir = "{qt_prefix_dir}".format(**_vars) + rel_meta_data_dir = os.fspath(Path(qt_meta_types_dir).relative_to(qt_prefix_dir)) + copydir(qt_meta_types_dir, destination_qt_dir / rel_meta_data_dir, + _filter=["*.json"], + recursive=False, _vars=_vars, force_copy_symlinks=True) diff --git a/build_scripts/wheel_files.py b/build_scripts/wheel_files.py new file mode 100644 index 000000000..d34ada113 --- /dev/null +++ b/build_scripts/wheel_files.py @@ -0,0 +1,1036 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + + +import json +import sys +from dataclasses import Field, dataclass, field +from typing import Dict, List + + +_pyside_package_path = None +_module_json_file_path = None + + +def set_pyside_package_path(p): + global _pyside_package_path, _module_json_file_path + _pyside_package_path = p + qt_path = p + if sys.platform != "win32": + qt_path /= "Qt" + _module_json_file_path = qt_path / "modules" + + +def get_module_json_data(module): + """Read the JSON module data.""" + json_path = _module_json_file_path / f"{module}.json" + json_data = None + if not json_path.is_file(): # Wayland is Linux only + print(f"Skipping {json_path}", file=sys.stderr) + return None + with json_path.open(encoding="utf-8") as json_file: + json_data = json.load(json_file) + return json_data + + +def get_module_plugins(json_data): + """Return the plugins from the JSON module data.""" + if json_data: + plugins = json_data.get("plugin_types") + if plugins: + return plugins + return [] + + +# This dataclass is in charge of holding the file information +# that each Qt module needs to have to be packaged in a wheel +@dataclass +class ModuleData: + name: str + ext: str = "" + # Libraries not related to Qt modules + lib: List[str] = field(default_factory=list) + # Libraries related to Qt modules + qtlib: List[str] = field(default_factory=list) + # Files from the Qt/qml directory + qml: List[str] = field(default_factory=list) + pyi: List[str] = field(default_factory=list) + translations: List[str] = field(default_factory=list) + typesystems: List[str] = field(default_factory=list) + include: List[str] = field(default_factory=list) + glue: List[str] = field(default_factory=list) + metatypes: List[str] = field(default_factory=list) + plugins: List[str] = field(default_factory=list) + + # For special cases when a file/directory doesn't fall into + # the previous categories. + extra_dirs: List[str] = field(default_factory=list) + extra_files: List[str] = field(default_factory=list) + + # Once the object is created, this method will be executed + # and automatically will initialize some of the files that are + # common for each module. + # Note: The goal of this list is to be used for a MANIFEST.in + # meaning that in case a file gets added and it doesn't + # exist, the wheel creation process will only throw a + # warning, but it will not interrupt the packaging process. + def __post_init__(self) -> None: + if not self.ext: + self.ext = self.get_extension_from_platform(sys.platform) + _lo = self.name.lower() + + self.lib.append(f"Qt{self.name}") + self.qtlib.append(f"libQt6{self.name}") + if not len(self.qml): + self.qml.append(f"Qt{self.name}") + self.pyi.append(f"Qt{self.name}.pyi") + self.typesystems.append(f"typesystem_{_lo}.xml") + self.include.append(f"Qt{self.name}/*.h") + self.glue.append(f"qt{_lo}.cpp") + if not len(self.metatypes): + self.metatypes.append(f"qt6{_lo}_relwithdebinfo_metatypes.json") + + # The PySide6 directory that gets packaged by the build_scripts + # 'prepare_packages()' has a certain structure that depends on + # the platform. Because that directory is the base for the wheel + # packaging to work, we use the relative paths that are included + # on each file. + # Note: The MANIFEST.in file doesn't need to have '\' or other + # separator, and respect the '/' even on Windows. + def adjusts_paths_and_extensions(self) -> None: + if sys.platform == "win32": + self.lib = [f"{i}.*{self.ext}".replace("lib", "") for i in self.lib] + self.qtlib = [f"{i}.*dll".replace("lib", "") for i in self.qtlib] + self.qml = [f"qml/{i}" for i in self.qml] + self.translations = [f"translations/{i}" for i in self.translations] + self.metatypes = [ + f"metatypes/{i}".replace("_relwithdebinfo", "") for i in self.metatypes + ] + self.plugins = [f"plugins/{i}" for i in self.plugins] + else: + if sys.platform == "darwin": + self.qtlib = [f"Qt/lib/{i.replace('libQt6', 'Qt')}.framework" for i in self.qtlib] + self.lib = [self.macos_pyside_wrappers_lib(i) for i in self.lib] + else: + self.lib = [f"{i}.*{self.ext}*" for i in self.lib] + self.qtlib = [f"Qt/lib/{i}.*{self.ext}*" for i in self.qtlib] + self.qml = [f"Qt/qml/{i}" for i in self.qml] + self.translations = [f"Qt/translations/{i}" for i in self.translations] + self.metatypes = [f"Qt/metatypes/{i}" for i in self.metatypes] + self.plugins = [f"Qt/plugins/{i}" for i in self.plugins] + + self.typesystems = [f"typesystems/{i}" for i in self.typesystems] + self.include = [f"include/{i}" for i in self.include] + self.glue = [f"glue/{i}" for i in self.glue] + + def macos_pyside_wrappers_lib(self, s): + if s.startswith("Qt"): + return f"{s}.*so*" + else: + return f"{s}.*{self.ext}*" + + @classmethod + def get_fields(cls) -> Dict[str, Field]: + return cls.__dataclass_fields__ + + @staticmethod + def get_extension_from_platform(platform: str) -> str: + if platform == "linux": + return "so" + elif platform == "darwin": + return "dylib" + elif platform == "win32": + return "pyd" + else: + print(f"Platform '{platform}' not supported. Exiting") + sys.exit(-1) + + +# Wheels auxiliary functions to return the ModuleData objects +# for each module that will be included in the wheel. + +# PySide wheel +def wheel_files_pyside_essentials() -> List[ModuleData]: + files = [ + module_QtCore(), + module_QtGui(), + module_QtWidgets(), + module_QtHelp(), + module_QtNetwork(), + module_QtConcurrent(), + module_QtDBus(), + module_QtDesigner(), + module_QtOpenGL(), + module_QtOpenGLWidgets(), + module_QtPrintSupport(), + module_QtQml(), + module_QtQuick(), + module_QtQuickControls2(), + module_QtQuickTest(), + module_QtQuickWidgets(), + module_QtXml(), + module_QtTest(), + module_QtSql(), + module_QtSvg(), + module_QtSvgWidgets(), + module_QtUiTools(), + module_QtExampleIcons(), + # Only for plugins + module_QtWayland(), + # there are no bindings for these modules, but their binaries are + # required for qmlls + module_QtLanguageServer(), + module_QtJsonRpc(), + ] + return files + + +# PySide Addons wheel +def wheel_files_pyside_addons() -> List[ModuleData]: + files = [ + module_Qt3DAnimation(), + module_Qt3DCore(), + module_Qt3DExtras(), + module_Qt3DInput(), + module_Qt3DLogic(), + module_Qt3DRender(), + module_QtAxContainer(), + module_QtBluetooth(), + module_QtCharts(), + module_QtDataVisualization(), + module_QtGraphs(), + module_QtMultimedia(), + module_QtMultimediaWidgets(), + module_QtNetworkAuth(), + module_QtNfc(), + module_QtPdf(), + module_QtPdfWidgets(), + module_QtPositioning(), + module_QtQuick3D(), + module_QtRemoteObjects(), + module_QtScxml(), + module_QtSensors(), + module_QtSerialPort(), + module_QtSerialBus(), + module_QtSpatialAudio(), + module_QtStateMachine(), + module_QtTextToSpeech(), + module_QtVirtualKeyboard(), + module_QtWebChannel(), + module_QtWebEngineCore(), + module_QtWebEngineQuick(), + module_QtWebEngineWidgets(), + module_QtWebSockets(), + module_QtHttpServer(), + module_QtLocation(), + module_QtAsyncio(), + ] + return files + + +# Functions that hold the information of all the files that needs +# to be included for the module to work, including Qt libraries, +# typesystems, glue, etc. +def module_QtCore() -> ModuleData: + # QtCore + data = ModuleData("Core") + + _typesystems = [ + "common.xml", + "core_common.xml", + "typesystem_core_common.xml", + "typesystem_core_win.xml" + ] + + data.typesystems.extend(_typesystems) + data.include.append("*.h") + if sys.platform == "win32": + data.qtlib.append("pyside6.*") + data.extra_files.append("qt.conf") + data.extra_files.append("rcc.exe") + data.extra_files.append("qtdiag.exe") + data.extra_files.append("pyside6.*.lib") + data.extra_files.append("resources/icudtl.dat") + from build_scripts.platforms.windows_desktop import msvc_redist + data.extra_files.extend(msvc_redist) + else: + data.lib.append("libpyside6.*") + data.extra_files.append("Qt/libexec/rcc") + data.extra_files.append("Qt/libexec/qt.conf") + + # *.py + data.extra_dirs.append("support") + + # pyside-tools with python backend + # Including the 'scripts' folder would include all the tools into the + # PySide6_Essentials wheel. The moment when we add a tool that has a + # dependency on a module in PySide6_AddOns, then we should split out + # the following line into individual subfolder and files, to better + # control which tool goes into which wheel + data.extra_dirs.append("scripts") + + data.extra_dirs.append("typesystems/glue") + + data.extra_files.append("__feature__.pyi") + data.extra_files.append("__init__.py") + data.extra_files.append("_git_pyside_version.py") + data.extra_files.append("_config.py") + data.extra_files.append("py.typed") + + # Assistant + if sys.platform == "darwin": + data.extra_dirs.append("Assistant.app") + else: + data.extra_files.append("assistant*") + data.translations.append("assistant_*") + + # Linguist + if sys.platform == "darwin": + data.extra_dirs.append("Linguist.app") + else: + data.extra_files.append("linguist*") + data.extra_files.append("lconvert*") + data.translations.append("linguist_*") + + data.extra_files.append("lrelease*") + data.extra_files.append("lupdate*") + + # General translations + data.translations.append("qtbase_*") + data.translations.append("qt_help_*") + data.translations.append("qt_*") + + # Extra libraries + data.qtlib.append("libicudata*") + data.qtlib.append("libicui18n*") + data.qtlib.append("libicule*") + data.qtlib.append("libiculx*") + data.qtlib.append("libicutest*") + data.qtlib.append("libicutu*") + data.qtlib.append("libicuuc*") + data.qtlib.append("libicuio*") + + return data + + +def module_QtGui() -> ModuleData: + data = ModuleData("Gui") + _typesystems = [ + "gui_common.xml", + "typesystem_gui_common.xml", + "typesystem_gui_mac.xml", + "typesystem_gui_win.xml", + "typesystem_gui_x11.xml", + "typesystem_gui_rhi.xml" + ] + + _metatypes = [ + "qt6eglfsdeviceintegrationprivate_relwithdebinfo_metatypes.json", + "qt6eglfskmssupportprivate_relwithdebinfo_metatypes.json", + "qt6kmssupportprivate_relwithdebinfo_metatypes.json", + "qt6xcbqpaprivate_relwithdebinfo_metatypes.json", + ] + + _qtlib = [ + "libQt6EglFSDeviceIntegration", + "libQt6EglFsKmsSupport", + "libQt6XcbQpa", + ] + + data.typesystems.extend(_typesystems) + data.metatypes.extend(_metatypes) + data.qtlib.extend(_qtlib) + + json_data = get_module_json_data("Gui") + data.plugins = get_module_plugins(json_data) + data.extra_files.append("Qt/plugins/platforms/libqeglfs*") + + return data + + +def module_QtWidgets() -> ModuleData: + data = ModuleData("Widgets") + data.typesystems.append("widgets_common.xml") + data.typesystems.append("typesystem_widgets_common.xml") + + if sys.platform == "win32": + data.extra_files.append("uic.exe") + else: + data.extra_files.append("Qt/libexec/uic") + json_data = get_module_json_data("Widgets") + data.plugins = get_module_plugins(json_data) + + return data + + +def module_QtHelp() -> ModuleData: + data = ModuleData("Help") + + return data + + +def module_QtNetwork() -> ModuleData: + data = ModuleData("Network") + json_data = get_module_json_data("Network") + data.plugins = get_module_plugins(json_data) + + return data + + +def module_QtBluetooth() -> ModuleData: + data = ModuleData("Bluetooth") + data.translations.append("qtconnectivity_*") + + return data + + +def module_QtConcurrent() -> ModuleData: + data = ModuleData("Concurrent") + + return data + + +def module_QtDBus() -> ModuleData: + data = ModuleData("DBus") + + return data + + +def module_QtDesigner() -> ModuleData: + data = ModuleData("Designer") + data.qtlib.append("libQt6DesignerComponents") + data.metatypes.append("qt6designercomponentsprivate_relwithdebinfo_metatypes.json") + json_data = get_module_json_data("Designer") + data.plugins = get_module_plugins(json_data) + data.extra_files.append("Qt/plugins/assetimporters/libuip*") + + # Designer + if sys.platform == "darwin": + data.extra_dirs.append("Designer.app") + else: + data.extra_files.append("designer*") + data.translations.append("designer_*") + + return data + + +def module_QtNfc() -> ModuleData: + data = ModuleData("Nfc") + + return data + + +def module_QtPdf() -> ModuleData: + data = ModuleData("Pdf") + data.qtlib.append("libQt6PdfQuick") + + return data + + +def module_QtPdfWidgets() -> ModuleData: + data = ModuleData("PdfWidgets") + + return data + + +def module_QtPrintSupport() -> ModuleData: + data = ModuleData("PrintSupport") + data.typesystems.append("typesystem_printsupport_common.xml") + json_data = get_module_json_data("PrintSupport") + data.plugins = get_module_plugins(json_data) + + return data + + +def module_QtQml() -> ModuleData: + data = ModuleData("Qml") + json_data = get_module_json_data("Qml") + data.plugins = get_module_plugins(json_data) + json_data = get_module_json_data("QmlCompilerPrivate") + data.plugins += get_module_plugins(json_data) + + _qtlib = [ + "libQt6LabsAnimation", + "libQt6LabsFolderListModel", + "libQt6LabsQmlModels*", + "libQt6LabsSettings", + "libQt6LabsSharedImage", + "libQt6LabsWavefrontMesh", + "libQt6QmlCore", + "libQt6QmlLocalStorage", + "libQt6QmlModels", + "libQt6QmlNetwork", + "libQt6QmlWorkerScript", + "libQt6QmlXmlListModel", + "libQt6QmlCompiler" + ] + + _include = [ + "pysideqml.h", + "pysideqmlmacros.h", + "pysideqmlregistertype.h", + ] + + _metatypes = [ + "qt6labsanimation_relwithdebinfo_metatypes.json", + "qt6labsfolderlistmodel_relwithdebinfo_metatypes.json", + "qt6labsqmlmodels_relwithdebinfo_metatypes.json", + "qt6labssettings_relwithdebinfo_metatypes.json", + "qt6labssharedimage_relwithdebinfo_metatypes.json", + "qt6labswavefrontmesh_relwithdebinfo_metatypes.json", + "qt6packetprotocolprivate_relwithdebinfo_metatypes.json", + "qt6qmlcompilerprivate_relwithdebinfo_metatypes.json", + "qt6qmlcompilerplusprivate_relwithdebinfo_metatypes.json", + "qt6qmlcore_relwithdebinfo_metatypes.json", + "qt6qmldebugprivate_relwithdebinfo_metatypes.json", + "qt6qmldomprivate_relwithdebinfo_metatypes.json", + "qt6qmllintprivate_relwithdebinfo_metatypes.json", + "qt6qmllocalstorage_relwithdebinfo_metatypes.json", + "qt6qmlmodels_relwithdebinfo_metatypes.json", + "qt6qmlworkerscript_relwithdebinfo_metatypes.json", + "qt6qmlxmllistmodel_relwithdebinfo_metatypes.json", + ] + + _qml = [ + "Qt/labs/animation", + "Qt/labs/folderlistmodel", + "Qt/labs/sharedimage", + "Qt/labs/wavefrontmesh", + "Qt/labs/qmlmodels", + "Qt/labs/platform", + "Qt/labs/settings", + ] + + data.lib.append("libpyside6qml") + json_data = get_module_json_data("Qml") + data.plugins = get_module_plugins(json_data) + data.translations.append("qtdeclarative_*") + if sys.platform == "win32": + data.extra_files.append("pyside6qml.*.lib") + data.extra_files.append("pyside6qml.*.dll") + data.extra_files.append("qml/builtins.qmltypes") + data.extra_files.append("qml/jsroot.qmltypes") + data.extra_files.append("qmlimportscanner.exe") + data.extra_files.append("qmltyperegistrar.exe") + data.extra_files.append("qmlcachegen.exe") + else: + data.extra_files.append("Qt/qml/builtins.qmltypes") + data.extra_files.append("Qt/qml/jsroot.qmltypes") + data.extra_files.append("Qt/libexec/qmlimportscanner") + data.extra_files.append("Qt/libexec/qmltyperegistrar") + data.extra_files.append("Qt/libexec/qmlcachegen") + + data.qtlib.extend(_qtlib) + data.include.extend(_include) + data.metatypes.extend(_metatypes) + data.qml.extend(_qml) + + data.extra_files.append("qmllint*") + data.extra_files.append("qmlformat*") + data.extra_files.append("qmlls*") + + return data + + +def module_QtQuick() -> ModuleData: + data = ModuleData("Quick") + _metatypes = [ + "qt6quickcontrolstestutilsprivate_relwithdebinfo_metatypes.json", + "qt6quickdialogs2_relwithdebinfo_metatypes.json", + "qt6quickdialogs2quickimpl_relwithdebinfo_metatypes.json", + "qt6quickdialogs2utils_relwithdebinfo_metatypes.json", + "qt6quickeffectsprivate_relwithdebinfo_metatypes.json", + "qt6quicketest_relwithdebinfo_metatypes.json", + "qt6quicketestutilsprivate_relwithdebinfo_metatypes.json", + "qt6quicklayouts_relwithdebinfo_metatypes.json", + "qt6quickparticlesprivate_relwithdebinfo_metatypes.json", + "qt6quickshapesprivate_relwithdebinfo_metatypes.json", + "qt6quicktemplates2_relwithdebinfo_metatypes.json", + "qt6quicktest_relwithdebinfo_metatypes.json", + "qt6quicktestutilsprivate_relwithdebinfo_metatypes.json", + "qt6quicktimeline_relwithdebinfo_metatypes.json", + ] + _qtlib = [ + "libQt6QuickEffects", + "libQt6QuickDialogs2", + "libQt6QuickDialogs2QuickImpl", + "libQt6QuickDialogs2Utils", + "libQt6QuickLayouts", + "libQt6QuickParticles", + "libQt6QuickShapes", + "libQt6QuickTemplates2", + "libQt6QuickTest", + "libQt6QuickTimeline", + "libQt6QuickTimelineBlendTrees", + ] + + # Adding GraphicalEffects files + data.qml.append("Qt5Compat/GraphicalEffects") + + data.qtlib.extend(_qtlib) + data.metatypes.extend(_metatypes) + json_data = get_module_json_data("Quick") + data.plugins = get_module_plugins(json_data) + + return data + + +def module_QtQuickControls2() -> ModuleData: + data = ModuleData("QuickControls2") + data.qtlib.append("libQt6QuickControls2") + data.qtlib.append("libQt6QuickControls2Basic") + data.qtlib.append("libQt6QuickControls2BasicStyleImpl") + data.qtlib.append("libQt6QuickControls2Fusion") + data.qtlib.append("libQt6QuickControls2FusionStyleImpl") + data.qtlib.append("libQt6QuickControls2Imagine") + data.qtlib.append("libQt6QuickControls2ImagineStyleImpl") + data.qtlib.append("libQt6QuickControls2Impl") + data.qtlib.append("libQt6QuickControls2Material") + data.qtlib.append("libQt6QuickControls2MaterialStyleImpl") + data.qtlib.append("libQt6QuickControls2Universal") + data.qtlib.append("libQt6QuickControls2UniversalStyleImpl") + if sys.platform == "win32": + data.qtlib.append("libQt6QuickControls2WindowsStyleImpl") + elif sys.platform == "darwin": + data.qtlib.append("libQt6QuickControls2IOSStyleImpl") + data.qtlib.append("libQt6QuickControls2MacOSStyleImpl") + + data.metatypes.append("qt6quickcontrols2impl_relwithdebinfo_metatypes.json") + + return data + + +def module_QtQuickTest() -> ModuleData: + data = ModuleData("QuickTest") + + return data + + +def module_QtQuickWidgets() -> ModuleData: + data = ModuleData("QuickWidgets") + return data + + +def module_QtXml() -> ModuleData: + data = ModuleData("Xml") + return data + + +def module_QtTest() -> ModuleData: + data = ModuleData("Test") + return data + + +def module_QtSql() -> ModuleData: + data = ModuleData("Sql") + json_data = get_module_json_data("Sql") + data.plugins = get_module_plugins(json_data) + + return data + + +def module_QtSvg() -> ModuleData: + data = ModuleData("Svg") + + return data + + +def module_QtSvgWidgets() -> ModuleData: + data = ModuleData("SvgWidgets") + + return data + + +def module_QtTextToSpeech() -> ModuleData: + data = ModuleData("TextToSpeech") + json_data = get_module_json_data("TextToSpeech") + data.plugins = get_module_plugins(json_data) + + return data + + +def module_QtUiTools() -> ModuleData: + data = ModuleData("UiTools") + + return data + + +def module_QtWayland() -> ModuleData: + data = ModuleData("Wayland") + + _qtlib = [ + "libQt6WaylandClient", + "libQt6WaylandCompositor", + "libQt6WaylandEglClientHwIntegration", + "libQt6WaylandEglCompositorHwIntegration", + "libQt6WlShellIntegration", + ] + + _metatypes = [ + "qt6waylandclient_relwithdebinfo_metatypes.json", + "qt6waylandeglclienthwintegrationprivate_relwithdebinfo_metatypes.json", + "qt6wlshellintegrationprivate_relwithdebinfo_metatypes.json", + ] + + data.qtlib.extend(_qtlib) + data.metatypes.extend(_metatypes) + json_data = get_module_json_data("WaylandClient") + data.plugins = get_module_plugins(json_data) + json_data = get_module_json_data("WaylandCompositor") + data.plugins += get_module_plugins(json_data) + return data + + +def module_Qt3DCore() -> ModuleData: + data = ModuleData("3DCore", qml=["Qt3D/Core"]) + + return data + + +def module_Qt3DAnimation() -> ModuleData: + data = ModuleData("3DAnimation", qml=["Qt3D/Animation"]) + + return data + + +def module_Qt3DExtras() -> ModuleData: + data = ModuleData("3DExtras", qml=["Qt3D/Extras"]) + + return data + + +def module_Qt3DInput() -> ModuleData: + data = ModuleData("3DInput", qml=["Qt3D/Input"]) + json_data = get_module_json_data("3DInput") + data.plugins = get_module_plugins(json_data) + + return data + + +def module_Qt3DLogic() -> ModuleData: + data = ModuleData("3DLogic", qml=["Qt3D/Logic"]) + + return data + + +def module_Qt3DRender() -> ModuleData: + data = ModuleData("3DRender", qml=["Qt3D/Render"]) + json_data = get_module_json_data("3DRender") + data.plugins = get_module_plugins(json_data) + + return data + + +def module_QtQuick3D() -> ModuleData: + data = ModuleData("Quick3D") + + _qtlib = [ + "libQt6Quick3DAssetImport", + "libQt6Quick3DAssetUtils", + "libQt6Quick3DEffects", + "libQt6Quick3DGlslParser", + "libQt6Quick3DHelpers", + "libQt6Quick3DHelpersImpl", + "libQt6Quick3DIblBaker", + "libQt6Quick3DParticleEffects", + "libQt6Quick3DParticles", + "libQt6Quick3DPhysics", + "libQt6Quick3DPhysicsHelpers", + "libQt6Quick3DRuntimeRender", + "libQt6Quick3DSpatialAudio", + "libQt6Quick3DUtils", + "libQt6ShaderTools", + "libQt63DQuick", + "libQt63DQuickAnimation", + "libQt63DQuickExtras", + "libQt63DQuickExtras", + "libQt63DQuickInput", + "libQt63DQuickRender", + "libQt63DQuickScene2D", + ] + + _metatypes = [ + "qt63dquick_relwithdebinfo_metatypes.json", + "qt63dquickanimation_relwithdebinfo_metatypes.json", + "qt63dquickextras_relwithdebinfo_metatypes.json", + "qt63dquickinput_relwithdebinfo_metatypes.json", + "qt63dquickrender_relwithdebinfo_metatypes.json", + "qt63dquickscene2d_relwithdebinfo_metatypes.json", + "qt6quick3dassetimport_relwithdebinfo_metatypes.json", + "qt6quick3dassetutils_relwithdebinfo_metatypes.json", + "qt6quick3deffects_relwithdebinfo_metatypes.json", + "qt6quick3dglslparserprivate_relwithdebinfo_metatypes.json", + "qt6quick3dhelpers_relwithdebinfo_metatypes.json", + "qt6quick3diblbaker_relwithdebinfo_metatypes.json", + "qt6quick3dparticleeffects_relwithdebinfo_metatypes.json", + "qt6quick3dparticles_relwithdebinfo_metatypes.json", + "qt6quick3druntimerender_relwithdebinfo_metatypes.json", + "qt6quick3dutils_relwithdebinfo_metatypes.json", + "qt6shadertools_relwithdebinfo_metatypes.json", + ] + + json_data = get_module_json_data("Quick3DAssetImport") + data.plugins = get_module_plugins(json_data) + data.qtlib.extend(_qtlib) + data.metatypes.extend(_metatypes) + data.extra_files.append("Qt/plugins/assetimporters/libassimp*") + data.extra_files.append("qsb*") + data.extra_files.append("balsam*") + + return data + + +def module_QtAxContainer() -> ModuleData: + data = ModuleData("AxContainer") + if sys.platform == "win32": + data.metatypes.append("qt6axbaseprivate_metatypes.json") + data.metatypes.append("qt6axserver_metatypes.json") + + return data + + +def module_QtWebEngineCore() -> ModuleData: + data = ModuleData("WebEngineCore", qml=["QtWebEngine"]) + data.translations.append("qtwebengine_locales/*") + data.translations.append("qtwebengine_*") + data.extra_dirs.append("Qt/resources") + if sys.platform == "win32": + data.extra_files.append("resources/qtwebengine*.pak") + data.extra_files.append("resources/v8_context_snapshot*.*") + data.extra_files.append("QtWebEngineProcess.exe") + else: + data.extra_files.append("Qt/libexec/QtWebEngineProcess") + + return data + + +def module_QtWebEngineWidgets() -> ModuleData: + data = ModuleData("WebEngineWidgets") + + return data + + +def module_QtWebEngineQuick() -> ModuleData: + data = ModuleData("WebEngineQuick") + data.qtlib.append("libQt6WebEngineQuickDelegatesQml") + data.metatypes.append("qt6webenginequickdelegatesqml_relwithdebinfo_metatypes.json") + + return data + + +def module_QtCharts() -> ModuleData: + data = ModuleData("Charts") + data.qtlib.append("libQt6ChartsQml") + data.metatypes.append("qt6chartsqml_relwithdebinfo_metatypes.json") + + return data + + +def module_QtDataVisualization() -> ModuleData: + data = ModuleData("DataVisualization") + data.qtlib.append("libQt6DataVisualizationQml") + data.metatypes.append("qt6datavisualizationqml_relwithdebinfo_metatypes.json") + data.typesystems.append("datavisualization_common.xml") + + return data + + +def module_QtGraphs() -> ModuleData: + data = ModuleData("Graphs") + + return data + + +def module_QtMultimedia() -> ModuleData: + data = ModuleData("Multimedia") + data.qtlib.append("libQt6MultimediaQuick") + data.metatypes.append("qt6multimediaquickprivate_relwithdebinfo_metatypes.json") + + json_data = get_module_json_data("Multimedia") + data.translations.append("qtmultimedia_*") + data.plugins = get_module_plugins(json_data) + + if sys.platform == "win32": + data.extra_files.extend(["avcodec-60.dll", "avformat-60.dll", "avutil-58.dll", + "swresample-4.dll", "swscale-7.dll"]) + + return data + + +def module_QtMultimediaWidgets() -> ModuleData: + data = ModuleData("MultimediaWidgets") + + return data + + +def module_QtNetworkAuth() -> ModuleData: + data = ModuleData("NetworkAuth") + + return data + + +def module_QtPositioning() -> ModuleData: + data = ModuleData("Positioning") + data.qtlib.append("libQt6PositioningQuick") + data.metatypes.append("qt6positioningquick_relwithdebinfo_metatypes.json") + json_data = get_module_json_data("Positioning") + data.plugins = get_module_plugins(json_data) + + return data + + +def module_QtRemoteObjects() -> ModuleData: + data = ModuleData("RemoteObjects") + data.qtlib.append("libQt6RemoteObjectsQml") + data.metatypes.append("qt6remoteobjectsqml_relwithdebinfo_metatypes.json") + + return data + + +def module_QtSensors() -> ModuleData: + data = ModuleData("Sensors") + data.qtlib.append("libQt6SensorsQuick") + data.metatypes.append("qt6sensorsquick_relwithdebinfo_metatypes.json") + json_data = get_module_json_data("Sensors") + data.plugins = get_module_plugins(json_data) + + return data + + +def module_QtSerialPort() -> ModuleData: + data = ModuleData("SerialPort") + data.translations.append("qtserialport_*") + + return data + + +def module_QtSpatialAudio() -> ModuleData: + data = ModuleData("SpatialAudio") + data.metatypes.append("qt6spatialaudio_debug_metatypes.json") + + return data + + +def module_QtStateMachine() -> ModuleData: + data = ModuleData("StateMachine") + data.qtlib.append("libQt6StateMachineQml") + data.metatypes.append("qt6statemachineqml_relwithdebinfo_metatypes.json") + + return data + + +def module_QtScxml() -> ModuleData: + data = ModuleData("Scxml") + data.qtlib.append("libQt6ScxmlQml") + data.metatypes.append("qt6scxmlqml_relwithdebinfo_metatypes.json") + json_data = get_module_json_data("Scxml") + data.plugins = get_module_plugins(json_data) + + return data + + +def module_QtWebChannel() -> ModuleData: + data = ModuleData("WebChannel") + data.qtlib.append("libQt6WebChannelQuick") + + return data + + +def module_QtWebSockets() -> ModuleData: + data = ModuleData("WebSockets") + data.translations.append("qtwebsockets_*") + + return data + + +def module_QtOpenGL() -> ModuleData: + data = ModuleData("OpenGL") + _typesystems = [ + "opengl_common.xml", + "typesystem_glgeti_v_includes.xml", + "typesystem_glgeti_v_modifications.xml", + "typesystem_glgetv_includes.xml", + "typesystem_glgetv_modifications.xml", + "typesystem_opengl_modifications1_0.xml", + "typesystem_opengl_modifications1_0_compat.xml", + "typesystem_opengl_modifications1_1.xml", + "typesystem_opengl_modifications1_1_compat.xml", + "typesystem_opengl_modifications1_2_compat.xml", + "typesystem_opengl_modifications1_3_compat.xml", + "typesystem_opengl_modifications1_4.xml", + "typesystem_opengl_modifications1_4_compat.xml", + "typesystem_opengl_modifications2_0.xml", + "typesystem_opengl_modifications2_0_compat.xml", + "typesystem_opengl_modifications2_1.xml", + "typesystem_opengl_modifications3_0.xml", + "typesystem_opengl_modifications3_3.xml", + "typesystem_opengl_modifications3_3a.xml", + "typesystem_opengl_modifications4_0.xml", + "typesystem_opengl_modifications4_1.xml", + "typesystem_opengl_modifications4_3.xml", + "typesystem_opengl_modifications4_4.xml", + "typesystem_opengl_modifications4_4_core.xml", + "typesystem_opengl_modifications4_5.xml", + "typesystem_opengl_modifications4_5_core.xml", + "typesystem_opengl_modifications_va.xml", + ] + + data.typesystems.extend(_typesystems) + if sys.platform == "win32": + data.extra_files.append("opengl32*.dll") + + return data + + +def module_QtOpenGLWidgets() -> ModuleData: + data = ModuleData("OpenGLWidgets") + return data + + +def module_QtSerialBus() -> ModuleData: + data = ModuleData("SerialBus") + json_data = get_module_json_data("SerialBus") + data.plugins = get_module_plugins(json_data) + return data + + +def module_QtVirtualKeyboard() -> ModuleData: + data = ModuleData("VirtualKeyboard") + data.plugins.append("virtualkeyboard") + return data + + +def module_QtHttpServer() -> ModuleData: + data = ModuleData("HttpServer") + return data + + +def module_QtLanguageServer() -> ModuleData: + data = ModuleData("LanguageServer") + data.metatypes.append("qt6languageserverprivate_relwithdebinfo_metatypes.json") + return data + + +def module_QtJsonRpc() -> ModuleData: + data = ModuleData("JsonRpc") + data.metatypes.append("qt6jsonrpcprivate_relwithdebinfo_metatypes.json") + return data + + +def module_QtLocation() -> ModuleData: + data = ModuleData("Location") + json_data = get_module_json_data("Location") + data.plugins = get_module_plugins(json_data) + data.translations.append("qtlocation_*") + return data + + +def module_QtAsyncio() -> ModuleData: + data = ModuleData("Asyncio") + data.extra_dirs.append("QtAsyncio") + return data + + +def module_QtExampleIcons() -> ModuleData: + data = ModuleData("ExampleIcons") + return data diff --git a/build_scripts/wheel_override.py b/build_scripts/wheel_override.py index 03c9c92ab..f3f9f17a9 100644 --- a/build_scripts/wheel_override.py +++ b/build_scripts/wheel_override.py @@ -1,95 +1,66 @@ -############################################################################# -## -## 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$ -## -############################################################################# +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +import platform +import sys +from pathlib import Path +from email.generator import Generator + +from .log import log +from .options import OPTION, CommandMixin +from .utils import is_64bit +from .wheel_utils import get_package_version, get_qt_version, macos_plat_name + wheel_module_exists = False + try: - import os - import sys - from distutils import log as logger - from wheel import pep425tags + from packaging import tags + from wheel import __version__ as wheel_version from wheel.bdist_wheel import bdist_wheel as _bdist_wheel + from wheel.bdist_wheel import get_abi_tag, get_platform from wheel.bdist_wheel import safer_name as _safer_name - from wheel.pep425tags import get_abbr_impl, get_impl_ver, get_abi_tag - from wheel.pep425tags import get_platform as wheel_get_platform - from email.generator import Generator - from wheel import __version__ as wheel_version - - from .options import OPTION wheel_module_exists = True except Exception as e: - _bdist_wheel, wheel_version = type, '' # dummy to make class statement happy - print('***** Exception while trying to prepare bdist_wheel override class: {}. ' - 'Skipping wheel overriding.'.format(e)) + _bdist_wheel, wheel_version = type, "" # dummy to make class statement happy + log.warning(f"***** Exception while trying to prepare bdist_wheel override class: {e}. " + "Skipping wheel overriding.") + + +def get_bdist_wheel_override(): + return PysideBuildWheel if wheel_module_exists else None -def get_bdist_wheel_override(params): - if wheel_module_exists: - class PysideBuildWheelDecorated(PysideBuildWheel): - def __init__(self, *args, **kwargs): - self.params = params - PysideBuildWheel.__init__(self, *args, **kwargs) - return PysideBuildWheelDecorated - else: - return None +class PysideBuildWheel(_bdist_wheel, CommandMixin): + user_options = (_bdist_wheel.user_options + CommandMixin.mixin_user_options + if wheel_module_exists else None) -class PysideBuildWheel(_bdist_wheel): def __init__(self, *args, **kwargs): - self.pyside_params = None + self.command_name = "bdist_wheel" + self._package_version = None _bdist_wheel.__init__(self, *args, **kwargs) + CommandMixin.__init__(self) def finalize_options(self): + CommandMixin.mixin_finalize_options(self) if sys.platform == 'darwin': # Override the platform name to contain the correct # minimum deployment target. # This is used in the final wheel name. - self.plat_name = self.params['macos_plat_name'] + self.plat_name = macos_plat_name() # When limited API is requested, notify bdist_wheel to - # create a properly named package. - limited_api_enabled = OPTION["LIMITED_API"] and sys.version_info[0] >= 3 + # create a properly named package, which will contain + # the initial cpython version we support. + limited_api_enabled = OPTION["LIMITED_API"] == 'yes' if limited_api_enabled: - self.py_limited_api = "cp35.cp36.cp37.cp38" + self.py_limited_api = "cp37" + + self._package_version = get_package_version() _bdist_wheel.finalize_options(self) @@ -98,23 +69,114 @@ class PysideBuildWheel(_bdist_wheel): # Slightly modified version of wheel's wheel_dist_name # method, to add the Qt version as well. # Example: - # PySide2-5.6-5.6.4-cp27-cp27m-macosx_10_10_intel.whl - # The PySide2 version is "5.6". - # The Qt version built against is "5.6.4". - qt_version = self.params['qt_version'] - package_version = self.params['package_version'] - wheel_version = "{}-{}".format(package_version, qt_version) + # PySide6-6.3-6.3.2-cp36-abi3-macosx_10_10_intel.whl + # The PySide6 version is "6.3". + # The Qt version built against is "6.3.2". + wheel_version = f"{self._package_version}-{get_qt_version()}" components = (_safer_name(self.distribution.get_name()), wheel_version) if self.build_number: components += (self.build_number,) return '-'.join(components) - # Copy of get_tag from bdist_wheel.py, to allow setting a - # multi-python impl tag, by removing an assert. Otherwise we - # would have to rename wheels manually for limited api - # packages. Also we set "none" abi tag on Windows, because - # pip does not yet support "abi3" tag, leading to - # installation failure when tried. + # Modify the returned wheel tag tuple to use correct python version + # info when cross-compiling. We use the python info extracted from + # the shiboken python config test. + # setuptools / wheel don't support cross compiling out of the box + # at the moment. Relevant discussion at + # https://discuss.python.org/t/towards-standardizing-cross-compiling/10357 + def get_cross_compiling_tag_tuple(self, tag_tuple): + (old_impl, old_abi_tag, plat_name) = tag_tuple + + # Compute tag from the python version that the build command + # queried. + build_command = self.get_finalized_command('build') + python_target_info = build_command.python_target_info['python_info'] + + impl = 'no-py-ver-impl-available' + abi = 'no-abi-tag-info-available' + py_version = python_target_info['version'].split('.') + py_version_major, py_version_minor, _ = py_version + + so_abi = python_target_info['so_abi'] + if so_abi and so_abi.startswith('cpython-'): + interpreter_name, cp_version = so_abi.split('-')[:2] + impl_name = tags.INTERPRETER_SHORT_NAMES.get(interpreter_name) or interpreter_name + impl_ver = f"{py_version_major}{py_version_minor}" + impl = impl_name + impl_ver + abi = f'cp{cp_version}' + tag_tuple = (impl, abi, plat_name) + return tag_tuple + + # Adjust wheel tag for limited api and cross compilation. + @staticmethod + def adjust_cross_compiled_many_linux_tag(old_tag): + (old_impl, old_abi_tag, old_plat_name) = old_tag + + new_plat_name = old_plat_name + + # TODO: Detect glibc version instead. We're abusing the + # manylinux2014 tag here, just like we did with manylinux1 + # for x86_64 builds. + many_linux_prefix = 'manylinux2014' + linux_prefix = "linux_" + if old_plat_name.startswith(linux_prefix): + # Extract the arch suffix like -armv7l or -aarch64 + _index = old_plat_name.index(linux_prefix) + len(linux_prefix) + plat_name_arch_suffix = old_plat_name[_index:] + + new_plat_name = f"{many_linux_prefix}_{plat_name_arch_suffix}" + + tag = (old_impl, old_abi_tag, new_plat_name) + return tag + + # Adjust wheel tag for limited api and cross compilation. + def adjust_tag_and_supported_tags(self, old_tag, supported_tags): + tag = old_tag + (old_impl, old_abi_tag, old_plat_name) = old_tag + + # Get new tag for cross builds. + if self.is_cross_compile: + tag = self.get_cross_compiling_tag_tuple(old_tag) + + # Use PEP600 for manylinux wheel name + # For Qt6 we know RHEL 8.4 is the base linux platform, + # and has GLIBC 2.28. + # This will generate a name that contains: + # manylinux_2_28 + # TODO: Add actual distro detection, instead of + # relying on limited_api option. + if (old_plat_name in ('linux-x86_64', 'linux_x86_64') + and is_64bit() + and self.py_limited_api): + _, _version = platform.libc_ver() + glibc = _version.replace(".", "_") + tag = (old_impl, old_abi_tag, f"manylinux_{glibc}_x86_64") + + # Set manylinux tag for cross-compiled builds when targeting + # limited api. + if self.is_cross_compile and self.py_limited_api: + tag = self.adjust_cross_compiled_many_linux_tag(tag) + + # Reset the abi name and python versions supported by this wheel + # when targeting limited API. This is the same code that's + # in get_tag(), but done later after our own customizations. + if self.py_limited_api and old_impl.startswith('cp3'): + (_, _, adjusted_plat_name) = tag + impl = self.py_limited_api + abi_tag = 'abi3' + tag = (impl, abi_tag, adjusted_plat_name) + + # If building for limited API or we created a new tag, add it + # to the list of supported tags. + if tag != old_tag or self.py_limited_api: + supported_tags.append(tag) + return tag + + # A slightly modified copy of get_tag from bdist_wheel.py, to allow + # adjusting the returned tag without triggering an assert. Otherwise + # we would have to rename wheels manually. + # Copy is up-to-date since commit + # 0acd203cd896afec7f715aa2ff5980a403459a3b in the wheel repo. def get_tag(self): # bdist sets self.plat_name if unset, we should only use it for purepy # wheels if the user supplied it. @@ -123,64 +185,51 @@ class PysideBuildWheel(_bdist_wheel): elif self.root_is_pure: plat_name = 'any' else: - plat_name = self.plat_name or wheel_get_platform() - if plat_name in ('linux-x86_64', 'linux_x86_64') and sys.maxsize == 2147483647: + # macosx contains system version in platform name so need special handle + if self.plat_name and not self.plat_name.startswith("macosx"): + plat_name = self.plat_name + else: + # on macOS always limit the platform name to comply with any + # c-extension modules in bdist_dir, since the user can specify + # a higher MACOSX_DEPLOYMENT_TARGET via tools like CMake + + # on other platforms, and on macOS if there are no c-extension + # modules, use the default platform name. + plat_name = get_platform(self.bdist_dir) + + if plat_name in ('linux-x86_64', 'linux_x86_64') and not is_64bit(): plat_name = 'linux_i686' - # To allow uploading to pypi, we need the wheel name - # to contain 'manylinux1'. - # The wheel which will be uploaded to pypi will be - # built on RHEL7, so it doesn't completely qualify for - # manylinux1 support, but it's the minimum requirement - # for building Qt. We only enable this for x64 limited - # api builds (which are the only ones uploaded to - # pypi). - # TODO: Add actual distro detection, instead of - # relying on limited_api option. - if (plat_name in ('linux-x86_64', 'linux_x86_64') - and sys.maxsize > 2147483647 - and (self.py_limited_api or sys.version_info[0] == 2)): - plat_name = 'manylinux1_x86_64' - plat_name = plat_name.replace('-', '_').replace('.', '_') + plat_name = plat_name.lower().replace('-', '_').replace('.', '_') if self.root_is_pure: if self.universal: - impl = 'py2.py3' + impl = 'py3' else: impl = self.python_tag tag = (impl, 'none', plat_name) else: - impl_name = get_abbr_impl() - impl_ver = get_impl_ver() + impl_name = tags.interpreter_name() + impl_ver = tags.interpreter_version() impl = impl_name + impl_ver # We don't work on CPython 3.1, 3.0. if self.py_limited_api and (impl_name + impl_ver).startswith('cp3'): impl = self.py_limited_api - abi_tag = "abi3" if sys.platform != "win32" else "none" + abi_tag = 'abi3' else: abi_tag = str(get_abi_tag()).lower() tag = (impl, abi_tag, plat_name) - try: - supported_tags = pep425tags.get_supported( - supplied_platform=plat_name if self.plat_name_supplied else None) - except TypeError: - # This was breaking the CI, specifically the: - # OpenSUSE 15 x86_64 using ICC - # Some versions of Python 2.7 require an argument called - # 'archive_root' which doesn't exist on 3, so we set it to - # 'None' for those version (e.g.: Python 2.7.14) - supported_tags = pep425tags.get_supported(None, - supplied_platform=plat_name if self.plat_name_supplied else None) - # XXX switch to this alternate implementation for non-pure: - if (self.py_limited_api) or (plat_name in ('manylinux1_x86_64') and sys.version_info[0] == 2): - return tag - assert tag == supported_tags[0], "%s != %s" % (tag, supported_tags[0]) - assert tag in supported_tags, ("would build wheel with unsupported tag {}".format(tag)) + # issue gh-374: allow overriding plat_name + supported_tags = [(t.interpreter, t.abi, plat_name) + for t in tags.sys_tags()] + # PySide's custom override. + tag = self.adjust_tag_and_supported_tags(tag, supported_tags) + assert tag in supported_tags, (f"would build wheel with unsupported tag {tag}") return tag # Copy of get_tag from bdist_wheel.py, to write a triplet Tag # only once for the limited_api case. - def write_wheelfile(self, wheelfile_base, generator='bdist_wheel (' + wheel_version + ')'): + def write_wheelfile(self, wheelfile_base, generator=f'bdist_wheel ({wheel_version})'): from email.message import Message msg = Message() msg['Wheel-Version'] = '1.0' # of the spec @@ -192,7 +241,7 @@ class PysideBuildWheel(_bdist_wheel): # Doesn't work for bdist_wininst impl_tag, abi_tag, plat_tag = self.get_tag() # To enable pypi upload we are adjusting the wheel name - pypi_ready = (OPTION["LIMITED_API"] and sys.version_info[0] >= 3) or (sys.version_info[0] == 2) + pypi_ready = True if OPTION["LIMITED_API"] else False def writeTag(impl): for abi in abi_tag.split('.'): @@ -204,8 +253,8 @@ class PysideBuildWheel(_bdist_wheel): for impl in impl_tag.split('.'): writeTag(impl) - wheelfile_path = os.path.join(wheelfile_base, 'WHEEL') - logger.info('creating %s', wheelfile_path) + wheelfile_path = Path(wheelfile_base) / 'WHEEL' + log.info(f'creating {wheelfile_path}') with open(wheelfile_path, 'w') as f: Generator(f, maxheaderlen=0).flatten(msg) diff --git a/build_scripts/wheel_utils.py b/build_scripts/wheel_utils.py new file mode 100644 index 000000000..5ec26c742 --- /dev/null +++ b/build_scripts/wheel_utils.py @@ -0,0 +1,124 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import time +from pathlib import Path +from sysconfig import get_config_var, get_platform + +from packaging.version import parse as parse_version +from setuptools.errors import SetupError + +from .options import OPTION +from .qtinfo import QtInfo +from .utils import memoize, parse_cmake_conf_assignments_by_key +from . import PYSIDE + + +@memoize +def get_package_timestamp(): + """ In a Coin CI build the returned timestamp will be the + Coin integration id timestamp. For regular builds it's + just the current timestamp or a user provided one.""" + option_value = OPTION["PACKAGE_TIMESTAMP"] + return option_value if option_value else int(time.time()) + + +def get_qt_version(): + qtinfo = QtInfo() + qt_version = qtinfo.version + + if not qt_version: + raise SetupError("Failed to query the Qt version with qmake {qtinfo.qmake_command}") + + if parse_version(qtinfo.version) < parse_version("5.7"): + raise SetupError(f"Incompatible Qt version detected: {qt_version}. " + "A Qt version >= 5.7 is required.") + + return qt_version + + +@memoize +def get_package_version(): + """ Returns the version string for the PySide6 package. """ + setup_script_dir = Path.cwd() + pyside_project_dir = setup_script_dir / "sources" / PYSIDE + d = parse_cmake_conf_assignments_by_key(pyside_project_dir) + major_version = d['pyside_MAJOR_VERSION'] + minor_version = d['pyside_MINOR_VERSION'] + patch_version = d['pyside_MICRO_VERSION'] + + final_version = f"{major_version}.{minor_version}.{patch_version}" + release_version_type = d.get('pyside_PRE_RELEASE_VERSION_TYPE') + pre_release_version = d.get('pyside_PRE_RELEASE_VERSION') + + if release_version_type and not release_version_type.startswith("comm") and pre_release_version: + final_version = f"{final_version}{release_version_type}{pre_release_version}" + if release_version_type and release_version_type.startswith("comm"): + final_version = f"{final_version}+{release_version_type}" + + # Add the current timestamp to the version number, to suggest it + # is a development snapshot build. + if OPTION["SNAPSHOT_BUILD"]: + final_version = f"{final_version}.dev{get_package_timestamp()}" + return final_version + + +def macos_qt_min_deployment_target(): + target = QtInfo().macos_min_deployment_target + + if not target: + raise SetupError("Failed to query for Qt's QMAKE_MACOSX_DEPLOYMENT_TARGET.") + return target + + +@memoize +def macos_pyside_min_deployment_target(): + """ + Compute and validate PySide6 MACOSX_DEPLOYMENT_TARGET value. + Candidate sources that are considered: + - setup.py provided value + - maximum value between minimum deployment target of the + Python interpreter and the minimum deployment target of + the Qt libraries. + If setup.py value is provided, that takes precedence. + Otherwise use the maximum of the above mentioned two values. + """ + python_target = get_config_var('MACOSX_DEPLOYMENT_TARGET') or None + qt_target = macos_qt_min_deployment_target() + setup_target = OPTION["MACOS_DEPLOYMENT_TARGET"] + + qt_target_split = [int(x) for x in qt_target.split('.')] + if python_target: + # macOS Big Sur returns a number not a string + python_target_split = [int(x) for x in str(python_target).split('.')] + if setup_target: + setup_target_split = [int(x) for x in setup_target.split('.')] + + message = ("Can't set MACOSX_DEPLOYMENT_TARGET value to {} because " + "{} was built with minimum deployment target set to {}.") + # setup.py provided OPTION["MACOS_DEPLOYMENT_TARGET"] value takes + # precedence. + if setup_target: + if python_target and setup_target_split < python_target_split: + raise SetupError(message.format(setup_target, "Python", python_target)) + if setup_target_split < qt_target_split: + raise SetupError(message.format(setup_target, "Qt", qt_target)) + # All checks clear, use setup.py provided value. + return setup_target + + # Setup.py value not provided, + # use same value as provided by Qt. + if python_target: + maximum_target = '.'.join([str(e) for e in max(python_target_split, qt_target_split)]) + else: + maximum_target = qt_target + return maximum_target + + +@memoize +def macos_plat_name(): + deployment_target = macos_pyside_min_deployment_target() + # Example triple "macosx-10.12-x86_64". + plat = get_platform().split("-") + plat_name = f"{plat[0]}-{deployment_target}-{plat[2]}" + return plat_name |