From e731e6345d7fa1c2326b18c56f5dc361ea3adbfb Mon Sep 17 00:00:00 2001 From: Egor Tensin Date: Sat, 20 Mar 2021 13:16:15 +0300 Subject: project.platform: add platform 'auto' There were two problems: * On Windows, VS 2019 defaults to x64 while VS 2017 defaults to x86. * Too much focus on x86(-64) might mean that building stuff on ARM can become difficult. These were all addressed by adding a new platform 'auto'. On Windows, it defaults to picking either x64 or x86 (depending on the host arch) for both Boost and CMake. On Linux, it lets the compiler decide what arch to target. --- project/boost/build.py | 15 ++----- project/boost/toolchain.py | 41 +++++++++--------- project/cmake/build.py | 16 +++---- project/cmake/toolchain.py | 37 +++++----------- project/configuration.py | 24 ++++++++++- project/mingw.py | 42 +++++++----------- project/platform.py | 105 ++++++++++++++++++++++++++++++++++++++++++--- 7 files changed, 176 insertions(+), 104 deletions(-) diff --git a/project/boost/build.py b/project/boost/build.py index b262e47..7cf1364 100644 --- a/project/boost/build.py +++ b/project/boost/build.py @@ -40,7 +40,7 @@ from project.os import on_linux_like from project.utils import normalize_path, setup_logging -DEFAULT_PLATFORMS = (Platform.native(),) +DEFAULT_PLATFORMS = (Platform.AUTO,) DEFAULT_CONFIGURATIONS = (Configuration.DEBUG, Configuration.RELEASE,) # For my development, I link everything statically (to be able to pull the # binaries from a CI, etc. and run them everywhere): @@ -71,7 +71,6 @@ class BuildParameters: self.boost_dir = boost_dir self.build_dir = build_dir - self.stage_dir = 'stage' self.platforms = platforms self.configurations = configurations self.link = link @@ -126,12 +125,11 @@ class BuildParameters: def _build_params(self, build_dir, toolchain, configuration, link, runtime_link): params = [] params.append(self._build_dir(build_dir)) - params.append(self._stagedir(toolchain, configuration)) - params.append('--layout=system') - params += toolchain.get_b2_args() - params.append(self._variant(configuration)) params.append(self._link(link)) params.append(self._runtime_link(runtime_link)) + params.append('--layout=system') + params += toolchain.b2_args(configuration) + params += configuration.b2_args() params += self.b2_args return params @@ -139,11 +137,6 @@ class BuildParameters: def _build_dir(build_dir): return f'--build-dir={build_dir}' - def _stagedir(self, toolchain, configuration): - platform = str(toolchain.platform) - configuration = str(configuration) - return f'--stagedir={os.path.join(self.stage_dir, platform, configuration)}' - @staticmethod def _link(link): return f'link={link}' diff --git a/project/boost/toolchain.py b/project/boost/toolchain.py index 08bc49c..5422a62 100644 --- a/project/boost/toolchain.py +++ b/project/boost/toolchain.py @@ -104,11 +104,8 @@ class Toolchain(abc.ABC): def __init__(self, platform): self.platform = platform - def get_b2_args(self): - return [ - # Always pass the address-model explicitly. - f'address-model={self.platform.get_address_model()}' - ] + def b2_args(self, configuration): + return self.platform.b2_args(configuration) @staticmethod @contextmanager @@ -139,8 +136,8 @@ class Auto(Toolchain): class MSVC(Auto): - def get_b2_args(self): - return super().get_b2_args() + [ + def b2_args(self, configuration): + return super().b2_args(configuration) + [ 'toolset=msvc', ] @@ -187,14 +184,16 @@ class BoostBuildToolset: self.options = options @property - def toolset_id(self): + def toolset(self): if self.version: return f'{self.compiler}-{self.version}' return self.compiler - @property - def b2_arg(self): - return f'toolset={self.toolset_id}' + def b2_toolset(self): + return f'toolset={self.toolset}' + + def b2_args(self): + return [self.b2_toolset()] def _format_using_options(self): return ''.join(f'\n <{name}>{val}' for name, val in self.options) @@ -232,13 +231,14 @@ class ConfigFile(Toolchain): with tmp as path: yield cls(platform, path, toolset) - def get_b2_args(self): + def b2_args(self, configuration): # All the required options and the toolset definition should be in the # user configuration file. - return super().get_b2_args() + [ - f'--user-config={self.config_path}', - self.toolset.b2_arg, - ] + args = [] + args += super().b2_args(configuration) + args.append(f'--user-config={self.config_path}') + args += self.toolset.b2_args() + return args class GCC(ConfigFile): @@ -273,8 +273,9 @@ class MinGW(GCC): @staticmethod def get_toolset(platform): - path = project.mingw.get_gxx(platform) - return BoostBuildToolset(MinGW.COMPILER, path, MinGW.get_options()) + paths = project.mingw.MinGW(platform) + compiler = paths.gxx() + return BoostBuildToolset(MinGW.COMPILER, compiler, MinGW.get_options()) class Clang(ConfigFile): @@ -320,8 +321,8 @@ class Clang(ConfigFile): class ClangCL(Toolchain): - def get_b2_args(self): - return super().get_b2_args() + [ + def b2_args(self, configuration): + return super().b2_args(configuration) + [ 'toolset=clang-win', 'define=BOOST_USE_WINDOWS_H', ] diff --git a/project/cmake/build.py b/project/cmake/build.py index 1e9a90d..a2fea78 100644 --- a/project/cmake/build.py +++ b/project/cmake/build.py @@ -34,7 +34,7 @@ from project.toolchain import ToolchainType from project.utils import normalize_path, mkdir_parent, run, setup_logging -DEFAULT_PLATFORM = None +DEFAULT_PLATFORM = Platform.AUTO DEFAULT_CONFIGURATION = Configuration.DEBUG DEFAULT_TOOLSET = ToolchainType.AUTO @@ -81,18 +81,14 @@ class GenerationPhase: def _get_boost_args(self): if self.boost_dir is None: return [] - stagedir = self._stagedir(self.boost_dir, self.platform, self.configuration) + root = self.boost_dir + librarydir = self.platform.boost_librarydir(self.configuration) + librarydir = os.path.join(self.boost_dir, librarydir) return [ - '-D', f'BOOST_ROOT={self.boost_dir}', - '-D', f'BOOST_LIBRARYDIR={stagedir}', + '-D', f'BOOST_ROOT={root}', + '-D', f'BOOST_LIBRARYDIR={librarydir}', ] - @staticmethod - def _stagedir(boost_dir, platform, configuration): - if platform is None: - platform = Platform.native() - return os.path.join(boost_dir, 'stage', str(platform), str(configuration), 'lib') - def run(self, toolchain): run_cmake(self._cmake_args(toolchain)) diff --git a/project/cmake/toolchain.py b/project/cmake/toolchain.py index 14197aa..0e68738 100644 --- a/project/cmake/toolchain.py +++ b/project/cmake/toolchain.py @@ -11,7 +11,6 @@ import shutil import project.mingw from project.os import on_windows -from project.platform import Platform from project.toolchain import ToolchainType @@ -68,11 +67,9 @@ class MSVC(Auto): self.platform = platform def get_cmake_args(self): - if self.platform is None: - return [] # This doesn't actually specify the generator of course, but I don't # want to implement VS detection logic. - return ['-A', self.platform.get_cmake_arch()] + return self.platform.cmake_msvc_args() def get_build_args(self): return ['/m'] @@ -102,17 +99,6 @@ class Makefile(Toolchain): file.write(contents) return cls(path) - @staticmethod - def _format_platform_compiler_flags(platform): - if platform is None: - # If the platform wasn't specified, don't use the -m flag, etc. - return '' - # Otherwise, use the standard -m32/-m64 flags. - return f''' -set(CMAKE_C_FLAGS -m{platform.get_address_model()}) -set(CMAKE_CXX_FLAGS -m{platform.get_address_model()}) -''' - def get_cmake_args(self): return [ '-D', f'CMAKE_TOOLCHAIN_FILE={self.path}', @@ -131,7 +117,7 @@ class GCC(Makefile): return f''' set(CMAKE_C_COMPILER gcc) set(CMAKE_CXX_COMPILER g++) -{Makefile._format_platform_compiler_flags(platform)}''' +{platform.makefile_toolchain_file()}''' @staticmethod def setup(platform, build_dir): @@ -141,16 +127,13 @@ set(CMAKE_CXX_COMPILER g++) class MinGW(Makefile): @staticmethod def _format(platform): - if platform is None: - # MinGW only supports x86/x64, plus we need the platform for the - # compiler file name, so default to x64 unless specified. - platform = Platform.X64 + paths = project.mingw.MinGW(platform) return f''' -set(CMAKE_C_COMPILER {project.mingw.get_gcc(platform)}) -set(CMAKE_CXX_COMPILER {project.mingw.get_gxx(platform)}) -set(CMAKE_AR {project.mingw.get_ar(platform)}) -set(CMAKE_RANLIB {project.mingw.get_ranlib(platform)}) -set(CMAKE_RC_COMPILER {project.mingw.get_windres(platform)}) +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) ''' @@ -170,7 +153,7 @@ else() set(CMAKE_C_COMPILER clang) set(CMAKE_CXX_COMPILER clang++) endif() -{Makefile._format_platform_compiler_flags(platform)}''' +{platform.makefile_toolchain_file()}''' def _get_makefile_generator(self): if on_windows(): @@ -192,7 +175,7 @@ class ClangCL(Clang): set(CMAKE_C_COMPILER clang-cl) set(CMAKE_CXX_COMPILER clang-cl) set(CMAKE_SYSTEM_NAME Windows) -{Makefile._format_platform_compiler_flags(platform)}''' +{platform.makefile_toolchain_file()}''' @staticmethod def setup(platform, build_dir): diff --git a/project/configuration.py b/project/configuration.py index 4b25c6e..5bfc317 100644 --- a/project/configuration.py +++ b/project/configuration.py @@ -29,7 +29,7 @@ class Configuration(Enum): except ValueError as e: raise argparse.ArgumentTypeError(f'invalid configuration: {s}') from e - def to_boost_variant(self): + def variant(self): '''Roughly maps CMake's CMAKE_BUILD_TYPE to Boost's variant. AFAIK, Boost only supports debug/release, MinSizeRel and RelWithDebInfo @@ -38,5 +38,25 @@ class Configuration(Enum): MinSizeRel/RelWithDebInfo. ''' if self in (Configuration.MINSIZEREL, Configuration.RELWITHDEBINFO): - return Configuration.RELEASE.to_boost_variant() + return Configuration.RELEASE.variant() return str(self).lower() + + def b2_variant(self): + return [f'variant={self.variant()}'] + + def b2_args(self): + args = [] + args += self.b2_variant() + return args + + def build_type(self): + '''Maps to CMAKE_BUILD_TYPE.''' + return str(self) + + def cmake_build_type(self): + return ['-D', f'CMAKE_BUILD_TYPE={self.build_type()}'] + + def cmake_args(self): + args = [] + args += self.cmake_build_type() + return args diff --git a/project/mingw.py b/project/mingw.py index 731cee9..4ac921f 100644 --- a/project/mingw.py +++ b/project/mingw.py @@ -4,36 +4,24 @@ # Distributed under the MIT License. -def _get_compiler_prefix(platform): - target_arch = platform.get_address_model() - if target_arch == 32: - return 'i686' - if target_arch == 64: - return 'x86_64' - raise RuntimeError(f'unexpected address model: {target_arch}') +class MinGW: + def __init__(self, platform): + self.prefix = platform.mingw_prefix() + def _get(self, what): + return f'{self.prefix}-w64-mingw32-{what}' -def _get(platform, what): - prefix = _get_compiler_prefix(platform) - path = f'{prefix}-w64-mingw32-{what}' - return path + def gcc(self): + return self._get('gcc') + def gxx(self): + return self._get('g++') -def get_gcc(platform): - return _get(platform, 'gcc') + def ar(self): + return self._get('gcc-ar') + def ranlib(self): + return self._get('gcc-ranlib') -def get_gxx(platform): - return _get(platform, 'g++') - - -def get_ar(platform): - return _get(platform, 'gcc-ar') - - -def get_ranlib(platform): - return _get(platform, 'gcc-ranlib') - - -def get_windres(platform): - return _get(platform, 'windres') + def windres(self): + return self._get('windres') diff --git a/project/platform.py b/project/platform.py index d25827b..b19c93a 100644 --- a/project/platform.py +++ b/project/platform.py @@ -6,19 +6,29 @@ import argparse from enum import Enum import platform +import os.path +from project.os import on_windows -class Platform(Enum): - '''I only build for x86(-64), so here it goes.''' +class Platform(Enum): + # I only build for x86(-64), so here it goes. X86 = 'x86' X64 = 'x64' + # 'auto' means that no additional arguments will be passed to either + # Boost's b2 nor CMake (except on Windows, see below). + AUTO = 'auto' def __str__(self): return str(self.value) @staticmethod - def native(): + def windows_native(): + # On Windows, no explicit platform would mean x64 for VS 2019 and x86 + # for VS 2017. To account for this discrepancy, it is assumed that + # Windows builds can only target either x86 or x64 (which I don't think + # is true?), and we default to x64 most of the time. + # # Source: https://stackoverflow.com/a/12578715/514684 if platform.machine().endswith('64'): return Platform.X64 @@ -26,30 +36,111 @@ class Platform(Enum): @staticmethod def all(): - return tuple(Platform) + return Platform.X86, Platform.X64, @staticmethod def parse(s): try: if s == 'Win32': - # AppVeyor convention: + # Visual Studio/AppVeyor convention: return Platform.X86 return Platform(s) except ValueError as e: raise argparse.ArgumentTypeError(f'invalid platform: {s}') from e - def get_address_model(self): + def mingw_prefix(self): + if self is Platform.AUTO: + if on_windows(): + # On Windows, use the host architecture. + return Platform.windows_native().mingw_prefix() + # On Linux, assume that the target is x64. + return Platform.X64.mingw_prefix() + if self is Platform.X86: + return 'i686' + if self is Platform.X64: + return 'x86_64' + raise NotImplementedError(f'unsupported platform: {self}') + + def address_model(self): '''Maps to Boost's address-model.''' + if self is Platform.AUTO: + if on_windows(): + # On Windows, use the host architecture. + return Platform.windows_native().address_model() + # On Linux, assume that the target is x64. + raise RuntimeError('cannot determine address model unless the target platform is specified explicitly') if self is Platform.X86: return 32 if self is Platform.X64: return 64 raise NotImplementedError(f'unsupported platform: {self}') - def get_cmake_arch(self): + def stagedir(self, configuration): + '''Path to the built libraries inside the Boost build directory.''' + if self is Platform.AUTO: + if on_windows(): + # On Windows, use the host architecture. + return Platform.windows_native().stagedir(configuration) + # On Linux, the libraries are stored in stage/auto/CONFIGURATION/lib. + return os.path.join('stage', str(self), str(configuration)) + + def boost_librarydir(self, configuration): + '''Same as above, but for CMake; adds /lib/ at the end.''' + return os.path.join(self.stagedir(configuration), 'lib') + + def b2_address_model(self): + if self is Platform.AUTO and not on_windows(): + # On Linux, don't specify the architecture explicitly (it is + # assumed that the host architecture will be targeted). + return [] + return [f'address-model={self.address_model()}'] + + def b2_stagedir(self, configuration): + return [f'--stagedir={self.stagedir(configuration)}'] + + def b2_args(self, configuration): + args = [] + args += self.b2_address_model() + args += self.b2_stagedir(configuration) + return args + + def makefile_toolchain_file(self): + # For Makefile generators, we make a special toolchain file that + # specifies the -m32/-m64 flags, etc. + if self is Platform.AUTO: + # Let the compiler decide. + return '' + if self is Platform.X86: + address_model = 32 + elif self is Platform.X64: + address_model = 64 + else: + raise NotImplementedError(f'unsupported platform: {self}') + return f''' +set(CMAKE_C_FLAGS -m{address_model}) +set(CMAKE_CXX_FLAGS -m{address_model}) +''' + + def msvc_arch(self): '''Maps to CMake's -A argument for MSVC.''' + if self is Platform.AUTO: + if on_windows(): + # On Windows, use the host architecture. + return Platform.windows_native().msvc_arch() + # I don't think the -A argument is supported on any generators + # except the Visual Studio ones. + raise RuntimeError('-A parameter is only supported for Visual Studio generators') if self is Platform.X86: return 'Win32' if self is Platform.X64: return 'x64' raise NotImplementedError(f'unsupported platform: {self}') + + def cmake_msvc_arch(self): + return ['-A', self.msvc_arch()] + + def cmake_msvc_args(self): + # When using the MSVC toolset, pass the appropriate -A flag. + args = [] + args += self.cmake_msvc_arch() + return args -- cgit v1.2.3