diff options
Diffstat (limited to 'project/toolset.py')
-rw-r--r-- | project/toolset.py | 378 |
1 files changed, 378 insertions, 0 deletions
diff --git a/project/toolset.py b/project/toolset.py index fc4aa85..6332d3e 100644 --- a/project/toolset.py +++ b/project/toolset.py @@ -23,8 +23,20 @@ limited to: 2. Boost 1.69.0 or higher only. ''' +# See docs/{boost,cmake}.md for a more thorough description of my pain. + +import abc import argparse +from contextlib import contextmanager from enum import Enum +import logging +import os.path +import shutil + +import project.mingw +from project.os import on_cygwin, on_linux, on_windows +from project.platform import Platform +from project.utils import temp_file class ToolsetHint(Enum): @@ -48,3 +60,369 @@ class ToolsetHint(Enum): return ToolsetHint(s) except ValueError as e: raise argparse.ArgumentTypeError(f'invalid toolset: {s}') from e + + +class Toolset(abc.ABC): + @contextmanager + def b2_args(self): + # Write the config file, etc. + yield [] + + @staticmethod + def get_bootstrap_bat_args(): + return [] + + @staticmethod + def get_bootstrap_sh_args(): + return [] + + def cmake_args(self, build_dir, platform): + return [] + + def build_system_args(self): + return [] + + @staticmethod + def detect(hint): + if hint is ToolsetHint.AUTO: + return Auto + if hint is ToolsetHint.MSVC: + return MSVC + if hint is ToolsetHint.GCC: + return GCC + if hint is ToolsetHint.MINGW: + return MinGW + if hint is ToolsetHint.CLANG: + return Clang + if hint is ToolsetHint.CLANG_CL: + return ClangCL + raise NotImplementedError(f'unrecognized toolset: {hint}') + + @staticmethod + def make(hint, platform): + # Platform is required here, since some toolsets (MinGW-w64) require + # it for the compiler path. + cls = Toolset.detect(hint) + if cls is MinGW: + return MinGW(platform) + return cls() + + +class Auto(Toolset): + # Let Boost.Build do the detection. Most commonly it means GCC on + # Linux-likes and MSVC on Windows. + + def cmake_args(self, build_dir, platform): + 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. + return MSVC().cmake_args(build_dir, platform) + # 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 [] + # If a specific platform was requested, we might need to set some + # CMake/compiler flags, like -m32/-m64. + return GCC().cmake_args(build_dir, platform) + + +class MSVC(Auto): + @contextmanager + def b2_args(self): + yield ['toolset=msvc'] + + # Note: bootstrap.bat picks up MSVC by default. + + def cmake_args(self, build_dir, platform): + # This doesn't actually specify the generator of course, but I don't + # want to implement VS detection logic. + return ['-A', platform.msvc_arch()] + + +def _full_exe_name(exe): + if 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 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(Toolset): + # Boost.Build toolset defined using a config file. + + 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 Makefile(Toolset): + # One of CMake's "... Makefiles" generator toolsets. + + @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' + + @staticmethod + def _write_config(build_dir, contents): + path = Makefile._get_config_path(build_dir) + with open(path, mode='w') as file: + file.write(contents) + return path + + @abc.abstractmethod + def format_cmake_toolset_file(self, platform): + pass + + def cmake_args(self, build_dir, platform): + contents = self.format_cmake_toolset_file(platform) + config_path = self._write_config(build_dir, contents) + return [ + '-D', f'CMAKE_TOOLCHAIN_FILE={config_path}', + # The Visual Studio generator is the default on Windows, override + # it: + '-G', self._get_makefile_generator(), + ] + + +class GCC(Custom, Makefile): + # Force GCC. We don't care whether it's a native Linux GCC or a + # MinGW-flavoured GCC on Windows. + + def __init__(self): + Custom.__init__(self, 'gcc', 'g++', self.get_build_options()) + Makefile.__init__(self) + + @staticmethod + def get_bootstrap_bat_args(): + return ['gcc'] + + @staticmethod + def get_bootstrap_sh_args(): + return ['--with-toolset=gcc'] + + @staticmethod + def get_build_options(): + return [] + + def format_cmake_toolset_file(self, platform): + return f''' +set(CMAKE_C_COMPILER gcc) +set(CMAKE_CXX_COMPILER g++) +{platform.makefile_toolset_file()}''' + + +def _gcc_or_auto(): + if shutil.which('gcc') is not None: + return ['gcc'] + return [] + + +class MinGW(Custom, Makefile): + # 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): + self.paths = project.mingw.MinGW(platform) + Custom.__init__(self, 'gcc', self.paths.gxx(), self.get_build_options()) + Makefile.__init__(self) + + @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 [] + + @staticmethod + def get_build_options(): + return GCC.get_build_options() + + def format_cmake_toolset_file(self, platform): + return f''' +set(CMAKE_C_COMPILER {self.paths.gcc()}) +set(CMAKE_CXX_COMPILER {self.paths.gxx()}) +set(CMAKE_AR {self.paths.ar()}) +set(CMAKE_RANLIB {self.paths.ranlib()}) +set(CMAKE_RC_COMPILER {self.paths.windres()}) +set(CMAKE_SYSTEM_NAME Windows) +''' + + +class Clang(Custom, Makefile): + def __init__(self): + Custom.__init__(self, 'clang', 'clang++', self.get_build_options()) + Makefile.__init__(self) + + @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 <warnings>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>::char_class_type' (aka 'unsigned int') + ('cxxflags', '-Wno-c++11-narrowing'), + ] + if 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 + <target-os>windows:<define>_MT + <target-os>windows,<variant>debug:<define>_DEBUG + <target-os>windows,<runtime-link>static,<variant>debug:<cxxflags>"-Xclang -flto-visibility-public-std -Xclang --dependent-lib=libcmtd" + <target-os>windows,<runtime-link>static,<variant>release:<cxxflags>"-Xclang -flto-visibility-public-std -Xclang --dependent-lib=libcmt" + <target-os>windows,<runtime-link>shared,<variant>debug:<cxxflags>"-D_DLL -Xclang --dependent-lib=msvcrtd" + <target-os>windows,<runtime-link>shared,<variant>release:<cxxflags>"-D_DLL -Xclang --dependent-lib=msvcrt" +; +{Custom.format_config(self)} +''' + + def format_cmake_toolset_file(self, 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_toolset_file()}''' + + @staticmethod + def _get_makefile_generator(): + 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 Makefile._get_makefile_generator() + + +class ClangCL(Makefile): + @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() + + def format_cmake_toolset_file(self, platform): + return f''' +set(CMAKE_C_COMPILER clang-cl) +set(CMAKE_CXX_COMPILER clang-cl) +set(CMAKE_SYSTEM_NAME Windows) +{platform.makefile_toolset_file()}''' + + @staticmethod + def _get_makefile_generator(): + return Clang._get_makefile_generator() |