Source code for skbuild.platform_specifics.abstract

"""This module defines objects useful to discover which CMake generator is
supported on the current platform."""

from __future__ import print_function

import os
import shutil
import subprocess
import textwrap

from ..constants import CMAKE_DEFAULT_EXECUTABLE
from ..exceptions import SKBuildGeneratorNotFoundError
from ..utils import push_dir

test_folder = "_cmake_test_compile"

[docs]class CMakePlatform(object): """This class encapsulates the logic allowing to get the identifier of a working CMake generator. Derived class should at least set :attr:`default_generators`. """ def __init__(self): self._default_generators = list() @property def default_generators(self): """List of generators considered by :func:`get_best_generator()`.""" return self._default_generators @default_generators.setter def default_generators(self, generators): self._default_generators = generators @property def generator_installation_help(self): """Return message guiding the user for installing a valid toolchain.""" raise NotImplementedError # pragma: no cover
[docs] @staticmethod def write_test_cmakelist(languages): """Write a minimal ``CMakeLists.txt`` useful to check if the requested ``languages`` are supported.""" if not os.path.exists(test_folder): os.makedirs(test_folder) with open("{:s}/{:s}".format(test_folder, "CMakeLists.txt"), "w") as f: f.write("cmake_minimum_required(VERSION 2.8)\n") f.write("PROJECT(compiler_test NONE)\n") for language in languages: f.write("ENABLE_LANGUAGE({:s})\n".format(language))
[docs] @staticmethod def cleanup_test(): """Delete test project directory.""" if os.path.exists(test_folder): shutil.rmtree(test_folder)
[docs] def get_generator(self, generator_name): """Loop over generators and return the first that matches the given name. """ for default_generator in self.default_generators: if == generator_name: return default_generator return CMakeGenerator(generator_name)
# TODO: this method name is not great. Does anyone have a better idea for # renaming it?
[docs] def get_best_generator( self, generator_name=None, skip_generator_test=False, languages=("CXX", "C"), cleanup=True, cmake_executable=CMAKE_DEFAULT_EXECUTABLE, cmake_args=()): """Loop over generators to find one that works by configuring and compiling a test project. :param generator_name: If provided, uses only provided generator, \ instead of trying :attr:`default_generators`. :type generator_name: string or None :param skip_generator_test: If set to True and if a generator name is \ specified, the generator test is skipped. If no generator_name is specified \ and the option is set to True, the first available generator is used. :type skip_generator_test: bool :param languages: The languages you'll need for your project, in terms \ that CMake recognizes. :type languages: tuple :param cleanup: If True, cleans up temporary folder used to test \ generators. Set to False for debugging to see CMake's output files. :type cleanup: bool :param cmake_executable: Path to CMake executable used to configure \ and build the test project used to evaluate if a generator is working. :type cmake_executable: string :param cmake_args: List of CMake arguments to use when configuring \ the test project. Only arguments starting with ``-DCMAKE_`` are \ used. :type cmake_args: tuple :return: CMake Generator object :rtype: :class:`CMakeGenerator` or None :raises skbuild.exceptions.SKBuildGeneratorNotFoundError: """ candidate_generators = [] if generator_name is None: candidate_generators = self.default_generators else: # Lookup CMakeGenerator by name. Doing this allow to get a # generator object with its ``env`` property appropriately # initialized. candidate_generators = [] for default_generator in self.default_generators: if == generator_name: candidate_generators.append(default_generator) if not candidate_generators: candidate_generators = [CMakeGenerator(generator_name)] self.write_test_cmakelist(languages) if skip_generator_test: working_generator = candidate_generators[0] else: working_generator = self.compile_test_cmakelist( cmake_executable, candidate_generators, cmake_args) if working_generator is None: raise SKBuildGeneratorNotFoundError(textwrap.dedent( """ {line} scikit-build could not get a working generator for your system. Aborting build. {installation_help} {line} """).strip().format( # noqa: E501 line="*"*80, installation_help=self.generator_installation_help) ) if cleanup: CMakePlatform.cleanup_test() return working_generator
[docs] @staticmethod @push_dir(directory=test_folder) def compile_test_cmakelist( cmake_exe_path, candidate_generators, cmake_args=()): """Attempt to configure the test project with each :class:`CMakeGenerator` from ``candidate_generators``. Only cmake arguments starting with ``-DCMAKE_`` are used to configure the test project. The function returns the first generator allowing to successfully configure the test project using ``cmake_exe_path``.""" # working generator is the first generator we find that works. working_generator = None # Include only -DCMAKE_* arguments cmake_args = [arg for arg in cmake_args if arg.startswith("-DCMAKE_")] # Do not complain about unused CMake arguments cmake_args.insert(0, "--no-warn-unused-cli") def _generator_discovery_status_msg(_generator, suffix=""): outer = "-" * 80 inner = ["-" * ((idx * 5) - 3) for idx in range(1, 8)] print(outer if suffix == "" else "\n".join(inner)) print("-- Trying \"%s\" generator%s" % (_generator.description, suffix)) print(outer if suffix != "" else "\n".join(inner[::-1])) for generator in candidate_generators: print("\n") _generator_discovery_status_msg(generator) # clear the cache for each attempted generator type if os.path.isdir('build'): shutil.rmtree('build') with push_dir('build', make_directory=True): # call cmake to see if the compiler specified by this # generator works for the specified languages if generator.toolset: toolset_arg = ['-T', generator.toolset] else: toolset_arg = [] cmd = [cmake_exe_path, '../', '-G',] cmd.extend(toolset_arg) cmd.extend(cmake_args) status =, env=generator.env) _generator_discovery_status_msg( generator, " - %s" % ("success" if status == 0 else "failure")) print("") # cmake succeeded, this generator should work if status == 0: # we have a working generator, don't bother looking for more working_generator = generator break return working_generator
[docs]class CMakeGenerator(object): """Represents a CMake generator. .. automethod:: __init__ """
[docs] def __init__(self, name, env=None, toolset=None): """Instantiate a generator object with the given ``name``. By default, ``os.environ`` is associated with the generator. Dictionary passed as ``env`` parameter will be merged with ``os.environ``. If an environment variable is set in both ``os.environ`` and ``env``, the variable in ``env`` is used. Some CMake generators support a ``toolset`` specification to tell the native build system how to choose a compiler. """ self._generator_name = name self.env = dict( list(os.environ.items()) + list(env.items() if env else [])) self._generator_toolset = toolset if toolset is None: self._description = name else: self._description = "%s %s" % (name, toolset)
@property def name(self): """Name of CMake generator.""" return self._generator_name @property def toolset(self): """Toolset specification associated with the CMake generator.""" return self._generator_toolset @property def description(self): """Name of CMake generator with properties describing the environment (e.g toolset)""" return self._description