Exemple #1
0
def setup(*args, **kw):  # noqa: C901
    """This function wraps setup() so that we can run cmake, make,
    CMake build, then proceed as usual with setuptools, appending the
    CMake-generated output as necessary.
    """
    sys.argv, cmake_args, make_args = parse_args()

    # work around https://bugs.python.org/issue1011113
    # (patches provided, but no updates since 2014)
    cmdclass = kw.get('cmdclass', {})
    cmdclass['build'] = cmdclass.get('build', build.build)
    cmdclass['build_py'] = cmdclass.get('build_py', build_py.build_py)
    cmdclass['install'] = cmdclass.get('install', install.install)
    cmdclass['install_lib'] = cmdclass.get('install_lib',
                                           install_lib.install_lib)
    cmdclass['install_scripts'] = cmdclass.get('install_scripts',
                                               install_scripts.install_scripts)
    cmdclass['clean'] = cmdclass.get('clean', clean.clean)
    cmdclass['sdist'] = cmdclass.get('sdist', sdist.sdist)
    cmdclass['bdist'] = cmdclass.get('bdist', bdist.bdist)
    cmdclass['bdist_wheel'] = cmdclass.get('bdist_wheel',
                                           bdist_wheel.bdist_wheel)
    cmdclass['egg_info'] = cmdclass.get('egg_info', egg_info.egg_info)
    cmdclass['generate_source_manifest'] = cmdclass.get(
        'generate_source_manifest',
        generate_source_manifest.generate_source_manifest)
    kw['cmdclass'] = cmdclass

    # Extract setup keywords specific to scikit-build and remove them from kw.
    # Removing the keyword from kw need to be done here otherwise, the
    # following call to _parse_setuptools_arguments would complain about
    # unknown setup options.
    parameters = {
        'cmake_args': [],
        'cmake_install_dir': '',
        'cmake_source_dir': '',
        'cmake_with_sdist': False
    }
    skbuild_kw = {
        param: kw.pop(param, parameters[param])
        for param in parameters
    }

    # ... and validate them
    try:
        _check_skbuild_parameters(skbuild_kw)
    except SKBuildError as ex:
        import traceback
        print("Traceback (most recent call last):")
        traceback.print_tb(sys.exc_info()[2])
        print('')
        sys.exit(ex)

    # Convert source dir to a path relative to the root
    # of the project
    cmake_source_dir = skbuild_kw['cmake_source_dir']
    if cmake_source_dir == ".":
        cmake_source_dir = ""
    if os.path.isabs(cmake_source_dir):
        cmake_source_dir = os.path.relpath(cmake_source_dir)

    # Skip running CMake in the following cases:
    # * flag "--skip-cmake" is provided
    # * "display only" argument is provided (e.g  '--help', '--author', ...)
    # * no command-line arguments or invalid ones are provided
    # * no command requiring cmake is provided
    # * no CMakeLists.txt if found
    display_only = has_invalid_arguments = help_commands = False
    force_cmake = skip_cmake = False
    commands = []
    try:
        (display_only, help_commands, commands,
         hide_listing, force_cmake, skip_cmake) = \
            _parse_setuptools_arguments(kw)
    except (DistutilsArgError, DistutilsGetoptError):
        has_invalid_arguments = True

    has_cmakelists = os.path.exists(
        os.path.join(cmake_source_dir, "CMakeLists.txt"))
    if not has_cmakelists:
        print('skipping skbuild (no CMakeLists.txt found)')

    skip_cmake = (
        skip_cmake or display_only or has_invalid_arguments
        or not _should_run_cmake(commands, skbuild_kw["cmake_with_sdist"])
        or not has_cmakelists)
    if skip_cmake and not force_cmake:
        if help_commands:
            # Prepend scikit-build help. Generate option descriptions using
            # argparse.
            skbuild_parser = create_skbuild_argparser()
            arg_descriptions = [
                line for line in skbuild_parser.format_help().split('\n')
                if line.startswith('  ')
            ]
            print('scikit-build options:')
            print('\n'.join(arg_descriptions))
            print('')
            print('Arguments following a "--" are passed directly to CMake '
                  '(e.g. -DMY_VAR:BOOL=TRUE).')
            print('Arguments following a second "--" are passed directly to '
                  ' the build tool.')
            print('')
        return upstream_setup(*args, **kw)

    developer_mode = "develop" in commands

    packages = kw.get('packages', [])
    package_dir = kw.get('package_dir', {})
    package_data = kw.get('package_data', {}).copy()

    py_modules = kw.get('py_modules', [])
    new_py_modules = {py_module: False for py_module in py_modules}

    scripts = kw.get('scripts', [])
    new_scripts = {script: False for script in scripts}

    data_files = {(parent_dir or '.'): set(file_list)
                  for parent_dir, file_list in kw.get('data_files', [])}

    # Since CMake arguments provided through the command line have more
    # weight and when CMake is given multiple times a argument, only the last
    # one is considered, let's prepend the one provided in the setup call.
    cmake_args = skbuild_kw['cmake_args'] + cmake_args

    try:
        cmkr = cmaker.CMaker()
        env = cmkr.configure(cmake_args,
                             cmake_source_dir=cmake_source_dir,
                             cmake_install_dir=skbuild_kw['cmake_install_dir'])
        cmkr.make(make_args, env=env)
    except SKBuildGeneratorNotFoundError as ex:
        sys.exit(ex)
    except SKBuildError as ex:
        import traceback
        print("Traceback (most recent call last):")
        traceback.print_tb(sys.exc_info()[2])
        print('')
        sys.exit(ex)

    # If needed, set reasonable defaults for package_dir
    for package in packages:
        if package not in package_dir:
            package_dir[package] = package.replace(".", os.path.sep)

    package_prefixes = _collect_package_prefixes(package_dir, packages)

    _classify_files(cmkr.install(), package_data, package_prefixes, py_modules,
                    new_py_modules, scripts, new_scripts, data_files,
                    cmake_source_dir, skbuild_kw['cmake_install_dir'])

    if developer_mode:
        for package, package_file_list in package_data.items():
            for package_file in package_file_list:
                package_file = os.path.join(package_dir[package], package_file)
                cmake_file = os.path.join(CMAKE_INSTALL_DIR, package_file)
                if os.path.exists(cmake_file):
                    _copy_file(cmake_file, package_file, hide_listing)
    else:
        _consolidate(cmake_source_dir, packages, package_dir, py_modules,
                     package_data, hide_listing)

    kw['package_data'] = package_data
    kw['package_dir'] = {
        package: (os.path.join(CMAKE_INSTALL_DIR, prefix) if os.path.exists(
            os.path.join(CMAKE_INSTALL_DIR, prefix)) else prefix)
        for prefix, package in package_prefixes
    }

    kw['py_modules'] = [
        os.path.join(CMAKE_INSTALL_DIR, py_module) if mask else py_module
        for py_module, mask in new_py_modules.items()
    ]

    kw['scripts'] = [
        os.path.join(CMAKE_INSTALL_DIR, script) if mask else script
        for script, mask in new_scripts.items()
    ]

    kw['data_files'] = [(parent_dir, list(file_set))
                        for parent_dir, file_set in data_files.items()]

    # Adapted from espdev/ITKPythonInstaller/setup.py.in
    # pylint: disable=missing-docstring
    class BinaryDistribution(upstream_Distribution):
        def has_ext_modules(self):  # pylint: disable=no-self-use
            return has_cmakelists

    kw['distclass'] = BinaryDistribution

    print("")

    return upstream_setup(*args, **kw)
def setup(*args, **kw):  # noqa: C901
    """This function wraps setup() so that we can run cmake, make,
    CMake build, then proceed as usual with setuptools, appending the
    CMake-generated output as necessary.

    The CMake project is re-configured only if needed. This is achieved by (1) retrieving the environment mapping
    associated with the generator set in the ``CMakeCache.txt`` file, (2) saving the CMake configure arguments and
    version in :func:`skbuild.constants.CMAKE_SPEC_FILE()`: and (3) re-configuring only if either the generator or
    the CMake specs change.
    """
    sys.argv, cmake_executable, skip_generator_test, cmake_args, make_args = parse_args()

    # work around https://bugs.python.org/issue1011113
    # (patches provided, but no updates since 2014)
    cmdclass = kw.get('cmdclass', {})
    cmdclass['build'] = cmdclass.get('build', build.build)
    cmdclass['build_py'] = cmdclass.get('build_py', build_py.build_py)
    cmdclass['build_ext'] = cmdclass.get('build_ext', build_ext.build_ext)
    cmdclass['install'] = cmdclass.get('install', install.install)
    cmdclass['install_lib'] = cmdclass.get('install_lib',
                                           install_lib.install_lib)
    cmdclass['install_scripts'] = cmdclass.get('install_scripts',
                                               install_scripts.install_scripts)
    cmdclass['clean'] = cmdclass.get('clean', clean.clean)
    cmdclass['sdist'] = cmdclass.get('sdist', sdist.sdist)
    cmdclass['bdist'] = cmdclass.get('bdist', bdist.bdist)
    cmdclass['bdist_wheel'] = cmdclass.get(
        'bdist_wheel', bdist_wheel.bdist_wheel)
    cmdclass['egg_info'] = cmdclass.get('egg_info', egg_info.egg_info)
    cmdclass['generate_source_manifest'] = cmdclass.get(
        'generate_source_manifest',
        generate_source_manifest.generate_source_manifest)
    cmdclass['test'] = cmdclass.get('test', test.test)
    kw['cmdclass'] = cmdclass

    # Extract setup keywords specific to scikit-build and remove them from kw.
    # Removing the keyword from kw need to be done here otherwise, the
    # following call to _parse_setuptools_arguments would complain about
    # unknown setup options.
    parameters = {
        'cmake_args': [],
        'cmake_install_dir': '',
        'cmake_source_dir': '',
        'cmake_with_sdist': False,
        'cmake_languages': ('C', 'CXX'),
        'cmake_minimum_required_version': None
    }
    skbuild_kw = {param: kw.pop(param, parameters[param])
                  for param in parameters}

    # ... and validate them
    try:
        _check_skbuild_parameters(skbuild_kw)
    except SKBuildError as ex:
        import traceback
        print("Traceback (most recent call last):")
        traceback.print_tb(sys.exc_info()[2])
        print('')
        sys.exit(ex)

    # Convert source dir to a path relative to the root
    # of the project
    cmake_source_dir = skbuild_kw['cmake_source_dir']
    if cmake_source_dir == ".":
        cmake_source_dir = ""
    if os.path.isabs(cmake_source_dir):
        cmake_source_dir = os.path.relpath(cmake_source_dir)

    # Skip running CMake in the following cases:
    # * flag "--skip-cmake" is provided
    # * "display only" argument is provided (e.g  '--help', '--author', ...)
    # * no command-line arguments or invalid ones are provided
    # * no command requiring cmake is provided
    # * no CMakeLists.txt if found
    display_only = has_invalid_arguments = help_commands = False
    force_cmake = skip_cmake = False
    commands = []
    try:
        (display_only, help_commands, commands,
         hide_listing, force_cmake, skip_cmake,
         plat_name, build_ext_inplace) = \
            _parse_setuptools_arguments(kw)
    except (DistutilsArgError, DistutilsGetoptError):
        has_invalid_arguments = True

    has_cmakelists = os.path.exists(
        os.path.join(cmake_source_dir, "CMakeLists.txt"))
    if not has_cmakelists:
        print('skipping skbuild (no CMakeLists.txt found)')

    skip_skbuild = (display_only
                    or has_invalid_arguments
                    or not _should_run_cmake(commands,
                                             skbuild_kw["cmake_with_sdist"])
                    or not has_cmakelists)
    if skip_skbuild and not force_cmake:
        if help_commands:
            # Prepend scikit-build help. Generate option descriptions using
            # argparse.
            skbuild_parser = create_skbuild_argparser()
            arg_descriptions = [
                line for line in skbuild_parser.format_help().split('\n')
                if line.startswith('  ')
                ]
            print('scikit-build options:')
            print('\n'.join(arg_descriptions))
            print('')
            print('Arguments following a "--" are passed directly to CMake '
                  '(e.g. -DMY_VAR:BOOL=TRUE).')
            print('Arguments following a second "--" are passed directly to '
                  ' the build tool.')
            print('')
        return upstream_setup(*args, **kw)

    developer_mode = "develop" in commands or "test" in commands or build_ext_inplace

    packages = kw.get('packages', [])
    package_dir = kw.get('package_dir', {})
    package_data = copy.deepcopy(kw.get('package_data', {}))

    py_modules = kw.get('py_modules', [])
    new_py_modules = {py_module: False for py_module in py_modules}

    scripts = kw.get('scripts', [])
    new_scripts = {script: False for script in scripts}

    data_files = {
        (parent_dir or '.'): set(file_list)
        for parent_dir, file_list in kw.get('data_files', [])
    }

    # Since CMake arguments provided through the command line have more
    # weight and when CMake is given multiple times a argument, only the last
    # one is considered, let's prepend the one provided in the setup call.
    cmake_args = skbuild_kw['cmake_args'] + cmake_args

    if sys.platform == 'darwin':

        # If no ``--plat-name`` argument was passed, set default value.
        if plat_name is None:
            plat_name = skbuild_plat_name()

        (_, version, machine) = plat_name.split('-')

        # The loop here allows for CMAKE_OSX_* command line arguments to overload
        # values passed with either the ``--plat-name`` command-line argument
        # or the ``cmake_args`` setup option.
        for cmake_arg in cmake_args:
            if 'CMAKE_OSX_DEPLOYMENT_TARGET' in cmake_arg:
                version = cmake_arg.split('=')[1]
            if 'CMAKE_OSX_ARCHITECTURES' in cmake_arg:
                machine = cmake_arg.split('=')[1]

        set_skbuild_plat_name("macosx-{}-{}".format(version, machine))

        # Set platform env. variable so that commands (e.g. bdist_wheel)
        # uses this information. The _PYTHON_HOST_PLATFORM env. variable is
        # used in distutils.util.get_platform() function.
        os.environ['_PYTHON_HOST_PLATFORM'] = skbuild_plat_name()

        # Set CMAKE_OSX_DEPLOYMENT_TARGET and CMAKE_OSX_ARCHITECTURES if not already
        # specified
        (_, version, machine) = skbuild_plat_name().split('-')
        if not cmaker.has_cmake_cache_arg(
                cmake_args, 'CMAKE_OSX_DEPLOYMENT_TARGET'):
            cmake_args.append(
                '-DCMAKE_OSX_DEPLOYMENT_TARGET:STRING=%s' % version
            )
        if not cmaker.has_cmake_cache_arg(
                cmake_args, 'CMAKE_OSX_ARCHITECTURES'):
            cmake_args.append(
                '-DCMAKE_OSX_ARCHITECTURES:STRING=%s' % machine
            )

    # Install cmake if listed in `setup_requires`
    for package in kw.get('setup_requires', []):
        if Requirement(package).name == 'cmake':
            setup_requires = [package]
            dist = upstream_Distribution({'setup_requires': setup_requires})
            dist.fetch_build_eggs(setup_requires)

            # Considering packages associated with "setup_requires" keyword are
            # installed in .eggs subdirectory without honoring setuptools "console_scripts"
            # entry_points and without settings the expected executable permissions, we are
            # taking care of it below.
            import cmake
            for executable in ['cmake', 'cpack', 'ctest']:
                executable = os.path.join(cmake.CMAKE_BIN_DIR, executable)
                if platform.system().lower() == 'windows':
                    executable += '.exe'
                st = os.stat(executable)
                permissions = (
                        st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
                )
                os.chmod(executable, permissions)
            cmake_executable = os.path.join(cmake.CMAKE_BIN_DIR, 'cmake')
            break

    # Languages are used to determine a working generator
    cmake_languages = skbuild_kw['cmake_languages']

    try:
        if cmake_executable is None:
            cmake_executable = CMAKE_DEFAULT_EXECUTABLE
        cmkr = cmaker.CMaker(cmake_executable)
        if not skip_cmake:
            cmake_minimum_required_version = skbuild_kw['cmake_minimum_required_version']
            if cmake_minimum_required_version is not None:
                if parse_version(cmkr.cmake_version) < parse_version(cmake_minimum_required_version):
                    raise SKBuildError(
                        "CMake version %s or higher is required. CMake version %s is being used" % (
                            cmake_minimum_required_version, cmkr.cmake_version))
            # Used to confirm that the cmake executable is the same, and that the environment
            # didn't change
            cmake_spec = {
                'args': [which(CMAKE_DEFAULT_EXECUTABLE)] + cmake_args,
                'version': cmkr.cmake_version,
                'environment': {
                    'PYTHONNOUSERSITE': os.environ.get("PYTHONNOUSERSITE"),
                    'PYTHONPATH': os.environ.get("PYTHONPATH")
                }
            }

            # skip the configure step for a cached build
            env = cmkr.get_cached_generator_env()
            if env is None or cmake_spec != _load_cmake_spec():
                env = cmkr.configure(cmake_args,
                                     skip_generator_test=skip_generator_test,
                                     cmake_source_dir=cmake_source_dir,
                                     cmake_install_dir=skbuild_kw['cmake_install_dir'],
                                     languages=cmake_languages
                                     )
                _save_cmake_spec(cmake_spec)
            cmkr.make(make_args, env=env)
    except SKBuildGeneratorNotFoundError as ex:
        sys.exit(ex)
    except SKBuildError as ex:
        import traceback
        print("Traceback (most recent call last):")
        traceback.print_tb(sys.exc_info()[2])
        print('')
        sys.exit(ex)

    # If any, strip ending slash from each package directory
    package_dir = {package: prefix[:-1] if prefix[-1] == "/" else prefix
                   for package, prefix in package_dir.items()}

    # If needed, set reasonable defaults for package_dir
    for package in packages:
        if package not in package_dir:
            package_dir[package] = package.replace(".", "/")
            if '' in package_dir:
                package_dir[package] = to_unix_path(os.path.join(package_dir[''], package_dir[package]))

    package_prefixes = _collect_package_prefixes(package_dir, packages)

    _classify_installed_files(cmkr.install(), package_data, package_prefixes,
                              py_modules, new_py_modules,
                              scripts, new_scripts,
                              data_files,
                              cmake_source_dir, skbuild_kw['cmake_install_dir'])

    original_manifestin_data_files = []
    if kw.get("include_package_data", False):
        original_manifestin_data_files = parse_manifestin(os.path.join(os.getcwd(), "MANIFEST.in"))
        for path in original_manifestin_data_files:
            _classify_file(path, package_data, package_prefixes,
                           py_modules, new_py_modules,
                           scripts, new_scripts,
                           data_files)

    if developer_mode:
        # Copy packages
        for package, package_file_list in package_data.items():
            for package_file in package_file_list:
                package_file = os.path.join(package_dir[package], package_file)
                cmake_file = os.path.join(CMAKE_INSTALL_DIR(), package_file)
                if os.path.exists(cmake_file):
                    _copy_file(cmake_file, package_file, hide_listing)

        # Copy modules
        for py_module in py_modules:
            package_file = py_module + ".py"
            cmake_file = os.path.join(CMAKE_INSTALL_DIR(), package_file)
            if os.path.exists(cmake_file):
                _copy_file(cmake_file, package_file, hide_listing)
    else:
        _consolidate_package_modules(
            cmake_source_dir, packages, package_dir, py_modules, package_data, hide_listing)

        original_package_data = kw.get('package_data', {}).copy()
        _consolidate_package_data_files(original_package_data, package_prefixes, hide_listing)

        for data_file in original_manifestin_data_files:
            dest_data_file = os.path.join(CMAKE_INSTALL_DIR(), data_file)
            _copy_file(data_file, dest_data_file, hide_listing)

    kw['package_data'] = package_data
    kw['package_dir'] = {
        package: (
            os.path.join(CMAKE_INSTALL_DIR(), prefix)
            if os.path.exists(os.path.join(CMAKE_INSTALL_DIR(), prefix))
            else prefix)
        for prefix, package in package_prefixes
    }

    kw['scripts'] = [
        os.path.join(CMAKE_INSTALL_DIR(), script) if mask else script
        for script, mask in new_scripts.items()
    ]

    kw['data_files'] = [
        (parent_dir, list(file_set))
        for parent_dir, file_set in data_files.items()
    ]

    if 'zip_safe' not in kw:
        kw['zip_safe'] = False

    # Adapted from espdev/ITKPythonInstaller/setup.py.in
    # pylint: disable=missing-docstring
    class BinaryDistribution(upstream_Distribution):
        def has_ext_modules(self):  # pylint: disable=no-self-use
            return has_cmakelists
    kw['distclass'] = BinaryDistribution

    print("")

    return upstream_setup(*args, **kw)
def setup(*args, **kw):
    """This function wraps setup() so that we can run cmake, make,
    CMake build, then proceed as usual with setuptools, appending the
    CMake-generated output as necessary.
    """
    sys.argv, cmake_args, make_args = parse_args()

    # work around https://bugs.python.org/issue1011113
    # (patches provided, but no updates since 2014)
    cmdclass = kw.get('cmdclass', {})
    cmdclass['build'] = cmdclass.get('build', build.build)
    cmdclass['install'] = cmdclass.get('install', install.install)
    cmdclass['clean'] = cmdclass.get('clean', clean.clean)
    cmdclass['sdist'] = cmdclass.get('sdist', sdist.sdist)
    cmdclass['bdist'] = cmdclass.get('bdist', bdist.bdist)
    cmdclass['bdist_wheel'] = cmdclass.get(
        'bdist_wheel', bdist_wheel.bdist_wheel)
    cmdclass['egg_info'] = cmdclass.get('egg_info', egg_info.egg_info)
    kw['cmdclass'] = cmdclass

    # Skip running CMake in the following cases:
    # * no command-line arguments or invalid ones are provided
    # * "display only" argument like '--help', '--help-commands'
    #   or '--author' are provided
    display_only = has_invalid_arguments = help_commands = False
    commands = []
    try:
        (display_only, help_commands, commands) = \
            _parse_setuptools_arguments(kw)
    except (DistutilsArgError, DistutilsGetoptError):
        has_invalid_arguments = True

    is_cmake_project = os.path.exists("CMakeLists.txt")
    if not is_cmake_project:
        print('skipping skbuild (no CMakeLists.txt found)')

    skip_cmake = (display_only
                  or has_invalid_arguments
                  or 'clean' in commands
                  or not is_cmake_project)
    if skip_cmake:
        if help_commands:
            # Prepend scikit-build help. Generate option descriptions using
            # argparse.
            skbuild_parser = create_skbuild_argparser()
            arg_descriptions = [
                line for line in skbuild_parser.format_help().split('\n')
                if line.startswith('  ')
                ]
            print('scikit-build options:')
            print('\n'.join(arg_descriptions))
            print('')
            print('Arguments following a "--" are passed directly to CMake '
                  '(e.g. -DMY_VAR:BOOL=TRUE).')
            print('Arguments following a second "--" are passed directly to '
                  ' the build tool.')
            print('')
        return upstream_setup(*args, **kw)

    packages = kw.get('packages', [])
    package_dir = kw.get('package_dir', {})
    package_data = kw.get('package_data', {}).copy()

    py_modules = kw.get('py_modules', [])

    scripts = kw.get('scripts', [])
    new_scripts = {script: False for script in scripts}

    data_files = {
        (parent_dir or '.'): set(file_list)
        for parent_dir, file_list in kw.get('data_files', [])
    }

    try:
        cmkr = cmaker.CMaker()
        cmkr.configure(cmake_args)
        cmkr.make(make_args)
    except SKBuildError as e:
        import traceback
        print("Traceback (most recent call last):")
        traceback.print_tb(sys.exc_info()[2])
        print('')
        sys.exit(e)

    package_prefixes = _collect_package_prefixes(package_dir, packages)

    _classify_files(cmkr.install(), package_data, package_prefixes, py_modules,
                    scripts, new_scripts, data_files)

    kw['package_data'] = package_data
    kw['package_dir'] = {
        package: os.path.join(cmaker.CMAKE_INSTALL_DIR, prefix)
        for prefix, package in package_prefixes
    }

    kw['py_modules'] = py_modules

    kw['scripts'] = [
        os.path.join(cmaker.CMAKE_INSTALL_DIR, script) if mask else script
        for script, mask in new_scripts.items()
    ]

    kw['data_files'] = [
        (parent_dir, list(file_set))
        for parent_dir, file_set in data_files.items()
    ]

    # Adapted from espdev/ITKPythonInstaller/setup.py.in
    class BinaryDistribution(upstream_Distribution):
        def has_ext_modules(self):
            return True
    kw['distclass'] = BinaryDistribution

    return upstream_setup(*args, **kw)
def setup(*args, **kw):  # noqa: C901
    """This function wraps setup() so that we can run cmake, make,
    CMake build, then proceed as usual with setuptools, appending the
    CMake-generated output as necessary.

    The CMake project is re-configured only if needed. This is achieved by (1) retrieving the environment mapping
    associated with the generator set in the ``CMakeCache.txt`` file, (2) saving the CMake configure arguments and
    version in :func:`skbuild.constants.CMAKE_SPEC_FILE()`: and (3) re-configuring only if either the generator or
    the CMake specs change.
    """
    sys.argv, cmake_executable, skip_generator_test, cmake_args, make_args = parse_args(
    )

    # work around https://bugs.python.org/issue1011113
    # (patches provided, but no updates since 2014)
    cmdclass = kw.get('cmdclass', {})
    cmdclass['build'] = cmdclass.get('build', build.build)
    cmdclass['build_py'] = cmdclass.get('build_py', build_py.build_py)
    cmdclass['build_ext'] = cmdclass.get('build_ext', build_ext.build_ext)
    cmdclass['install'] = cmdclass.get('install', install.install)
    cmdclass['install_lib'] = cmdclass.get('install_lib',
                                           install_lib.install_lib)
    cmdclass['install_scripts'] = cmdclass.get('install_scripts',
                                               install_scripts.install_scripts)
    cmdclass['clean'] = cmdclass.get('clean', clean.clean)
    cmdclass['sdist'] = cmdclass.get('sdist', sdist.sdist)
    cmdclass['bdist'] = cmdclass.get('bdist', bdist.bdist)
    cmdclass['bdist_wheel'] = cmdclass.get('bdist_wheel',
                                           bdist_wheel.bdist_wheel)
    cmdclass['egg_info'] = cmdclass.get('egg_info', egg_info.egg_info)
    cmdclass['generate_source_manifest'] = cmdclass.get(
        'generate_source_manifest',
        generate_source_manifest.generate_source_manifest)
    cmdclass['test'] = cmdclass.get('test', test.test)
    kw['cmdclass'] = cmdclass

    # Extract setup keywords specific to scikit-build and remove them from kw.
    # Removing the keyword from kw need to be done here otherwise, the
    # following call to _parse_setuptools_arguments would complain about
    # unknown setup options.
    parameters = {
        'cmake_args': [],
        'cmake_install_dir': '',
        'cmake_source_dir': '',
        'cmake_with_sdist': False,
        'cmake_languages': ('C', 'CXX'),
        'cmake_minimum_required_version': None,
        'cmake_process_manifest_hook': None
    }
    skbuild_kw = {
        param: kw.pop(param, parameters[param])
        for param in parameters
    }

    # ... and validate them
    try:
        _check_skbuild_parameters(skbuild_kw)
    except SKBuildError as ex:
        import traceback
        print("Traceback (most recent call last):")
        traceback.print_tb(sys.exc_info()[2])
        print('')
        sys.exit(ex)

    # Convert source dir to a path relative to the root
    # of the project
    cmake_source_dir = skbuild_kw['cmake_source_dir']
    if cmake_source_dir == ".":
        cmake_source_dir = ""
    if os.path.isabs(cmake_source_dir):
        cmake_source_dir = os.path.relpath(cmake_source_dir)

    # Skip running CMake in the following cases:
    # * flag "--skip-cmake" is provided
    # * "display only" argument is provided (e.g  '--help', '--author', ...)
    # * no command-line arguments or invalid ones are provided
    # * no command requiring cmake is provided
    # * no CMakeLists.txt if found
    display_only = has_invalid_arguments = help_commands = False
    force_cmake = skip_cmake = False
    commands = []
    try:
        (display_only, help_commands, commands,
         hide_listing, force_cmake, skip_cmake,
         plat_name, build_ext_inplace) = \
            _parse_setuptools_arguments(kw)
    except (DistutilsArgError, DistutilsGetoptError):
        has_invalid_arguments = True

    has_cmakelists = os.path.exists(
        os.path.join(cmake_source_dir, "CMakeLists.txt"))
    if not has_cmakelists:
        print('skipping skbuild (no CMakeLists.txt found)')

    skip_skbuild = (
        display_only or has_invalid_arguments
        or not _should_run_cmake(commands, skbuild_kw["cmake_with_sdist"])
        or not has_cmakelists)
    if skip_skbuild and not force_cmake:
        if help_commands:
            # Prepend scikit-build help. Generate option descriptions using
            # argparse.
            skbuild_parser = create_skbuild_argparser()
            arg_descriptions = [
                line for line in skbuild_parser.format_help().split('\n')
                if line.startswith('  ')
            ]
            print('scikit-build options:')
            print('\n'.join(arg_descriptions))
            print('')
            print('Arguments following a "--" are passed directly to CMake '
                  '(e.g. -DMY_VAR:BOOL=TRUE).')
            print('Arguments following a second "--" are passed directly to '
                  ' the build tool.')
            print('')
        return upstream_setup(*args, **kw)

    developer_mode = "develop" in commands or "test" in commands or build_ext_inplace

    packages = kw.get('packages', [])
    package_dir = kw.get('package_dir', {})
    package_data = copy.deepcopy(kw.get('package_data', {}))

    py_modules = kw.get('py_modules', [])
    new_py_modules = {py_module: False for py_module in py_modules}

    scripts = kw.get('scripts', [])
    new_scripts = {script: False for script in scripts}

    data_files = {(parent_dir or '.'): set(file_list)
                  for parent_dir, file_list in kw.get('data_files', [])}

    # Since CMake arguments provided through the command line have more
    # weight and when CMake is given multiple times a argument, only the last
    # one is considered, let's prepend the one provided in the setup call.
    cmake_args = skbuild_kw['cmake_args'] + cmake_args

    if sys.platform == 'darwin':

        # If no ``--plat-name`` argument was passed, set default value.
        if plat_name is None:
            plat_name = skbuild_plat_name()

        (_, version, machine) = plat_name.split('-')

        # The loop here allows for CMAKE_OSX_* command line arguments to overload
        # values passed with either the ``--plat-name`` command-line argument
        # or the ``cmake_args`` setup option.
        for cmake_arg in cmake_args:
            if 'CMAKE_OSX_DEPLOYMENT_TARGET' in cmake_arg:
                version = cmake_arg.split('=')[1]
            if 'CMAKE_OSX_ARCHITECTURES' in cmake_arg:
                machine = cmake_arg.split('=')[1]
                if set(machine.split(';')) == {'x86_64', 'arm64'}:
                    machine = 'universal2'

        set_skbuild_plat_name("macosx-{}-{}".format(version, machine))

        # Set platform env. variable so that commands (e.g. bdist_wheel)
        # uses this information. The _PYTHON_HOST_PLATFORM env. variable is
        # used in distutils.util.get_platform() function.
        os.environ.setdefault('_PYTHON_HOST_PLATFORM', skbuild_plat_name())

        # Set CMAKE_OSX_DEPLOYMENT_TARGET and CMAKE_OSX_ARCHITECTURES if not already
        # specified
        (_, version, machine) = skbuild_plat_name().split('-')
        if not cmaker.has_cmake_cache_arg(cmake_args,
                                          'CMAKE_OSX_DEPLOYMENT_TARGET'):
            cmake_args.append('-DCMAKE_OSX_DEPLOYMENT_TARGET:STRING=%s' %
                              version)
        if not cmaker.has_cmake_cache_arg(cmake_args,
                                          'CMAKE_OSX_ARCHITECTURES'):
            machine_archs = 'x86_64;arm64' if machine == 'universal2' else machine
            cmake_args.append('-DCMAKE_OSX_ARCHITECTURES:STRING=%s' %
                              machine_archs)

    # Install cmake if listed in `setup_requires`
    for package in kw.get('setup_requires', []):
        if Requirement(package).name == 'cmake':
            setup_requires = [package]
            dist = upstream_Distribution({'setup_requires': setup_requires})
            dist.fetch_build_eggs(setup_requires)

            # Considering packages associated with "setup_requires" keyword are
            # installed in .eggs subdirectory without honoring setuptools "console_scripts"
            # entry_points and without settings the expected executable permissions, we are
            # taking care of it below.
            import cmake
            for executable in ['cmake', 'cpack', 'ctest']:
                executable = os.path.join(cmake.CMAKE_BIN_DIR, executable)
                if platform.system().lower() == 'windows':
                    executable += '.exe'
                st = os.stat(executable)
                permissions = (st.st_mode | stat.S_IXUSR | stat.S_IXGRP
                               | stat.S_IXOTH)
                os.chmod(executable, permissions)
            cmake_executable = os.path.join(cmake.CMAKE_BIN_DIR, 'cmake')
            break

    # Languages are used to determine a working generator
    cmake_languages = skbuild_kw['cmake_languages']

    try:
        if cmake_executable is None:
            cmake_executable = CMAKE_DEFAULT_EXECUTABLE
        cmkr = cmaker.CMaker(cmake_executable)
        if not skip_cmake:
            cmake_minimum_required_version = skbuild_kw[
                'cmake_minimum_required_version']
            if cmake_minimum_required_version is not None:
                if parse_version(cmkr.cmake_version) < parse_version(
                        cmake_minimum_required_version):
                    raise SKBuildError(
                        "CMake version {} or higher is required. CMake version {} is being used"
                        .format(cmake_minimum_required_version,
                                cmkr.cmake_version))
            # Used to confirm that the cmake executable is the same, and that the environment
            # didn't change
            cmake_spec = {
                'args': [which(CMAKE_DEFAULT_EXECUTABLE)] + cmake_args,
                'version': cmkr.cmake_version,
                'environment': {
                    'PYTHONNOUSERSITE': os.environ.get("PYTHONNOUSERSITE"),
                    'PYTHONPATH': os.environ.get("PYTHONPATH")
                }
            }

            # skip the configure step for a cached build
            env = cmkr.get_cached_generator_env()
            if env is None or cmake_spec != _load_cmake_spec():
                env = cmkr.configure(
                    cmake_args,
                    skip_generator_test=skip_generator_test,
                    cmake_source_dir=cmake_source_dir,
                    cmake_install_dir=skbuild_kw['cmake_install_dir'],
                    languages=cmake_languages)
                _save_cmake_spec(cmake_spec)
            cmkr.make(make_args, env=env)
    except SKBuildGeneratorNotFoundError as ex:
        sys.exit(ex)
    except SKBuildError as ex:
        import traceback
        print("Traceback (most recent call last):")
        traceback.print_tb(sys.exc_info()[2])
        print('')
        sys.exit(ex)

    # If any, strip ending slash from each package directory
    package_dir = {
        package: prefix[:-1] if prefix and prefix[-1] == "/" else prefix
        for package, prefix in package_dir.items()
    }

    # If needed, set reasonable defaults for package_dir
    for package in packages:
        if package not in package_dir:
            package_dir[package] = package.replace(".", "/")
            if '' in package_dir:
                package_dir[package] = to_unix_path(
                    os.path.join(package_dir[''], package_dir[package]))

    package_prefixes = _collect_package_prefixes(package_dir, packages)

    # This hook enables custom processing of the cmake manifest
    cmake_manifest = cmkr.install()
    process_manifest = skbuild_kw.get('cmake_process_manifest_hook')
    if process_manifest is not None:
        if callable(process_manifest):
            cmake_manifest = process_manifest(cmake_manifest)
        else:
            raise SKBuildError(
                'The cmake_process_manifest_hook argument should be callable.')

    _classify_installed_files(cmake_manifest, package_data, package_prefixes,
                              py_modules, new_py_modules, scripts, new_scripts,
                              data_files, cmake_source_dir,
                              skbuild_kw['cmake_install_dir'])

    original_manifestin_data_files = []
    if kw.get("include_package_data", False):
        original_manifestin_data_files = parse_manifestin(
            os.path.join(os.getcwd(), "MANIFEST.in"))
        for path in original_manifestin_data_files:
            _classify_file(path, package_data, package_prefixes, py_modules,
                           new_py_modules, scripts, new_scripts, data_files)

    if developer_mode:
        # Copy packages
        for package, package_file_list in package_data.items():
            for package_file in package_file_list:
                package_file = os.path.join(package_dir[package], package_file)
                cmake_file = os.path.join(CMAKE_INSTALL_DIR(), package_file)
                if os.path.exists(cmake_file):
                    _copy_file(cmake_file, package_file, hide_listing)

        # Copy modules
        for py_module in py_modules:
            package_file = py_module + ".py"
            cmake_file = os.path.join(CMAKE_INSTALL_DIR(), package_file)
            if os.path.exists(cmake_file):
                _copy_file(cmake_file, package_file, hide_listing)
    else:
        _consolidate_package_modules(cmake_source_dir, packages, package_dir,
                                     py_modules, package_data, hide_listing)

        original_package_data = kw.get('package_data', {}).copy()
        _consolidate_package_data_files(original_package_data,
                                        package_prefixes, hide_listing)

        for data_file in original_manifestin_data_files:
            dest_data_file = os.path.join(CMAKE_INSTALL_DIR(), data_file)
            _copy_file(data_file, dest_data_file, hide_listing)

    kw['package_data'] = package_data
    kw['package_dir'] = {
        package: (os.path.join(CMAKE_INSTALL_DIR(), prefix) if os.path.exists(
            os.path.join(CMAKE_INSTALL_DIR(), prefix)) else prefix)
        for prefix, package in package_prefixes
    }

    kw['scripts'] = [
        os.path.join(CMAKE_INSTALL_DIR(), script) if mask else script
        for script, mask in new_scripts.items()
    ]

    kw['data_files'] = [(parent_dir, list(file_set))
                        for parent_dir, file_set in data_files.items()]

    if 'zip_safe' not in kw:
        kw['zip_safe'] = False

    # Adapted from espdev/ITKPythonInstaller/setup.py.in
    # pylint: disable=missing-docstring
    class BinaryDistribution(upstream_Distribution):
        def has_ext_modules(self):  # pylint: disable=no-self-use
            return has_cmakelists

    kw['distclass'] = BinaryDistribution

    print("")

    return upstream_setup(*args, **kw)
def setup(*args, **kw):
    """This function wraps setup() so that we can run cmake, make,
    CMake build, then proceed as usual with setuptools, appending the
    CMake-generated output as necessary.
    """
    sys.argv, cmake_args, make_args = parse_args()

    packages = kw.get('packages', [])
    package_dir = kw.get('package_dir', {})
    package_data = kw.get('package_data', {}).copy()

    py_modules = kw.get('py_modules', [])

    scripts = kw.get('scripts', [])
    new_scripts = {script: False for script in scripts}

    data_files = {
        (parent_dir or '.'): set(file_list)
        for parent_dir, file_list in kw.get('data_files', [])
    }

    # collect the list of prefixes for all packages
    #
    # The list is used to match paths in the install manifest to packages
    # specified in the setup.py script.
    #
    # The list is sorted in decreasing order of prefix length so that paths are
    # matched with their immediate parent package, instead of any of that
    # package's ancestors.
    #
    # For example, consider the project structure below.  Assume that the
    # setup call was made with a package list featuring "top" and "top.bar", but
    # not "top.not_a_subpackage".
    #
    # top/                -> top/
    #   __init__.py       -> top/__init__.py                 (parent: top)
    #   foo.py            -> top/foo.py                      (parent: top)
    #   bar/              -> top/bar/                        (parent: top)
    #     __init__.py     -> top/bar/__init__.py             (parent: top.bar)
    #
    #   not_a_subpackage/ -> top/not_a_subpackage/           (parent: top)
    #     data_0.txt      -> top/not_a_subpackage/data_0.txt (parent: top)
    #     data_1.txt      -> top/not_a_subpackage/data_1.txt (parent: top)
    #
    # The paths in the generated install manifest are matched to packages
    # according to the parents indicated on the right.  Only packages that are
    # specified in the setup() call are considered.  Because of the sort order,
    # the data files on the bottom would have been mapped to
    # "top.not_a_subpackage" instead of "top", proper -- had such a package been
    # specified.
    package_prefixes = list(sorted(
        (
            (package_dir[package].replace('.', '/'), package)
            for package in packages
        ),
        key=lambda tup: len(tup[0]),
        reverse=True
    ))

    try:
        cmkr = cmaker.CMaker()
        cmkr.configure(cmake_args)
        cmkr.make(make_args)
    except SKBuildError as e:
        import traceback
        print("Traceback (most recent call last):")
        traceback.print_tb(sys.exc_info()[2])
        print()
        sys.exit(e)

    _classify_files(cmkr.install(), package_data, package_prefixes, py_modules,
                    scripts, new_scripts, data_files)

    kw['package_data'] = package_data
    kw['package_dir'] = {
        package: os.path.join(cmaker.CMAKE_INSTALL_DIR, prefix)
        for prefix, package in package_prefixes
    }

    kw['py_modules'] = py_modules

    kw['scripts'] = [
        os.path.join(cmaker.CMAKE_INSTALL_DIR, script) if mask else script
        for script, mask in new_scripts.items()
    ]

    kw['data_files'] = [
        (parent_dir, list(file_set))
        for parent_dir, file_set in data_files.items()
    ]

    # work around https://bugs.python.org/issue1011113
    # (patches provided, but no updates since 2014)
    cmdclass = kw.get('cmdclass', {})
    cmdclass['build'] = cmdclass.get('build', build.build)
    cmdclass['install'] = cmdclass.get('install', install.install)
    cmdclass['clean'] = cmdclass.get('clean', clean.clean)
    cmdclass['bdist'] = cmdclass.get('bdist', bdist.bdist)
    cmdclass['bdist_wheel'] = cmdclass.get(
        'bdist_wheel', bdist_wheel.bdist_wheel)
    kw['cmdclass'] = cmdclass

    return upstream_setup(*args, **kw)