From 25689dbab5aa7cf8bcc3607e58e049cc1103dc93 Mon Sep 17 00:00:00 2001 From: Egor Tensin Date: Thu, 6 May 2021 23:32:11 +0300 Subject: "toolchain" -> "toolset", part 1 --- project/boost/build.py | 4 +- project/boost/directory.py | 2 +- project/boost/toolchain.py | 273 --------------------------------------------- project/boost/toolset.py | 273 +++++++++++++++++++++++++++++++++++++++++++++ project/ci/dirs.py | 2 +- project/cmake/build.py | 4 +- project/cmake/toolchain.py | 182 ------------------------------ project/cmake/toolset.py | 182 ++++++++++++++++++++++++++++++ project/toolchain.py | 50 --------- project/toolset.py | 50 +++++++++ 10 files changed, 511 insertions(+), 511 deletions(-) delete mode 100644 project/boost/toolchain.py create mode 100644 project/boost/toolset.py delete mode 100644 project/cmake/toolchain.py create mode 100644 project/cmake/toolset.py delete mode 100644 project/toolchain.py create mode 100644 project/toolset.py (limited to 'project') diff --git a/project/boost/build.py b/project/boost/build.py index 32b355a..d1766ee 100644 --- a/project/boost/build.py +++ b/project/boost/build.py @@ -31,8 +31,8 @@ import sys import tempfile from project.boost.directory import BoostDir -from project.toolchain import ToolchainType -from project.boost.toolchain import Toolchain +from project.toolset import ToolchainType +from project.boost.toolset import Toolchain from project.configuration import Configuration from project.linkage import Linkage from project.platform import Platform diff --git a/project/boost/directory.py b/project/boost/directory.py index 162562c..7c39383 100644 --- a/project/boost/directory.py +++ b/project/boost/directory.py @@ -6,7 +6,7 @@ import logging import os.path -from project.boost.toolchain import Toolchain +from project.boost.toolset import Toolchain from project.utils import cd, run from project.os import on_windows diff --git a/project/boost/toolchain.py b/project/boost/toolchain.py deleted file mode 100644 index 5fa2043..0000000 --- a/project/boost/toolchain.py +++ /dev/null @@ -1,273 +0,0 @@ -# Copyright (c) 2020 Egor Tensin -# This file is part of the "cmake-common" project. -# For details, see https://github.com/egor-tensin/cmake-common. -# Distributed under the MIT License. - -# See docs/boost.md for a more thorough description of my pain. - -import abc -from contextlib import contextmanager -import logging -import os.path -import shutil - -import project.mingw -import project.os -from project.toolchain import ToolchainType -from project.utils import temp_file - - -def _gcc_or_auto(): - if shutil.which('gcc') is not None: - return ['gcc'] - return [] - - -class Toolchain(abc.ABC): - @contextmanager - def b2_args(self): - # Write the config file, etc. - yield [] - - @staticmethod - @abc.abstractmethod - def get_bootstrap_bat_args(): - pass - - @staticmethod - @abc.abstractmethod - def get_bootstrap_sh_args(): - pass - - @staticmethod - def detect(hint): - if hint is ToolchainType.AUTO: - return Auto - if hint is ToolchainType.MSVC: - return MSVC - if hint is ToolchainType.GCC: - return GCC - if hint is ToolchainType.MINGW: - return MinGW - if hint is ToolchainType.CLANG: - return Clang - if hint is ToolchainType.CLANG_CL: - return ClangCL - raise NotImplementedError(f'unrecognized toolset: {hint}') - - @staticmethod - def make(hint, platform): - # Platform is required here, since some toolchains (MinGW-w64) require - # it for the compiler path. - cls = Toolchain.detect(hint) - if cls is MinGW: - return MinGW(platform) - return cls() - - -class Auto(Toolchain): - # Let Boost.Build do the detection. Most commonly it means GCC on - # Linux-likes and MSVC on Windows. - - @staticmethod - def get_bootstrap_bat_args(): - return [] - - @staticmethod - def get_bootstrap_sh_args(): - return [] - - -class MSVC(Auto): - @contextmanager - def b2_args(self): - yield ['toolset=msvc'] - - # Note: bootstrap.bat picks up MSVC by default. - - -def _full_exe_name(exe): - if project.os.on_linux(): - # There's no PATHEXT on Linux. - return exe - # b2 on Windows/Cygwin doesn't like it when the executable name doesn't - # include the extension. - dir_path = os.path.dirname(exe) or None - path = shutil.which(exe, path=dir_path) - if not path: - raise RuntimeError(f"executable '{exe}' could not be found") - if project.os.on_cygwin(): - # On Cygwin, shutil.which('gcc') == '/usr/bin/gcc' and shutil.which('gcc.exe') - # == '/usr/bin/gcc.exe'; we want the latter version. shutil.which('clang++') - # == '/usr/bin/clang++' is fine though, since it _is_ the complete path - # (clang++ is a symlink). - if os.path.exists(path) and os.path.exists(path + '.exe'): - path += '.exe' - if dir_path: - # If it was found in a specific directory, include the directory in the - # result. shutil.which returns the executable name prefixed with the - # path argument. - return path - # If it was found in PATH, just return the basename (which includes the - # extension). - return os.path.basename(path) - - -class Custom(Toolchain): - COMPILER_VERSION = 'custom' - - def __init__(self, compiler, path=None, build_options=None): - if not compiler: - raise RuntimeError('compiler type is required (like gcc, clang, etc.)') - self.compiler = compiler - version = Custom.COMPILER_VERSION - self.version = version - path = path or '' - path = path and _full_exe_name(path) - self.path = path - build_options = build_options or [] - self.build_options = build_options - - def toolset(self): - if self.version: - return f'{self.compiler}-{self.version}' - return self.compiler - - def b2_arg_toolset(self): - return f'toolset={self.toolset()}' - - def _format_build_options(self): - return ''.join(f'\n <{name}>{val}' for name, val in self.build_options) - - def format_config(self): - version = self.version and f'{self.version} ' - path = self.path and f'{self.path} ' - return f'''using {self.compiler} : {version}: {path}:{self._format_build_options()} -;''' - - @contextmanager - def b2_args(self): - config_file = temp_file(prefix='user_config_', suffix='.jam') - with config_file as config_path: - config = self.format_config() - logging.info('Using user config:\n%s', config) - with open(config_path, mode='w') as fd: - fd.write(config) - args = [] - args.append(self.b2_arg_toolset()) - args.append(f'--user-config={config_path}') - yield args - - -class GCC(Custom): - # Force GCC. We don't care whether it's a native Linux GCC or a - # MinGW-flavoured GCC on Windows. - def __init__(self, path='g++', build_options=None): - build_options = build_options or self.get_build_options() - super().__init__('gcc', path, build_options) - - @staticmethod - def get_bootstrap_bat_args(): - return ['gcc'] - - @staticmethod - def get_bootstrap_sh_args(): - return ['--with-toolset=gcc'] - - @staticmethod - def get_build_options(): - return [] - - -class MinGW(GCC): - # It's important that Boost.Build is actually smart enough to detect the - # GCC prefix (like "x86_64-w64-mingw32" and prepend it to other tools like - # "ar"). - - def __init__(self, platform): - paths = project.mingw.MinGW(platform) - super().__init__(paths.gxx()) - - @staticmethod - def get_bootstrap_bat_args(): - # On Windows, prefer GCC if it's available. - return _gcc_or_auto() - - @staticmethod - def get_bootstrap_sh_args(): - return [] - - -class Clang(Custom): - def __init__(self): - super().__init__('clang', 'clang++', self.get_build_options()) - - @staticmethod - def get_bootstrap_bat_args(): - # As of 1.74.0, bootstrap.bat isn't really aware of Clang, so try GCC, - # then auto-detect. - return _gcc_or_auto() - - @staticmethod - def get_bootstrap_sh_args(): - # bootstrap.sh, on the other hand, is very much aware of Clang, and - # it can build b2 using this compiler. - return ['--with-toolset=clang'] - - @staticmethod - def get_build_options(): - options = GCC.get_build_options() - options += [ - ('cxxflags', '-DBOOST_USE_WINDOWS_H'), - - # Even with off, the build might sometimes fail with the - # following error: - # - # error: constant expression evaluates to -105 which cannot be narrowed to type 'boost::re_detail::cpp_regex_traits_implementation::char_class_type' (aka 'unsigned int') - ('cxxflags', '-Wno-c++11-narrowing'), - ] - if project.os.on_windows(): - # Prefer LLVM binutils: - if shutil.which('llvm-ar') is not None: - options.append(('archiver', 'llvm-ar')) - if shutil.which('llvm-ranlib') is not None: - options.append(('ranlib', 'llvm-ranlib')) - return options - - def format_config(self): - # To make clang.exe/clang++.exe work on Windows, some tweaks are - # required. I borrowed them from CMake's Windows-Clang.cmake [1]. - # Adding them globally to Boost.Build options is described in [2]. - # - # [1]: https://github.com/Kitware/CMake/blob/v3.18.4/Modules/Platform/Windows-Clang.cmake - # [2]: https://stackoverflow.com/questions/2715106/how-to-create-a-new-variant-in-bjam - return f'''project : requirements - windows:_MT - windows,debug:_DEBUG - windows,static,debug:"-Xclang -flto-visibility-public-std -Xclang --dependent-lib=libcmtd" - windows,static,release:"-Xclang -flto-visibility-public-std -Xclang --dependent-lib=libcmt" - windows,shared,debug:"-D_DLL -Xclang --dependent-lib=msvcrtd" - windows,shared,release:"-D_DLL -Xclang --dependent-lib=msvcrt" -; -{super().format_config()} -''' - - -class ClangCL(Toolchain): - @contextmanager - def b2_args(self): - yield [ - 'toolset=clang-win', - 'define=BOOST_USE_WINDOWS_H', - ] - - # There's no point in building b2 using clang-cl; clang though, presumably - # installed alongside clang-cl, should still be used if possible. - - @staticmethod - def get_bootstrap_bat_args(): - return Clang.get_bootstrap_bat_args() - - @staticmethod - def get_bootstrap_sh_args(): - return Clang.get_bootstrap_sh_args() diff --git a/project/boost/toolset.py b/project/boost/toolset.py new file mode 100644 index 0000000..c59ef23 --- /dev/null +++ b/project/boost/toolset.py @@ -0,0 +1,273 @@ +# Copyright (c) 2020 Egor Tensin +# This file is part of the "cmake-common" project. +# For details, see https://github.com/egor-tensin/cmake-common. +# Distributed under the MIT License. + +# See docs/boost.md for a more thorough description of my pain. + +import abc +from contextlib import contextmanager +import logging +import os.path +import shutil + +import project.mingw +import project.os +from project.toolset import ToolchainType +from project.utils import temp_file + + +def _gcc_or_auto(): + if shutil.which('gcc') is not None: + return ['gcc'] + return [] + + +class Toolchain(abc.ABC): + @contextmanager + def b2_args(self): + # Write the config file, etc. + yield [] + + @staticmethod + @abc.abstractmethod + def get_bootstrap_bat_args(): + pass + + @staticmethod + @abc.abstractmethod + def get_bootstrap_sh_args(): + pass + + @staticmethod + def detect(hint): + if hint is ToolchainType.AUTO: + return Auto + if hint is ToolchainType.MSVC: + return MSVC + if hint is ToolchainType.GCC: + return GCC + if hint is ToolchainType.MINGW: + return MinGW + if hint is ToolchainType.CLANG: + return Clang + if hint is ToolchainType.CLANG_CL: + return ClangCL + raise NotImplementedError(f'unrecognized toolset: {hint}') + + @staticmethod + def make(hint, platform): + # Platform is required here, since some toolchains (MinGW-w64) require + # it for the compiler path. + cls = Toolchain.detect(hint) + if cls is MinGW: + return MinGW(platform) + return cls() + + +class Auto(Toolchain): + # Let Boost.Build do the detection. Most commonly it means GCC on + # Linux-likes and MSVC on Windows. + + @staticmethod + def get_bootstrap_bat_args(): + return [] + + @staticmethod + def get_bootstrap_sh_args(): + return [] + + +class MSVC(Auto): + @contextmanager + def b2_args(self): + yield ['toolset=msvc'] + + # Note: bootstrap.bat picks up MSVC by default. + + +def _full_exe_name(exe): + if project.os.on_linux(): + # There's no PATHEXT on Linux. + return exe + # b2 on Windows/Cygwin doesn't like it when the executable name doesn't + # include the extension. + dir_path = os.path.dirname(exe) or None + path = shutil.which(exe, path=dir_path) + if not path: + raise RuntimeError(f"executable '{exe}' could not be found") + if project.os.on_cygwin(): + # On Cygwin, shutil.which('gcc') == '/usr/bin/gcc' and shutil.which('gcc.exe') + # == '/usr/bin/gcc.exe'; we want the latter version. shutil.which('clang++') + # == '/usr/bin/clang++' is fine though, since it _is_ the complete path + # (clang++ is a symlink). + if os.path.exists(path) and os.path.exists(path + '.exe'): + path += '.exe' + if dir_path: + # If it was found in a specific directory, include the directory in the + # result. shutil.which returns the executable name prefixed with the + # path argument. + return path + # If it was found in PATH, just return the basename (which includes the + # extension). + return os.path.basename(path) + + +class Custom(Toolchain): + COMPILER_VERSION = 'custom' + + def __init__(self, compiler, path=None, build_options=None): + if not compiler: + raise RuntimeError('compiler type is required (like gcc, clang, etc.)') + self.compiler = compiler + version = Custom.COMPILER_VERSION + self.version = version + path = path or '' + path = path and _full_exe_name(path) + self.path = path + build_options = build_options or [] + self.build_options = build_options + + def toolset(self): + if self.version: + return f'{self.compiler}-{self.version}' + return self.compiler + + def b2_arg_toolset(self): + return f'toolset={self.toolset()}' + + def _format_build_options(self): + return ''.join(f'\n <{name}>{val}' for name, val in self.build_options) + + def format_config(self): + version = self.version and f'{self.version} ' + path = self.path and f'{self.path} ' + return f'''using {self.compiler} : {version}: {path}:{self._format_build_options()} +;''' + + @contextmanager + def b2_args(self): + config_file = temp_file(prefix='user_config_', suffix='.jam') + with config_file as config_path: + config = self.format_config() + logging.info('Using user config:\n%s', config) + with open(config_path, mode='w') as fd: + fd.write(config) + args = [] + args.append(self.b2_arg_toolset()) + args.append(f'--user-config={config_path}') + yield args + + +class GCC(Custom): + # Force GCC. We don't care whether it's a native Linux GCC or a + # MinGW-flavoured GCC on Windows. + def __init__(self, path='g++', build_options=None): + build_options = build_options or self.get_build_options() + super().__init__('gcc', path, build_options) + + @staticmethod + def get_bootstrap_bat_args(): + return ['gcc'] + + @staticmethod + def get_bootstrap_sh_args(): + return ['--with-toolset=gcc'] + + @staticmethod + def get_build_options(): + return [] + + +class MinGW(GCC): + # It's important that Boost.Build is actually smart enough to detect the + # GCC prefix (like "x86_64-w64-mingw32" and prepend it to other tools like + # "ar"). + + def __init__(self, platform): + paths = project.mingw.MinGW(platform) + super().__init__(paths.gxx()) + + @staticmethod + def get_bootstrap_bat_args(): + # On Windows, prefer GCC if it's available. + return _gcc_or_auto() + + @staticmethod + def get_bootstrap_sh_args(): + return [] + + +class Clang(Custom): + def __init__(self): + super().__init__('clang', 'clang++', self.get_build_options()) + + @staticmethod + def get_bootstrap_bat_args(): + # As of 1.74.0, bootstrap.bat isn't really aware of Clang, so try GCC, + # then auto-detect. + return _gcc_or_auto() + + @staticmethod + def get_bootstrap_sh_args(): + # bootstrap.sh, on the other hand, is very much aware of Clang, and + # it can build b2 using this compiler. + return ['--with-toolset=clang'] + + @staticmethod + def get_build_options(): + options = GCC.get_build_options() + options += [ + ('cxxflags', '-DBOOST_USE_WINDOWS_H'), + + # Even with off, the build might sometimes fail with the + # following error: + # + # error: constant expression evaluates to -105 which cannot be narrowed to type 'boost::re_detail::cpp_regex_traits_implementation::char_class_type' (aka 'unsigned int') + ('cxxflags', '-Wno-c++11-narrowing'), + ] + if project.os.on_windows(): + # Prefer LLVM binutils: + if shutil.which('llvm-ar') is not None: + options.append(('archiver', 'llvm-ar')) + if shutil.which('llvm-ranlib') is not None: + options.append(('ranlib', 'llvm-ranlib')) + return options + + def format_config(self): + # To make clang.exe/clang++.exe work on Windows, some tweaks are + # required. I borrowed them from CMake's Windows-Clang.cmake [1]. + # Adding them globally to Boost.Build options is described in [2]. + # + # [1]: https://github.com/Kitware/CMake/blob/v3.18.4/Modules/Platform/Windows-Clang.cmake + # [2]: https://stackoverflow.com/questions/2715106/how-to-create-a-new-variant-in-bjam + return f'''project : requirements + windows:_MT + windows,debug:_DEBUG + windows,static,debug:"-Xclang -flto-visibility-public-std -Xclang --dependent-lib=libcmtd" + windows,static,release:"-Xclang -flto-visibility-public-std -Xclang --dependent-lib=libcmt" + windows,shared,debug:"-D_DLL -Xclang --dependent-lib=msvcrtd" + windows,shared,release:"-D_DLL -Xclang --dependent-lib=msvcrt" +; +{super().format_config()} +''' + + +class ClangCL(Toolchain): + @contextmanager + def b2_args(self): + yield [ + 'toolset=clang-win', + 'define=BOOST_USE_WINDOWS_H', + ] + + # There's no point in building b2 using clang-cl; clang though, presumably + # installed alongside clang-cl, should still be used if possible. + + @staticmethod + def get_bootstrap_bat_args(): + return Clang.get_bootstrap_bat_args() + + @staticmethod + def get_bootstrap_sh_args(): + return Clang.get_bootstrap_sh_args() diff --git a/project/ci/dirs.py b/project/ci/dirs.py index 6c9aa7a..6804ff4 100644 --- a/project/ci/dirs.py +++ b/project/ci/dirs.py @@ -11,7 +11,7 @@ from project.boost.version import Version from project.ci.appveyor.generator import Generator, Image from project.configuration import Configuration from project.platform import Platform -from project.toolchain import ToolchainType +from project.toolset import ToolchainType from project.utils import env diff --git a/project/cmake/build.py b/project/cmake/build.py index 74eb820..b6f3e85 100644 --- a/project/cmake/build.py +++ b/project/cmake/build.py @@ -27,10 +27,10 @@ import os.path import sys import tempfile -from project.cmake.toolchain import Toolchain +from project.cmake.toolset import Toolchain from project.configuration import Configuration from project.platform import Platform -from project.toolchain import ToolchainType +from project.toolset import ToolchainType from project.utils import normalize_path, mkdir_parent, run, setup_logging diff --git a/project/cmake/toolchain.py b/project/cmake/toolchain.py deleted file mode 100644 index c12ee84..0000000 --- a/project/cmake/toolchain.py +++ /dev/null @@ -1,182 +0,0 @@ -# Copyright (c) 2020 Egor Tensin -# This file is part of the "cmake-common" project. -# For details, see https://github.com/egor-tensin/cmake-common. -# Distributed under the MIT License. - -# See docs/cmake.md for a more thorough description of my pain. - -import abc -import os.path -import shutil - -import project.mingw -from project.os import on_windows -from project.platform import Platform -from project.toolchain import ToolchainType - - -class Toolchain(abc.ABC): - @abc.abstractmethod - def cmake_args(self): - pass - - @abc.abstractmethod - def build_system_args(self): - pass - - @staticmethod - def detect(hint, platform, build_dir): - if hint is ToolchainType.AUTO: - if on_windows(): - # On Windows, 'auto' means 'msvc', and we need to specify the - # -A parameter. This might break if none of the Visual Studio - # generators are available, but the NMake one is, although I - # don't know how this can be possible normally. - hint = ToolchainType.MSVC - else: - # On Linux, if the platform wasn't specified, auto-detect - # everything. There's no need to set -mXX flags, etc. - if platform is Platform.AUTO: - return Auto() - # If a specific platform was requested, we might need to set - # some CMake/compiler flags, like -m32/-m64. - hint = ToolchainType.GCC - if hint is ToolchainType.MSVC: - return MSVC(platform) - if hint is ToolchainType.GCC: - return GCC.setup(platform, build_dir) - if hint is ToolchainType.MINGW: - return MinGW.setup(platform, build_dir) - if hint is ToolchainType.CLANG: - return Clang.setup(platform, build_dir) - if hint is ToolchainType.CLANG_CL: - return ClangCL.setup(platform, build_dir) - raise NotImplementedError(f'unrecognized toolset: {hint}') - - -class Auto(Toolchain): - def cmake_args(self): - return [] - - def build_system_args(self): - return [] - - -class MSVC(Auto): - def __init__(self, platform): - self.platform = platform - - def cmake_args(self): - # This doesn't actually specify the generator of course, but I don't - # want to implement VS detection logic. - return ['-A', self.platform.msvc_arch()] - - def build_system_args(self): - return [] - - -class Makefile(Toolchain): - def __init__(self, path): - self.path = path - - @staticmethod - def _get_config_path(build_dir): - return os.path.join(build_dir, 'custom_toolchain.cmake') - - @staticmethod - def _get_makefile_generator(): - if on_windows(): - if shutil.which('mingw32-make'): - return 'MinGW Makefiles' - return 'Unix Makefiles' - # On Linux/Cygwin, make all the way: - return 'Unix Makefiles' - - @classmethod - def write_config(cls, build_dir, contents): - path = Makefile._get_config_path(build_dir) - with open(path, mode='w') as file: - file.write(contents) - return cls(path) - - def cmake_args(self): - return [ - '-D', f'CMAKE_TOOLCHAIN_FILE={self.path}', - # The Visual Studio generator is the default on Windows, override - # it: - '-G', self._get_makefile_generator(), - ] - - def build_system_args(self): - return [] - - -class GCC(Makefile): - @staticmethod - def _format(platform): - return f''' -set(CMAKE_C_COMPILER gcc) -set(CMAKE_CXX_COMPILER g++) -{platform.makefile_toolchain_file()}''' - - @staticmethod - def setup(platform, build_dir): - return GCC.write_config(build_dir, GCC._format(platform)) - - -class MinGW(Makefile): - @staticmethod - def _format(platform): - paths = project.mingw.MinGW(platform) - return f''' -set(CMAKE_C_COMPILER {paths.gcc()}) -set(CMAKE_CXX_COMPILER {paths.gxx()}) -set(CMAKE_AR {paths.ar()}) -set(CMAKE_RANLIB {paths.ranlib()}) -set(CMAKE_RC_COMPILER {paths.windres()}) -set(CMAKE_SYSTEM_NAME Windows) -''' - - @staticmethod - def setup(platform, build_dir): - return MinGW.write_config(build_dir, MinGW._format(platform)) - - -class Clang(Makefile): - @staticmethod - def _format(platform): - return f''' -if(CMAKE_VERSION VERSION_LESS "3.15" AND WIN32) - set(CMAKE_C_COMPILER clang-cl) - set(CMAKE_CXX_COMPILER clang-cl) -else() - set(CMAKE_C_COMPILER clang) - set(CMAKE_CXX_COMPILER clang++) -endif() -{platform.makefile_toolchain_file()}''' - - def _get_makefile_generator(self): - if on_windows(): - # MinGW utilities like make might be unavailable, but NMake can - # very much be there. - if shutil.which('nmake'): - return 'NMake Makefiles' - return super()._get_makefile_generator() - - @staticmethod - def setup(platform, build_dir): - return Clang.write_config(build_dir, Clang._format(platform)) - - -class ClangCL(Clang): - @staticmethod - def _format(platform): - return f''' -set(CMAKE_C_COMPILER clang-cl) -set(CMAKE_CXX_COMPILER clang-cl) -set(CMAKE_SYSTEM_NAME Windows) -{platform.makefile_toolchain_file()}''' - - @staticmethod - def setup(platform, build_dir): - return ClangCL.write_config(build_dir, ClangCL._format(platform)) diff --git a/project/cmake/toolset.py b/project/cmake/toolset.py new file mode 100644 index 0000000..0bf8e64 --- /dev/null +++ b/project/cmake/toolset.py @@ -0,0 +1,182 @@ +# Copyright (c) 2020 Egor Tensin +# This file is part of the "cmake-common" project. +# For details, see https://github.com/egor-tensin/cmake-common. +# Distributed under the MIT License. + +# See docs/cmake.md for a more thorough description of my pain. + +import abc +import os.path +import shutil + +import project.mingw +from project.os import on_windows +from project.platform import Platform +from project.toolset import ToolchainType + + +class Toolchain(abc.ABC): + @abc.abstractmethod + def cmake_args(self): + pass + + @abc.abstractmethod + def build_system_args(self): + pass + + @staticmethod + def detect(hint, platform, build_dir): + if hint is ToolchainType.AUTO: + if on_windows(): + # On Windows, 'auto' means 'msvc', and we need to specify the + # -A parameter. This might break if none of the Visual Studio + # generators are available, but the NMake one is, although I + # don't know how this can be possible normally. + hint = ToolchainType.MSVC + else: + # On Linux, if the platform wasn't specified, auto-detect + # everything. There's no need to set -mXX flags, etc. + if platform is Platform.AUTO: + return Auto() + # If a specific platform was requested, we might need to set + # some CMake/compiler flags, like -m32/-m64. + hint = ToolchainType.GCC + if hint is ToolchainType.MSVC: + return MSVC(platform) + if hint is ToolchainType.GCC: + return GCC.setup(platform, build_dir) + if hint is ToolchainType.MINGW: + return MinGW.setup(platform, build_dir) + if hint is ToolchainType.CLANG: + return Clang.setup(platform, build_dir) + if hint is ToolchainType.CLANG_CL: + return ClangCL.setup(platform, build_dir) + raise NotImplementedError(f'unrecognized toolset: {hint}') + + +class Auto(Toolchain): + def cmake_args(self): + return [] + + def build_system_args(self): + return [] + + +class MSVC(Auto): + def __init__(self, platform): + self.platform = platform + + def cmake_args(self): + # This doesn't actually specify the generator of course, but I don't + # want to implement VS detection logic. + return ['-A', self.platform.msvc_arch()] + + def build_system_args(self): + return [] + + +class Makefile(Toolchain): + def __init__(self, path): + self.path = path + + @staticmethod + def _get_config_path(build_dir): + return os.path.join(build_dir, 'custom_toolchain.cmake') + + @staticmethod + def _get_makefile_generator(): + if on_windows(): + if shutil.which('mingw32-make'): + return 'MinGW Makefiles' + return 'Unix Makefiles' + # On Linux/Cygwin, make all the way: + return 'Unix Makefiles' + + @classmethod + def write_config(cls, build_dir, contents): + path = Makefile._get_config_path(build_dir) + with open(path, mode='w') as file: + file.write(contents) + return cls(path) + + def cmake_args(self): + return [ + '-D', f'CMAKE_TOOLCHAIN_FILE={self.path}', + # The Visual Studio generator is the default on Windows, override + # it: + '-G', self._get_makefile_generator(), + ] + + def build_system_args(self): + return [] + + +class GCC(Makefile): + @staticmethod + def _format(platform): + return f''' +set(CMAKE_C_COMPILER gcc) +set(CMAKE_CXX_COMPILER g++) +{platform.makefile_toolchain_file()}''' + + @staticmethod + def setup(platform, build_dir): + return GCC.write_config(build_dir, GCC._format(platform)) + + +class MinGW(Makefile): + @staticmethod + def _format(platform): + paths = project.mingw.MinGW(platform) + return f''' +set(CMAKE_C_COMPILER {paths.gcc()}) +set(CMAKE_CXX_COMPILER {paths.gxx()}) +set(CMAKE_AR {paths.ar()}) +set(CMAKE_RANLIB {paths.ranlib()}) +set(CMAKE_RC_COMPILER {paths.windres()}) +set(CMAKE_SYSTEM_NAME Windows) +''' + + @staticmethod + def setup(platform, build_dir): + return MinGW.write_config(build_dir, MinGW._format(platform)) + + +class Clang(Makefile): + @staticmethod + def _format(platform): + return f''' +if(CMAKE_VERSION VERSION_LESS "3.15" AND WIN32) + set(CMAKE_C_COMPILER clang-cl) + set(CMAKE_CXX_COMPILER clang-cl) +else() + set(CMAKE_C_COMPILER clang) + set(CMAKE_CXX_COMPILER clang++) +endif() +{platform.makefile_toolchain_file()}''' + + def _get_makefile_generator(self): + if on_windows(): + # MinGW utilities like make might be unavailable, but NMake can + # very much be there. + if shutil.which('nmake'): + return 'NMake Makefiles' + return super()._get_makefile_generator() + + @staticmethod + def setup(platform, build_dir): + return Clang.write_config(build_dir, Clang._format(platform)) + + +class ClangCL(Clang): + @staticmethod + def _format(platform): + return f''' +set(CMAKE_C_COMPILER clang-cl) +set(CMAKE_CXX_COMPILER clang-cl) +set(CMAKE_SYSTEM_NAME Windows) +{platform.makefile_toolchain_file()}''' + + @staticmethod + def setup(platform, build_dir): + return ClangCL.write_config(build_dir, ClangCL._format(platform)) diff --git a/project/toolchain.py b/project/toolchain.py deleted file mode 100644 index f92e536..0000000 --- a/project/toolchain.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (c) 2020 Egor Tensin -# This file is part of the "cmake-common" project. -# For details, see https://github.com/egor-tensin/cmake-common. -# Distributed under the MIT License. - -'''Supported platform/build system/compiler combinations include, but are not -limited to: - -| Platform | Build system | Compiler -| -------- | ------------ | -------- -| Linux | make | Clang -| | | GCC -| | | MinGW-w64 -| Windows | make [1] | Clang (clang/clang++) -| | | Clang (clang-cl [2]) -| | | MinGW-w64 -| | msbuild | MSVC -| Cygwin | make | Clang -| | | GCC -| | | MinGW-w64 - -1. Both GNU make and MinGW mingw32-make. -2. Boost 1.69.0 or higher only. -''' - -import argparse -from enum import Enum - - -class ToolchainType(Enum): - AUTO = 'auto' # This most commonly means GCC on Linux and MSVC on Windows. - MSVC = 'msvc' # Force MSVC. - GCC = 'gcc' # Force GCC. - MINGW = 'mingw' # As in MinGW-w64; GCC with the PLATFORM-w64-mingw32 prefix. - CLANG = 'clang' - CLANG_CL = 'clang-cl' - - def __str__(self): - return str(self.value) - - @staticmethod - def all(): - return tuple(ToolchainType) - - @staticmethod - def parse(s): - try: - return ToolchainType(s) - except ValueError as e: - raise argparse.ArgumentTypeError(f'invalid toolset: {s}') from e diff --git a/project/toolset.py b/project/toolset.py new file mode 100644 index 0000000..f92e536 --- /dev/null +++ b/project/toolset.py @@ -0,0 +1,50 @@ +# Copyright (c) 2020 Egor Tensin +# This file is part of the "cmake-common" project. +# For details, see https://github.com/egor-tensin/cmake-common. +# Distributed under the MIT License. + +'''Supported platform/build system/compiler combinations include, but are not +limited to: + +| Platform | Build system | Compiler +| -------- | ------------ | -------- +| Linux | make | Clang +| | | GCC +| | | MinGW-w64 +| Windows | make [1] | Clang (clang/clang++) +| | | Clang (clang-cl [2]) +| | | MinGW-w64 +| | msbuild | MSVC +| Cygwin | make | Clang +| | | GCC +| | | MinGW-w64 + +1. Both GNU make and MinGW mingw32-make. +2. Boost 1.69.0 or higher only. +''' + +import argparse +from enum import Enum + + +class ToolchainType(Enum): + AUTO = 'auto' # This most commonly means GCC on Linux and MSVC on Windows. + MSVC = 'msvc' # Force MSVC. + GCC = 'gcc' # Force GCC. + MINGW = 'mingw' # As in MinGW-w64; GCC with the PLATFORM-w64-mingw32 prefix. + CLANG = 'clang' + CLANG_CL = 'clang-cl' + + def __str__(self): + return str(self.value) + + @staticmethod + def all(): + return tuple(ToolchainType) + + @staticmethod + def parse(s): + try: + return ToolchainType(s) + except ValueError as e: + raise argparse.ArgumentTypeError(f'invalid toolset: {s}') from e -- cgit v1.2.3