From ce359a1ae006b588b4427ef8784224a36ada4caf Mon Sep 17 00:00:00 2001 From: Egor Tensin Date: Fri, 7 May 2021 16:48:07 +0300 Subject: project.toolset: support versioned MSVC toolsets You can now use something like msvc-141, vs-2017, etc. --- project/boost/build.py | 18 ++-- project/boost/directory.py | 6 +- project/ci/boost.py | 2 +- project/ci/cmake.py | 2 +- project/ci/dirs.py | 4 +- project/cmake/build.py | 19 ++-- project/toolset.py | 244 ++++++++++++++++++++++++++++++++++++++++----- 7 files changed, 244 insertions(+), 51 deletions(-) (limited to 'project') diff --git a/project/boost/build.py b/project/boost/build.py index 53fdf42..cb3688c 100644 --- a/project/boost/build.py +++ b/project/boost/build.py @@ -35,7 +35,7 @@ from project.configuration import Configuration from project.linkage import Linkage from project.os import on_linux_like from project.platform import Platform -from project.toolset import Toolset, ToolsetHint +from project.toolset import Toolset, ToolsetVersion from project.utils import normalize_path, setup_logging @@ -45,6 +45,7 @@ DEFAULT_CONFIGURATIONS = (Configuration.DEBUG, Configuration.RELEASE,) # binaries from a CI, etc. and run them everywhere): DEFAULT_LINK = (Linkage.STATIC,) DEFAULT_RUNTIME_LINK = Linkage.STATIC +DEFAULT_TOOLSET_VERSION = ToolsetVersion.default() B2_QUIET = ['warnings=off', '-d0'] B2_VERBOSE = ['warnings=all', '-d2', '--debug-configuration'] @@ -52,7 +53,7 @@ B2_VERBOSE = ['warnings=all', '-d2', '--debug-configuration'] class BuildParameters: def __init__(self, boost_dir, build_dir=None, platforms=None, configurations=None, link=None, runtime_link=None, - toolset_hint=None, verbose=False, b2_args=None): + toolset_version=None, verbose=False, b2_args=None): boost_dir = normalize_path(boost_dir) if build_dir is not None: @@ -61,7 +62,7 @@ class BuildParameters: configurations = configurations or DEFAULT_CONFIGURATIONS link = link or DEFAULT_LINK runtime_link = runtime_link or DEFAULT_RUNTIME_LINK - toolset_hint = toolset_hint or ToolsetHint.AUTO + toolset_version = toolset_version or DEFAULT_TOOLSET_VERSION verbosity = B2_VERBOSE if verbose else B2_QUIET if b2_args: b2_args = verbosity + b2_args @@ -74,7 +75,7 @@ class BuildParameters: self.configurations = configurations self.link = link self.runtime_link = runtime_link - self.toolset_hint = toolset_hint + self.toolset_version = toolset_version self.b2_args = b2_args @staticmethod @@ -84,7 +85,7 @@ class BuildParameters: def enum_b2_args(self): with self._create_build_dir() as build_dir: for platform in self.platforms: - toolset = Toolset.make(self.toolset_hint, platform) + toolset = Toolset.make(self.toolset_version, platform) for configuration in self.configurations: for link, runtime_link in self._enum_linkage_options(): with self._b2_args(build_dir, toolset, platform, configuration, link, runtime_link) as args: @@ -167,10 +168,9 @@ def _parse_args(argv=None): type=Linkage.parse, default=DEFAULT_RUNTIME_LINK, help=f'how the libraries link to the runtime ({linkage_options})') - toolset_options = '/'.join(map(str, ToolsetHint.all())) - parser.add_argument('--toolset', metavar='TOOLSET', dest='toolset_hint', - type=ToolsetHint.parse, default=ToolsetHint.AUTO, - help=f'toolset to use ({toolset_options})') + parser.add_argument('--toolset', metavar='TOOLSET', dest='toolset_version', + type=ToolsetVersion.parse, default=DEFAULT_TOOLSET_VERSION, + help=f'toolset to use ({ToolsetVersion.usage()})') parser.add_argument('--build', metavar='DIR', dest='build_dir', type=normalize_path, diff --git a/project/boost/directory.py b/project/boost/directory.py index 4da58a7..be633fe 100644 --- a/project/boost/directory.py +++ b/project/boost/directory.py @@ -33,7 +33,7 @@ class BoostDir: def bootstrap(self, params): with self._go(): - run([self._bootstrap_path()] + self._bootstrap_args(params.toolset_hint)) + run([self._bootstrap_path()] + self._bootstrap_args(params.toolset_version)) def _b2(self, params): for b2_params in params.enum_b2_args(): @@ -51,8 +51,8 @@ class BoostDir: return f'bootstrap{ext}' @staticmethod - def _bootstrap_args(hint): - toolset = Toolset.detect(hint) + def _bootstrap_args(toolset_version): + toolset = Toolset.detect(toolset_version) if on_windows(): return toolset.bootstrap_bat_args() return toolset.bootstrap_sh_args() diff --git a/project/ci/boost.py b/project/ci/boost.py index 8784e19..0d89b75 100644 --- a/project/ci/boost.py +++ b/project/ci/boost.py @@ -59,7 +59,7 @@ def build_ci(dirs, argv=None): configurations=(dirs.get_configuration(),), link=args.link, runtime_link=args.runtime_link, - toolset_hint=dirs.get_toolset(), + toolset_version=dirs.get_toolset(), b2_args=args.b2_args) build(params) diff --git a/project/ci/cmake.py b/project/ci/cmake.py index 9234a39..1f49f78 100644 --- a/project/ci/cmake.py +++ b/project/ci/cmake.py @@ -63,7 +63,7 @@ def build_ci(dirs, argv=None): platform=dirs.get_platform(), configuration=dirs.get_configuration(), boost_dir=boost_dir, - toolset_hint=dirs.get_toolset(), + toolset_version=dirs.get_toolset(), cmake_args=dirs.get_cmake_args() + args.cmake_args) build(params) diff --git a/project/ci/dirs.py b/project/ci/dirs.py index 7e3ce03..eb4651b 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.toolset import ToolsetHint +from project.toolset import ToolsetVersion from project.utils import env @@ -47,7 +47,7 @@ class Dirs(abc.ABC): @staticmethod def get_toolset(): if 'TOOLSET' in os.environ: - return ToolsetHint.parse(os.environ['TOOLSET']) + return ToolsetVersion.parse(os.environ['TOOLSET']) return None @staticmethod diff --git a/project/cmake/build.py b/project/cmake/build.py index 7513336..c9e798b 100644 --- a/project/cmake/build.py +++ b/project/cmake/build.py @@ -28,13 +28,13 @@ import tempfile from project.configuration import Configuration from project.platform import Platform -from project.toolset import Toolset, ToolsetHint +from project.toolset import Toolset, ToolsetVersion from project.utils import normalize_path, mkdir_parent, run, setup_logging DEFAULT_PLATFORM = Platform.AUTO DEFAULT_CONFIGURATION = Configuration.DEBUG -DEFAULT_TOOLSET_HINT = ToolsetHint.AUTO +DEFAULT_TOOLSET_VERSION = ToolsetVersion.default() # This way of basically passing `-j` to make is more universal compared to @@ -132,7 +132,7 @@ class BuildPhase: class BuildParameters: def __init__(self, src_dir, build_dir=None, install_dir=None, platform=None, configuration=None, boost_dir=None, - toolset_hint=None, cmake_args=None): + toolset_version=None, cmake_args=None): src_dir = normalize_path(src_dir) if build_dir is not None: @@ -143,7 +143,7 @@ class BuildParameters: configuration = configuration or DEFAULT_CONFIGURATION if boost_dir is not None: boost_dir = normalize_path(boost_dir) - toolset_hint = toolset_hint or DEFAULT_TOOLSET_HINT + toolset_version = toolset_version or DEFAULT_TOOLSET_VERSION cmake_args = cmake_args or [] self.src_dir = src_dir @@ -152,7 +152,7 @@ class BuildParameters: self.platform = platform self.configuration = configuration self.boost_dir = boost_dir - self.toolset_hint = toolset_hint + self.toolset_version = toolset_version self.cmake_args = cmake_args @staticmethod @@ -178,7 +178,7 @@ class BuildParameters: def build(params): with params.create_build_dir() as build_dir: - toolset = Toolset.make(params.toolset_hint, params.platform) + toolset = Toolset.make(params.toolset_version, params.platform) gen_phase = GenerationPhase(params.src_dir, build_dir, install_dir=params.install_dir, @@ -221,10 +221,9 @@ def _parse_args(argv=None): type=normalize_path, help='set Boost directory path') - toolset_options = '/'.join(map(str, ToolsetHint.all())) - parser.add_argument('--toolset', metavar='TOOLSET', dest='toolset_hint', - type=ToolsetHint.parse, default=ToolsetHint.AUTO, - help=f'toolset to use ({toolset_options})') + parser.add_argument('--toolset', metavar='TOOLSET', dest='toolset_version', + type=ToolsetVersion.parse, default=DEFAULT_TOOLSET_VERSION, + help=f'toolset to use ({ToolsetVersion.usage()})') parser.add_argument('src_dir', metavar='DIR', type=normalize_path, diff --git a/project/toolset.py b/project/toolset.py index d1a2d29..b9cb6c3 100644 --- a/project/toolset.py +++ b/project/toolset.py @@ -28,6 +28,7 @@ limited to: import abc import argparse from contextlib import contextmanager +from decimal import Decimal from enum import Enum import logging import os.path @@ -39,9 +40,119 @@ from project.platform import Platform from project.utils import temp_file -class ToolsetHint(Enum): +class MSVCVersion(Enum): + # It's the "toolset" version, or whatever that is. + # Source: https://cmake.org/cmake/help/v3.20/variable/MSVC_TOOLSET_VERSION.html#variable:MSVC_TOOLSET_VERSION + + VS2005 = '80' + VS2008 = '90' + VS2010 = '100' + VS2012 = '110' + VS2013 = '120' + VS2015 = '140' + VS2017 = '141' + VS2019 = '142' + + def __str__(self): + return str(self.value) + + @staticmethod + def parse(s): + try: + return MSVCVersion(s) + except ValueError as e: + raise argparse.ArgumentTypeError(f'invalid MSVC version: {s}') from e + + @staticmethod + def all(): + return tuple(MSVCVersion) + + def to_msvc_version(self): + return self + + def to_visual_studio_version(self): + if MSVCVersion.VS2005: + return VisualStudioVersion.VS2005 + if MSVCVersion.VS2008: + return VisualStudioVersion.VS2008 + if MSVCVersion.VS2010: + return VisualStudioVersion.VS2010 + if MSVCVersion.VS2012: + return VisualStudioVersion.VS2012 + if MSVCVersion.VS2013: + return VisualStudioVersion.VS2013 + if MSVCVersion.VS2015: + return VisualStudioVersion.VS2015 + if MSVCVersion.VS2017: + return VisualStudioVersion.VS2017 + if MSVCVersion.VS2019: + return VisualStudioVersion.VS2019 + raise NotImplementedError(f'unsupported MSVC version: {self}') + + def to_boost_msvc_version(self): + try: + numeric = int(self.value) + except ValueError: + raise RuntimeError(f'what? MSVC versions are supposed to be integers: {self.value}') + numeric = Decimal(numeric) / 10 + numeric = numeric.quantize(Decimal('1.0')) + return str(numeric) + + def to_cmake_toolset(self): + return f'v{self}' + + +class VisualStudioVersion(Enum): + VS2005 = '2005' + VS2008 = '2008' + VS2010 = '2010' + VS2012 = '2012' + VS2013 = '2013' + VS2015 = '2015' + VS2017 = '2017' + VS2019 = '2019' + + def __str__(self): + return str(self.value) + + @staticmethod + def parse(s): + try: + return VisualStudioVersion(s) + except ValueError as e: + raise argparse.ArgumentTypeError(f'invalid Visual Studio version: {s}') from e + + @staticmethod + def all(): + return tuple(VisualStudioVersion) + + def to_msvc_version(self): + if self is VisualStudioVersion.VS2005: + return MSVCVersion.VS2005 + if self is VisualStudioVersion.VS2008: + return MSVCVersion.VS2008 + if self is VisualStudioVersion.VS2010: + return MSVCVersion.VS2010 + if self is VisualStudioVersion.VS2012: + return MSVCVersion.VS2012 + if self is VisualStudioVersion.VS2013: + return MSVCVersion.VS2013 + if self is VisualStudioVersion.VS2015: + return MSVCVersion.VS2015 + if self is VisualStudioVersion.VS2017: + return MSVCVersion.VS2017 + if self is VisualStudioVersion.VS2019: + return MSVCVersion.VS2019 + raise NotImplementedError(f'unsupported Visual Studio version: {self}') + + def to_visual_studio_version(self): + return self + + +class ToolsetType(Enum): AUTO = 'auto' # This most commonly means GCC on Linux and MSVC on Windows. MSVC = 'msvc' # Force MSVC. + VISUAL_STUDIO = 'visual-studio' # Same as 'msvc'. GCC = 'gcc' # Force GCC. MINGW = 'mingw' # As in MinGW-w64; GCC with the PLATFORM-w64-mingw32 prefix. CLANG = 'clang' @@ -50,21 +161,84 @@ class ToolsetHint(Enum): def __str__(self): return str(self.value) + @staticmethod + def parse(s): + try: + return ToolsetType(s) + except ValueError as e: + raise argparse.ArgumentTypeError(f'invalid toolset: {s}') from e + + @property + def supports_version(self): + if self is ToolsetType.MSVC or self is ToolsetType.VISUAL_STUDIO: + return True + return False + + def parse_version(self, s): + if self is ToolsetType.MSVC: + return MSVCVersion.parse(s) + if self is ToolsetType.VISUAL_STUDIO: + return VisualStudioVersion.parse(s) + raise RuntimeError(f"this toolset doesn't support versions: {self}") + @staticmethod def all(): - return tuple(ToolsetHint) + return tuple(ToolsetType) + + @staticmethod + def versioned(): + return (t for t in ToolsetType.all() if t.supports_version) + + @staticmethod + def non_versioned(): + return (t for t in ToolsetType.all() if not t.supports_version) + + +class ToolsetVersion: + _VERSION_SEP = '-' + + def __init__(self, hint, version): + self.hint = hint + self.version = version + + def __str__(self): + if self.version is None: + return str(self.hint) + return f'{self.hint}{ToolsetVersion._VERSION_SEP}{self.version}' + + @staticmethod + def default(): + return ToolsetVersion(ToolsetType.AUTO, None) + + @staticmethod + def all_usage_placeholders(): + for hint in ToolsetType.all(): + if hint.supports_version: + yield f'{hint}[{ToolsetVersion._VERSION_SEP}VERSION]' + else: + yield str(hint) + + @staticmethod + def usage(): + return '/'.join(ToolsetVersion.all_usage_placeholders()) @staticmethod def parse(s): try: - return ToolsetHint(s) - except ValueError as e: - raise argparse.ArgumentTypeError(f'invalid toolset: {s}') from e + return ToolsetVersion(ToolsetType(s), None) + except ValueError: + pass + for hint in ToolsetType.versioned(): + prefix = f'{hint}{ToolsetVersion._VERSION_SEP}' + if s.startswith(prefix): + return ToolsetVersion(hint, hint.parse_version(s[len(prefix):])) + raise argparse.ArgumentTypeError(f'invalid toolset: {s}') class Toolset(abc.ABC): + @staticmethod @contextmanager - def b2_args(self): + def b2_args(): # Write the config file, etc. yield [] @@ -77,36 +251,45 @@ class Toolset(abc.ABC): return [] @staticmethod - def cmake_args(build_dir, platform): - return [] + def cmake_generator(): + return None + + def cmake_args(self, build_dir, platform): + args = [] + generator = self.cmake_generator() + if generator is not None: + args += ['-G', generator] + return args @staticmethod def build_system_args(): return [] @staticmethod - def detect(hint): - if hint is ToolsetHint.AUTO: + def detect(version): + if version.hint is ToolsetType.AUTO: return Auto - if hint is ToolsetHint.MSVC: + if version.hint is ToolsetType.MSVC or version.hint is ToolsetType.VISUAL_STUDIO: return MSVC - if hint is ToolsetHint.GCC: + if version.hint is ToolsetType.GCC: return GCC - if hint is ToolsetHint.MINGW: + if version.hint is ToolsetType.MINGW: return MinGW - if hint is ToolsetHint.CLANG: + if version.hint is ToolsetType.CLANG: return Clang - if hint is ToolsetHint.CLANG_CL: + if version.hint is ToolsetType.CLANG_CL: return ClangCL - raise NotImplementedError(f'unrecognized toolset: {hint}') + raise NotImplementedError(f'unrecognized toolset: {version}') @staticmethod - def make(hint, platform): + def make(version, platform): # Platform is required here, since some toolsets (MinGW-w64) require # it for the compiler path. - cls = Toolset.detect(hint) + cls = Toolset.detect(version) if cls is MinGW: return MinGW(platform) + if version.hint.supports_version: + return cls(version.version) return cls() @@ -124,23 +307,35 @@ class Auto(Toolset): # 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 [] + return super().cmake_args(build_dir, platform) # 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): + def __init__(self, version=None): + self.version = version + + def b2_toolset(self): + if self.version is not None: + return f'msvc-{self.version.to_msvc_version().to_boost_msvc_version()}' + return 'msvc' + @contextmanager def b2_args(self): - yield ['toolset=msvc'] + yield [f'toolset={self.b2_toolset()}'] # 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()] + args = super().cmake_args(build_dir, platform) + args += ['-A', platform.msvc_arch()] + if self.version is not None: + args += ['-T', self.version.to_msvc_version().to_cmake_toolset()] + return args def _full_exe_name(exe): @@ -224,6 +419,7 @@ class BoostCustom(Toolset): class CMakeCustom(Toolset): @staticmethod def cmake_generator(): + # The Visual Studio generator is the default on Windows, override it: return CMakeCustom.makefiles() @staticmethod @@ -258,11 +454,9 @@ class CMakeCustom(Toolset): def cmake_args(self, build_dir, platform): contents = self.cmake_format_config(platform) config_path = self._cmake_write_config(build_dir, contents) - return [ + + return super().cmake_args(build_dir, platform) + [ '-D', f'CMAKE_TOOLCHAIN_FILE={config_path}', - # The Visual Studio generator is the default on Windows, override - # it: - '-G', self.cmake_generator(), ] -- cgit v1.2.3