# 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. ''' # 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): 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(ToolsetHint) @staticmethod def parse(s): try: 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 bootstrap_bat_args(): return [] @staticmethod def 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(Toolset): @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 BoostCustom(Toolset): 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 = BoostCustom.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 b2_toolset(self): if self.version: return f'{self.compiler}-{self.version}' return self.compiler def b2_toolset_arg(self): return f'toolset={self.b2_toolset()}' @contextmanager def _b2_write_config(self): config_file = temp_file(prefix='user_config_', suffix='.jam') with config_file as config_path: config = self.b2_format_config() logging.info('Using user config:\n%s', config) with open(config_path, mode='w') as fd: fd.write(config) yield config_path def _b2_format_build_options(self): return ''.join(f'\n <{name}>{val}' for name, val in self.build_options) def b2_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._b2_format_build_options()} ;''' @contextmanager def b2_args(self): with self._b2_write_config() as config_path: args = [] args.append(self.b2_toolset_arg()) args.append(f'--user-config={config_path}') yield args class CMakeCustom(Toolset): @staticmethod def cmake_generator(): return CMakeCustom.makefiles() @staticmethod def makefiles(): 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 nmake_or_makefiles(): 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 CMakeCustom.cmake_generator() @staticmethod def _cmake_write_config(build_dir, contents): path = os.path.join(build_dir, 'custom_toolchain.cmake') with open(path, mode='w') as file: file.write(contents) return path @abc.abstractmethod def cmake_format_config(self, platform): pass def cmake_args(self, build_dir, platform): contents = self.cmake_format_config(platform) config_path = self._cmake_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.cmake_generator(), ] class GCC(BoostCustom, CMakeCustom): # Force GCC. We don't care whether it's a native Linux GCC or a # MinGW-flavoured GCC on Windows. def __init__(self): BoostCustom.__init__(self, 'gcc', 'g++', self.b2_build_options()) CMakeCustom.__init__(self) @staticmethod def bootstrap_bat_args(): return ['gcc'] @staticmethod def bootstrap_sh_args(): return ['--with-toolset=gcc'] @staticmethod def b2_build_options(): return [] def cmake_format_config(self, platform): return f''' set(CMAKE_C_COMPILER gcc) set(CMAKE_CXX_COMPILER g++) {platform.cmake_toolset_file()}''' def _gcc_or_auto(): if shutil.which('gcc') is not None: return ['gcc'] return [] class MinGW(BoostCustom, CMakeCustom): # 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) BoostCustom.__init__(self, 'gcc', self.paths.gxx(), self.b2_build_options()) CMakeCustom.__init__(self) @staticmethod def bootstrap_bat_args(): # On Windows, prefer GCC if it's available. return _gcc_or_auto() @staticmethod def bootstrap_sh_args(): return [] @staticmethod def b2_build_options(): return GCC.b2_build_options() def cmake_format_config(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(BoostCustom, CMakeCustom): def __init__(self): BoostCustom.__init__(self, 'clang', 'clang++', self.b2_build_options()) CMakeCustom.__init__(self) @staticmethod def 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 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 b2_build_options(): options = GCC.b2_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 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 b2_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" ; {BoostCustom.b2_format_config(self)} ''' def cmake_format_config(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.cmake_toolset_file()}''' @staticmethod def cmake_generator(): return CMakeCustom.nmake_or_makefiles() class ClangCL(CMakeCustom): @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 bootstrap_bat_args(): return Clang.bootstrap_bat_args() @staticmethod def bootstrap_sh_args(): return Clang.bootstrap_sh_args() def cmake_format_config(self, platform): return f''' set(CMAKE_C_COMPILER clang-cl) set(CMAKE_CXX_COMPILER clang-cl) set(CMAKE_SYSTEM_NAME Windows) {platform.cmake_toolset_file()}''' @staticmethod def cmake_generator(): return Clang.cmake_generator()