#############################################################################
##
## 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 packaging.version import parse as parse_version

import os
import platform
import re
import sys
import importlib
from textwrap import dedent
import time
from .config import config
from .utils import get_numpy_location, get_python_dict
from .options import DistUtilsCommandMixin, OPTION
from .build_info_collector import BuildInfoCollectorMixin
from .versions import PYSIDE, PYSIDE_MODULE, SHIBOKEN
from .wheel_utils import (get_package_version,
                          get_package_timestamp, macos_plat_name,
                          macos_pyside_min_deployment_target)

import setuptools  # Import setuptools before distutils
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

# PYSIDE-1760: Although not used here, pre-load this module early to avoid
#              a racing condition with the import order. Note that this problem
#              happens only with custom builds of Python without virtual environment.
import setuptools.command.install_scripts

# Use the distutils implementation within setuptools
from setuptools._distutils.errors import DistutilsSetupError
from setuptools._distutils import log
from setuptools._distutils import sysconfig as sconfig
from setuptools._distutils.command.build import build as _build

from shutil import which
from setuptools.command.build_ext import build_ext as _build_ext
from setuptools import Command

from .qtinfo import QtInfo
from .utils import (rmtree, detect_clang, copyfile, copydir, run_process_output, run_process,
                    update_env_path, init_msvc_env, filter_match, macos_fix_rpaths_for_library,
                    linux_fix_rpaths_for_library, platform_cmake_options)
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


setup_script_dir = os.getcwd()
build_scripts_dir = os.path.join(setup_script_dir, 'build_scripts')
setup_py_path = os.path.join(setup_script_dir, "setup.py")

start_time = int(time.time())


def elapsed():
    return int(time.time()) - start_time


def get_setuptools_extension_modules():
    # Setting py_limited_api on the extension is the "correct" thing
    # to do, but it doesn't actually do anything, because we
    # override build_ext. So this is just foolproofing for the
    # future.
    extension_args = ('QtCore', [])
    extension_kwargs = {}
    if OPTION["LIMITED_API"] == 'yes':
        extension_kwargs['py_limited_api'] = True
    extension_modules = [Extension(*extension_args, **extension_kwargs)]
    return extension_modules


def _get_make(platform_arch, build_type):
    """Helper for retrieving the make command and CMake generator name"""
    makespec = OPTION["MAKESPEC"]
    if makespec == "make":
        return ("make", "Unix Makefiles")
    if makespec == "msvc":
        nmake_path = which("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 = which("nmake")
            if not nmake_path or not os.path.exists(nmake_path):
                raise DistutilsSetupError('"nmake" could not be found.')
        if not OPTION["NO_JOM"]:
            jom_path = which("jom")
            if jom_path:
                log.info(f"jom was found in {jom_path}")
                return (jom_path, "NMake Makefiles JOM")
        log.info(f"nmake was found in {nmake_path}")
        if OPTION["JOBS"]:
            msg = "Option --jobs can only be used with 'jom' on Windows."
            raise DistutilsSetupError(msg)
        return (nmake_path, "NMake Makefiles")
    if makespec == "mingw":
        return ("mingw32-make", "mingw32-make")
    if makespec == "ninja":
        return ("ninja", "Ninja")
    raise DistutilsSetupError(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 os.path.isabs(make_path):
        found_path = which(make_path)
        if not found_path or not os.path.exists(found_path):
            m = f"You need the program '{make_path}' on your system path to compile {PYSIDE_MODULE}."
            raise DistutilsSetupError(m)
        make_path = found_path
    return (make_path, make_generator)


def check_allowed_python_version():
    """
    Make sure that setup.py is run with an allowed python version.
    """

    pattern = r'Programming Language :: Python :: (\d+)\.(\d+)'
    supported = []

    for line in config.python_version_classifiers:
        found = re.search(pattern, line)
        if found:
            major = int(found.group(1))
            minor = int(found.group(2))
            supported.append((major, minor))
    this_py = sys.version_info[:2]
    if this_py not in supported:
        log.error(f"Unsupported python version detected. Supported versions: {supported}")
        sys.exit(1)


qt_src_dir = ''


def prepare_build():
    # locate Qt sources for the documentation
    if OPTION["QT_SRC"] is None:
        install_prefix = QtInfo().prefix_dir
        if install_prefix:
            global qt_src_dir
            # In-source, developer build
            if install_prefix.endswith("qtbase"):
                qt_src_dir = install_prefix
            else:  # SDK: Use 'Src' directory
                maybe_qt_src_dir = os.path.join(os.path.dirname(install_prefix), 'Src', 'qtbase')
                if os.path.exists(maybe_qt_src_dir):
                    qt_src_dir = maybe_qt_src_dir


class PysideInstall(_install, DistUtilsCommandMixin):

    user_options = _install.user_options + DistUtilsCommandMixin.mixin_user_options

    def __init__(self, *args, **kwargs):
        _install.__init__(self, *args, **kwargs)
        DistUtilsCommandMixin.__init__(self)

    def initialize_options(self):
        _install.initialize_options(self)

        if sys.platform == 'darwin':
            # Because we change the plat_name to include a correct
            # deployment target on macOS distutils thinks we are
            # cross-compiling, and throws an exception when trying to
            # execute setup.py install. The check looks like this
            # if self.warn_dir and build_plat != get_platform():
            #   raise DistutilsPlatformError("Can't install when "
            #                                  "cross-compiling")
            # Obviously get_platform will return the old deployment
            # target. The fix is to disable the warn_dir flag, which
            # was created for bdist_* derived classes to override, for
            # similar cases.
            self.warn_dir = False

    def finalize_options(self):
        DistUtilsCommandMixin.mixin_finalize_options(self)
        _install.finalize_options(self)

    def run(self):
        _install.run(self)
        log.info(f"--- Install completed ({elapsed()}s)")


class PysideDevelop(_develop):

    def __init__(self, *args, **kwargs):
        _develop.__init__(self, *args, **kwargs)

    def run(self):
        self.run_command("build")
        _develop.run(self)


class PysideBdistEgg(_bdist_egg):

    def __init__(self, *args, **kwargs):
        _bdist_egg.__init__(self, *args, **kwargs)

    def run(self):
        self.run_command("build")
        _bdist_egg.run(self)


class PysideBuildExt(_build_ext):

    def __init__(self, *args, **kwargs):
        _build_ext.__init__(self, *args, **kwargs)

    def run(self):
        pass


class PysideBuildPy(_build_py):

    def __init__(self, *args, **kwargs):
        _build_py.__init__(self, *args, **kwargs)


# _install_lib is reimplemented to preserve
# symlinks when distutils / setuptools copy files to various
# directories from the setup tools build dir to the install dir.
class PysideInstallLib(_install_lib):

    def __init__(self, *args, **kwargs):
        _install_lib.__init__(self, *args, **kwargs)

    def install(self):
        """
        Installs files from 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):
            # Using our own copydir makes sure to preserve symlinks.
            outfiles = copydir(os.path.abspath(self.build_dir), os.path.abspath(self.install_dir))
        else:
            self.warn(f"'{self.build_dir}' does not exist -- no Python modules to install")
            return
        return outfiles


class PysideBuild(_build, DistUtilsCommandMixin, BuildInfoCollectorMixin):

    user_options = _build.user_options + DistUtilsCommandMixin.mixin_user_options

    def __init__(self, *args, **kwargs):
        _build.__init__(self, *args, **kwargs)
        DistUtilsCommandMixin.__init__(self)
        BuildInfoCollectorMixin.__init__(self)

    def finalize_options(self):
        os_name_backup = os.name
        DistUtilsCommandMixin.mixin_finalize_options(self)
        BuildInfoCollectorMixin.collect_and_assign(self)

        if sys.platform == 'darwin':
            self.plat_name = macos_plat_name()
            # This is a hack to circumvent the dubious check in
            # distutils.commands.build -> finalize_options, which only
            # allows setting the plat_name for windows NT.
            # That is not the case for the wheel module though (which
            # does allow setting plat_name), so we circumvent by faking
            # the os name when finalizing the options, and then
            # restoring the original os name.
            os.name = "nt"

        _build.finalize_options(self)

        # Must come after _build.finalize_options
        BuildInfoCollectorMixin.post_collect_and_assign(self)

        if sys.platform == 'darwin':
            os.name = os_name_backup

    def initialize_options(self):
        _build.initialize_options(self)
        self.make_path = None
        self.make_generator = None
        self.script_dir = None
        self.sources_dir = None
        self.build_dir = None
        self.install_dir = None
        self.py_executable = None
        self.py_include_dir = None
        self.py_library = None
        self.py_version = None
        self.py_arch = None
        self.build_type = "Release"
        self.qtinfo = None
        self.build_tests = False

    def run(self):
        prepare_build()

        # Check env
        make_path = None
        make_generator = None
        if not OPTION["ONLYPACKAGE"]:
            platform_arch = platform.architecture()[0]
            (make_path, make_generator) = get_make(platform_arch, self.build_type)

        self.qtinfo = QtInfo()
        # Update the PATH environment variable
        # 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.
        additional_paths = [self.py_scripts_dir]

        # Add Clang to path for Windows.
        # Revisit once Clang is bundled with Qt.
        if (sys.platform == "win32"
                and parse_version(self.qtinfo.version) >= parse_version("5.7.0")):
            clang_dir, clang_source = detect_clang()
            if clang_dir:
                clangBinDir = os.path.join(clang_dir, 'bin')
                if 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")

        update_env_path(additional_paths)

        self.make_path = make_path
        self.make_generator = make_generator

        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, SHIBOKEN)

        self.log_pre_build_info()

        # Prepare folders
        if not os.path.exists(self.sources_dir):
            log.info(f"Creating sources folder {self.sources_dir}...")
            os.makedirs(self.sources_dir)
        if not os.path.exists(self.build_dir):
            log.info(f"Creating build folder {self.build_dir}...")
            os.makedirs(self.build_dir)
        if not os.path.exists(self.install_dir):
            log.info(f"Creating install folder {self.install_dir}...")
            os.makedirs(self.install_dir)

        if (not OPTION["ONLYPACKAGE"]
                and not config.is_internal_shiboken_generator_build_and_part_of_top_level_all()):
            # Build extensions
            for ext in config.get_buildable_extensions():
                self.build_extension(ext)

            if OPTION["BUILDTESTS"]:
                # we record the latest successful build and note the
                # build directory for supporting the tests.
                timestamp = time.strftime('%Y-%m-%d_%H%M%S')
                build_history = os.path.join(setup_script_dir, 'build_history')
                unique_dir = os.path.join(build_history, timestamp)
                os.makedirs(unique_dir)
                fpath = os.path.join(unique_dir, 'build_dir.txt')
                with open(fpath, 'w') as f:
                    print(self.build_dir, file=f)
                    print(self.build_classifiers, file=f)
                log.info(f"Created {build_history}")

        if not OPTION["SKIP_PACKAGING"]:
            # Build patchelf if needed
            self.build_patchelf()

            # Prepare packages
            self.prepare_packages()

            # Build packages
            _build.run(self)
        else:
            log.info("Skipped preparing and building packages.")
        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 = sconfig.get_python_lib(1)
        if OPTION["FINAL_INSTALL_PREFIX"]:
            setuptools_install_prefix = OPTION["FINAL_INSTALL_PREFIX"]
        log.info("=" * 30)
        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(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(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 ->
            make install directory ->
            setuptools build directory ->
            setuptools install directory
              (usually path-installed-python/lib/python*/site-packages/*)
         """))
        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'
         """))
        log.info("-" * 3)
        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("-" * 3)
        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(f"OpenSSL dll directory: {OPTION['OPENSSL']}")
        if sys.platform == 'darwin':
            pyside_macos_deployment_target = (macos_pyside_min_deployment_target())
            log.info(f"MACOSX_DEPLOYMENT_TARGET set to: {pyside_macos_deployment_target}")
        log.info("=" * 30)

    def build_patchelf(self):
        if not sys.platform.startswith('linux'):
            return
        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(f"Using {self._patchelf_path} ...")
            return
        log.info("Building patchelf...")
        module_src_dir = os.path.join(self.sources_dir, "patchelf")
        build_cmd = ["g++", f"{module_src_dir}/patchelf.cc", "-o", "patchelf"]
        if run_process(build_cmd) != 0:
            raise DistutilsSetupError("Error building patchelf")
        self._patchelf_path = os.path.join(self.script_dir, "patchelf")

    def build_extension(self, extension):
        # calculate the subrepos folder name

        log.info(f"Building module {extension}...")

        # Prepare folders
        os.chdir(self.build_dir)
        module_build_dir = os.path.join(self.build_dir, extension)
        skipflag_file = f"{module_build_dir} -skip"
        if os.path.exists(skipflag_file):
            log.info(f"Skipping {extension} because {skipflag_file} exists")
            return

        module_build_exists = os.path.exists(module_build_dir)
        if module_build_exists:
            if not OPTION["REUSE_BUILD"]:
                log.info(f"Deleting module build folder {module_build_dir}...")
                try:
                    rmtree(module_build_dir)
                except Exception as e:
                    log.error(f'***** problem removing "{module_build_dir}"')
                    log.error(f'ignored error: {e}')
            else:
                log.info(f"Reusing module build folder {module_build_dir}...")
        if not os.path.exists(module_build_dir):
            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)

        # Build module
        cmake_cmd = [OPTION["CMAKE"]]
        if OPTION["QUIET"]:
            # Pass a special custom option, to allow printing a lot less information when doing
            # a quiet build.
            cmake_cmd.append('-DQUIET_BUILD=1')
            if self.make_generator == "Unix Makefiles":
                # Hide progress messages for each built source file.
                # Doesn't seem to work if set within the cmake files themselves.
                cmake_cmd.append('-DCMAKE_RULE_MESSAGES=0')

        cmake_cmd += [
            "-G", self.make_generator,
            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}",
            module_src_dir
        ]
        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():
            config_dir = OPTION["SHIBOKEN_CONFIG_DIR"]
            if os.path.exists(config_dir):
                log.info(f"Using custom provided {SHIBOKEN} installation: {config_dir}")
                cmake_cmd.append(f"-DShiboken6_DIR={config_dir}")
            else:

                log.info(f"Custom provided {SHIBOKEN} installation not found. Path given: {config_dir}")

        if OPTION["MODULE_SUBSET"]:
            module_sub_set = ''
            for m in OPTION["MODULE_SUBSET"].split(','):
                if m.startswith('Qt'):
                    m = m[2:]
                if module_sub_set:
                    module_sub_set += ';'
                module_sub_set += m
            cmake_cmd.append(f"-DMODULES={module_sub_set}")
        if OPTION["SKIP_MODULES"]:
            skip_modules = ''
            for m in OPTION["SKIP_MODULES"].split(','):
                if m.startswith('Qt'):
                    m = m[2:]
                if skip_modules:
                    skip_modules += ';'
                skip_modules += m
            cmake_cmd.append(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
        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']:
            cmake_cmd.append("-DSKIP_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")

        numpy = get_numpy_location()
        if numpy:
            cmake_cmd.append(f"-DNUMPY_INCLUDE_DIR={numpy}")

        if self.build_type.lower() == 'debug':
            cmake_cmd.append(f"-DPYTHON_DEBUG_LIBRARY={self.py_library}")

        if OPTION["LIMITED_API"] == "yes":
            cmake_cmd.append("-DFORCE_LIMITED_API=yes")
        elif OPTION["LIMITED_API"] == "no":
            cmake_cmd.append("-DFORCE_LIMITED_API=no")
        elif not OPTION["LIMITED_API"]:
            pass
        else:
            raise DistutilsSetupError("option limited-api must be 'yes' or 'no' "
                                      "(default yes if applicable, i.e. python version >= 3.6)")

        if OPTION["VERBOSE_BUILD"]:
            cmake_cmd.append("-DCMAKE_VERBOSE_MAKEFILE:BOOL=ON")

        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.
            if (sys.platform.startswith('linux')
                    or sys.platform.startswith('darwin')):
                cmake_cmd.append("-DSANITIZE_ADDRESS=ON")
            else:
                raise DistutilsSetupError("Address sanitizer can only be used on Linux and macOS.")

        if extension.lower() == PYSIDE:
            pyside_qt_conf_prefix = ''
            if OPTION["QT_CONF_PREFIX"]:
                pyside_qt_conf_prefix = OPTION["QT_CONF_PREFIX"]
            else:
                if OPTION["STANDALONE"]:
                    pyside_qt_conf_prefix = '"Qt"'
                if sys.platform == 'win32':
                    pyside_qt_conf_prefix = '"."'
            cmake_cmd.append(f"-DPYSIDE_QT_CONF_PREFIX={pyside_qt_conf_prefix}")

        # Pass package version to CMake, so this string can be
        # embedded into _config.py file.
        package_version = get_package_version()
        cmake_cmd.append(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
        # version component that is actually generated by setup.py.
        timestamp = ''
        if OPTION["SNAPSHOT_BUILD"]:
            timestamp = get_package_timestamp()
        cmake_cmd.append(f"-DPACKAGE_SETUP_PY_PACKAGE_TIMESTAMP={timestamp}")

        if extension.lower() in [SHIBOKEN]:
            cmake_cmd.append("-DCMAKE_INSTALL_RPATH_USE_LINK_PATH=yes")
            cmake_cmd.append("-DUSE_PYTHON_VERSION=3.6")

        cmake_cmd += platform_cmake_options()

        if sys.platform == 'darwin':
            if OPTION["MACOS_ARCH"]:
                # also tell cmake which architecture to use
                cmake_cmd.append(f"-DCMAKE_OSX_ARCHITECTURES:STRING={OPTION['MACOS_ARCH']}")

            if OPTION["MACOS_USE_LIBCPP"]:
                # Explicitly link the libc++ standard library (useful
                # for macOS deployment targets lower than 10.9).
                # This is not on by default, because most libraries and
                # executables on macOS <= 10.8 are linked to libstdc++,
                # and mixing standard libraries can lead to crashes.
                # On macOS >= 10.9 with a similar minimum deployment
                # target, libc++ is linked in implicitly, thus the
                # option is a no-op in those cases.
                cmake_cmd.append("-DOSX_USE_LIBCPP=ON")

            if OPTION["MACOS_SYSROOT"]:
                cmake_cmd.append(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(f"-DCMAKE_OSX_SYSROOT={latest_sdk_path}")

            # Set macOS minimum deployment target (version).
            # This is required so that calling
            #   run_process -> distutils.spawn()
            # does not set its own minimum deployment target
            # environment variable which is based on the python
            # interpreter sysconfig value.
            # Doing so could break the detected clang include paths
            # for example.
            deployment_target = macos_pyside_min_deployment_target()
            cmake_cmd.append(f"-DCMAKE_OSX_DEPLOYMENT_TARGET={deployment_target}")
            os.environ['MACOSX_DEPLOYMENT_TARGET'] = deployment_target

        if not OPTION["SKIP_DOCS"]:
            # Build the whole documentation (rst + 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("-DSKIP_DOCS=1")

        if OPTION["PYSIDE_NUMPY_SUPPORT"]:
            cmake_cmd.append("-DPYSIDE_NUMPY_SUPPORT=1")

        target_qt_prefix_path = self.qtinfo.prefix_dir
        cmake_cmd.append(f"-DCMAKE_PREFIX_PATH={target_qt_prefix_path}")

        if not OPTION["SKIP_CMAKE"]:
            log.info(f"Configuring module {extension} ({module_src_dir})...")
            if run_process(cmake_cmd) != 0:
                raise DistutilsSetupError(f"Error configuring {extension}")
        else:
            log.info(f"Reusing old configuration for module {extension} ({module_src_dir})...")

        log.info(f"-- Compiling module {extension}...")
        cmd_make = [self.make_path]
        if OPTION["JOBS"]:
            cmd_make.append(OPTION["JOBS"])
        if OPTION["VERBOSE_BUILD"] and self.make_generator == "Ninja":
            cmd_make.append("-v")
        if run_process(cmd_make) != 0:
            raise DistutilsSetupError(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 not OPTION["SKIP_DOCS"]:
            if extension.lower() == SHIBOKEN:
                found = importlib.util.find_spec("sphinx")
                if found:
                    log.info("Generating Shiboken documentation")
                    make_doc_cmd = [self.make_path, "doc"]
                    if OPTION["VERBOSE_BUILD"] and self.make_generator == "Ninja":
                        make_doc_cmd.append("-v")
                    if run_process(make_doc_cmd) != 0:
                        raise DistutilsSetupError("Error generating documentation "
                                                  f"for {extension}")
                else:
                    log.info("Sphinx not found, skipping documentation build")
        else:
            log.info("Skipped documentation generation")
            cmake_cmd.append("-DSKIP_DOCS=1")

        if not OPTION["SKIP_MAKE_INSTALL"]:
            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
            # for issue details.
            if sys.platform == 'darwin':
                log.info("Waiting 1 second, to ensure installation is successful...")
                time.sleep(1)
            # ninja: error: unknown target 'install/fast'
            target = 'install/fast' if self.make_generator != 'Ninja' else 'install'
            if run_process([self.make_path, target]) != 0:
                raise DistutilsSetupError(f"Error pseudo installing {extension}")
        else:
            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).

        After that setuptools.command.build_py is smart enough to copy everything
        from the build dir to the install dir (the virtualenv site-packages for example).
        """
        try:
            log.info("\nPreparing setup tools build directory.\n")
            vars = {
                "site_packages_dir": self.site_packages_dir,
                "sources_dir": self.sources_dir,
                "install_dir": self.install_dir,
                "build_dir": self.build_dir,
                "script_dir": self.script_dir,
                "st_build_dir": self.st_build_dir,
                "cmake_package_name": config.package_name(),
                "st_package_name": config.package_name(),
                "ssl_libs_dir": OPTION["OPENSSL"],
                "py_version": self.py_version,
                "qt_version": self.qtinfo.version,
                "qt_bin_dir": self.qtinfo.bins_dir,
                "qt_data_dir": self.qtinfo.data_dir,
                "qt_doc_dir": self.qtinfo.docs_dir,
                "qt_lib_dir": self.qtinfo.libs_dir,
                "qt_lib_execs_dir": self.qtinfo.lib_execs_dir,
                "qt_plugins_dir": self.qtinfo.plugins_dir,
                "qt_prefix_dir": self.qtinfo.prefix_dir,
                "qt_translations_dir": self.qtinfo.translations_dir,
                "qt_qml_dir": self.qtinfo.qml_dir,
                "target_arch": self.py_arch,
            }

            # Needed for correct file installation in generator build
            # case.
            if config.is_internal_shiboken_generator_build():
                vars['cmake_package_name'] = config.shiboken_module_option_name

            os.chdir(self.script_dir)

            # 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 os.path.isdir(self.st_build_dir):
                log.info(f"Removing {self.st_build_dir}")
                try:
                    rmtree(self.st_build_dir)
                except Exception as e:
                    log.warn(f'***** problem removing "{self.st_build_dir}"')
                    log.warn(f'ignored error: {e}')

            if sys.platform == "win32":
                vars['dbg_postfix'] = OPTION["DEBUG"] and "_d" or ""
                return prepare_packages_win32(self, vars)
            else:
                return prepare_packages_posix(self, vars)
        except IOError as e:
            print('setup.py/prepare_packages: ', e)
            raise

    def qt_is_framework_build(self):
        if os.path.isdir(self.qtinfo.headers_dir + "/../lib/QtCore.framework"):
            return True
        return False

    def get_built_pyside_config(self, vars):
        # Get config that contains list of built modules, and
        # SOVERSIONs of the built libraries.
        st_build_dir = vars['st_build_dir']
        config_path = os.path.join(st_build_dir, config.package_name(), "_config.py")
        temp_config = get_python_dict(config_path)
        if 'built_modules' not in temp_config:
            temp_config['built_modules'] = []
        return temp_config

    def is_webengine_built(self, built_modules):
        return ('WebEngineWidgets' in built_modules
                or 'WebEngineCore' in built_modules
                or 'WebEngine' in built_modules)

    def prepare_standalone_clang(self, is_win=False):
        """
        Copies the libclang library to the shiboken6-generator
        package so that the shiboken executable works.
        """
        log.info('Finding path to the libclang shared library.')
        cmake_cmd = [
            OPTION["CMAKE"],
            "-L",         # Lists variables
            "-N",         # Just inspects the cache (faster)
            "-B",         # Specifies the build dir
            self.shiboken_build_dir
        ]
        out = run_process_output(cmake_cmd)
        lines = [s.strip() for s in out]
        pattern = re.compile(r"CLANG_LIBRARY:FILEPATH=(.+)$")

        clang_lib_path = None
        for line in lines:
            match = pattern.search(line)
            if match:
                clang_lib_path = match.group(1)
                break

        if not clang_lib_path:
            raise RuntimeError("Could not find the location of the libclang "
                               "library inside the CMake cache file.")

        if is_win:
            # clang_lib_path points to the static import library
            # (lib/libclang.lib), whereas we want to copy the shared
            # library (bin/libclang.dll).
            clang_lib_path = re.sub(r'lib/libclang.lib$',
                                    'bin/libclang.dll',
                                    clang_lib_path)
        else:
            # 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
            # file) on Linux and macOS,
            # so that we get the path to the "SO version" symlink
            # (the one used as the install name in the shared library
            # dependency section).
            # E.g. On Linux libclang.so -> libclang.so.6 ->
            # libclang.so.6.0.
            # "libclang.so.6" is the name we want for the copied file.
            if os.path.islink(clang_lib_path):
                link_target = os.readlink(clang_lib_path)
                if os.path.isabs(link_target):
                    clang_lib_path = link_target
                else:
                    # link_target is relative, transform to absolute.
                    clang_lib_path = os.path.join(os.path.dirname(clang_lib_path), link_target)
            clang_lib_path = os.path.abspath(clang_lib_path)

        # The destination will be the shiboken package folder.
        vars = {}
        vars['st_build_dir'] = self.st_build_dir
        vars['st_package_name'] = config.package_name()
        destination_dir = "{st_build_dir}/{st_package_name}".format(**vars)

        if os.path.exists(clang_lib_path):
            basename = os.path.basename(clang_lib_path)
            log.info(f"Copying libclang shared library {clang_lib_path} to the package "
                     f"folder as {basename}.")
            destination_path = os.path.join(destination_dir, basename)

            # Need to modify permissions in case file is not writable
            # (a reinstall would cause a permission denied error).
            copyfile(clang_lib_path,
                     destination_path,
                     force_copy_symlink=True,
                     make_writable_by_owner=True)
        else:
            raise RuntimeError("Error copying libclang library "
                               f"from {clang_lib_path} to {destination_dir}. ")

    def package_libraries(self, package_path):
        """Returns the libraries of the Python module"""
        UNIX_FILTERS = ["*.so", "*.so.*"]
        DARWIN_FILTERS = ["*.so", "*.dylib"]
        FILTERS = DARWIN_FILTERS if sys.platform == 'darwin' else UNIX_FILTERS
        return [lib for lib in os.listdir(
            package_path) if filter_match(lib, FILTERS)]

    def update_rpath(self, package_path, executables, libexec=False):
        ROOT = '@loader_path' if sys.platform == 'darwin' else '$ORIGIN'
        QT_PATH = '/../lib' if libexec else '/Qt/lib'

        if sys.platform.startswith('linux'):
            def rpath_cmd(srcpath):
                final_rpath = ''
                # Command line rpath option takes precedence over
                # automatically added one.
                if OPTION["RPATH_VALUES"]:
                    final_rpath = OPTION["RPATH_VALUES"]
                else:
                    # Add rpath values pointing to $ORIGIN and the
                    # installed qt lib directory.
                    final_rpath = self.qtinfo.libs_dir
                    if OPTION["STANDALONE"]:
                        final_rpath = f'{ROOT}{QT_PATH}'
                override = OPTION["STANDALONE"]
                linux_fix_rpaths_for_library(self._patchelf_path, srcpath, final_rpath,
                                             override=override)

        elif sys.platform == 'darwin':
            def rpath_cmd(srcpath):
                final_rpath = ''
                # Command line rpath option takes precedence over
                # automatically added one.
                if OPTION["RPATH_VALUES"]:
                    final_rpath = OPTION["RPATH_VALUES"]
                else:
                    if OPTION["STANDALONE"]:
                        final_rpath = f'{ROOT}{QT_PATH}'
                    else:
                        final_rpath = self.qtinfo.libs_dir
                macos_fix_rpaths_for_library(srcpath, final_rpath)

        else:
            raise RuntimeError(f"Not configured for platform {sys.platform}")

        # Update rpath
        for srcname in executables:
            srcpath = os.path.join(package_path, srcname)
            if os.path.isdir(srcpath) or os.path.islink(srcpath):
                continue
            if not os.path.exists(srcpath):
                continue
            rpath_cmd(srcpath)
            log.info("Patched rpath to '$ORIGIN/' (Linux) or "
                     f"updated rpath (OS/X) in {srcpath}.")


class PysideRstDocs(Command, DistUtilsCommandMixin):
    description = "Build .rst documentation only"
    user_options = DistUtilsCommandMixin.mixin_user_options

    def initialize_options(self):
        DistUtilsCommandMixin.__init__(self)
        log.info("-- This build process will not include the API documentation."
                 "API documentation requires a full build of pyside/shiboken.")
        self.skip = False
        if config.is_internal_shiboken_generator_build():
            self.skip = True
        if not self.skip:
            self.name = config.package_name().lower()
            self.doc_dir = os.path.join(config.setup_script_dir, "sources")
            self.doc_dir = os.path.join(self.doc_dir, self.name)
            self.doc_dir = os.path.join(self.doc_dir, "doc")
            # Check if sphinx is installed to proceed.
            found = importlib.util.find_spec("sphinx")
            if found:
                if self.name == SHIBOKEN:
                    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 DistutilsSetupError("Sphinx not found - aborting")
            self.html_dir = "html"

            # creating directories html/pyside6/shiboken6
            try:
                if not os.path.isdir(self.html_dir):
                    os.mkdir(self.html_dir)
                if self.name == SHIBOKEN:
                    out_pyside = os.path.join(self.html_dir, PYSIDE)
                    if not os.path.isdir(out_pyside):
                        os.mkdir(out_pyside)
                    out_shiboken = os.path.join(out_pyside, SHIBOKEN)
                    if not os.path.isdir(out_shiboken):
                        os.mkdir(out_shiboken)
                    self.out_dir = out_shiboken
                # We know that on the shiboken step, we already created the
                # 'pyside6' directory
                elif self.name == PYSIDE:
                    self.out_dir = os.path.join(self.html_dir, PYSIDE)
            except (PermissionError, FileExistsError):
                raise DistutilsSetupError(f"Error while creating directories for {self.doc_dir}")

    def run(self):
        if not self.skip:
            cmake_cmd = [
                OPTION["CMAKE"],
                "-S", self.doc_dir,
                "-B", self.out_dir,
                "-DDOC_OUTPUT_FORMAT=html",
                "-DFULLDOCSBUILD=0",
            ]
            if OPTION["QUIET"]:
                cmake_cmd.append('-DQUIET_BUILD=1')
            if run_process(cmake_cmd) != 0:
                raise DistutilsSetupError(f"Error running CMake for {self.doc_dir}")

            if self.name == PYSIDE:
                self.sphinx_src = os.path.join(self.out_dir, "rst")
            elif self.name == SHIBOKEN:
                self.sphinx_src = self.out_dir

            sphinx_cmd = ["sphinx-build", "-b", "html", "-j", "auto", "-c", self.sphinx_src,
                          self.doc_dir, self.out_dir]
            if run_process(sphinx_cmd) != 0:
                raise DistutilsSetupError(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):
        DistUtilsCommandMixin.mixin_finalize_options(self)


cmd_class_dict = {
    'build': PysideBuild,
    'build_py': PysideBuildPy,
    'build_ext': PysideBuildExt,
    'bdist_egg': PysideBdistEgg,
    'develop': PysideDevelop,
    'install': PysideInstall,
    'install_lib': PysideInstallLib,
    'build_rst_docs': PysideRstDocs,
}
if wheel_module_exists:
    pyside_bdist_wheel = get_bdist_wheel_override()
    if pyside_bdist_wheel:
        cmd_class_dict['bdist_wheel'] = pyside_bdist_wheel
