diff --git a/steps/cmake/cmake_options.py b/steps/cmake/cmake_options.py deleted file mode 100644 index 28681338..00000000 --- a/steps/cmake/cmake_options.py +++ /dev/null @@ -1,27 +0,0 @@ -from enum import Enum - -from .flags import CMakeFlag - - -# Explicitly enumerate CMake flags so that we can do type checking and avoid -# typos, as it's very easy to have a typo go unnoticed. -class CMAKE(Enum): - AR = 'AR' - BUILD_TYPE = 'BUILD_TYPE' - CXX_COMPILER = 'CXX_COMPILER', - CXX_FLAGS = 'CXX_FLAGS' - C_COMPILER = 'C_COMPILER', - C_FLAGS = 'C_FLAGS' - C_COMPILER_LAUNCHER = 'C_COMPILER_LAUNCHER' - CXX_COMPILER_LAUNCHER = 'CXX_COMPILER_LAUNCHER' - INSTALL_PREFIX = 'INSTALL_PREFIX' - LIBRARY_PATH = 'LIBRARY_PATH' - - -# Any option of the form: -# -DCMAKE_XXX -class CMakeOption(CMakeFlag): - def __init__(self, name: CMAKE, value: str): - assert isinstance(name, CMAKE) - assert isinstance(value, str) - self.super().__init__(name=f'CMAKE_{name}', value=value) diff --git a/steps/cmake/compilers.py b/steps/cmake/compilers.py index 5717f99e..4c74ef02 100644 --- a/steps/cmake/compilers.py +++ b/steps/cmake/compilers.py @@ -1,5 +1,7 @@ class CompilerCommand: def __init__(self, cc: str, cxx: str): + assert isinstance(cc, str) + assert isinstance(cxx, str) self.cc_ = cc self.cxx_ = cxx diff --git a/steps/cmake/flags.py b/steps/cmake/flags.py deleted file mode 100644 index f0bb84b3..00000000 --- a/steps/cmake/flags.py +++ /dev/null @@ -1,16 +0,0 @@ -class CMakeFlag: - def __init__(self, name: str, value: str): - assert isinstance(name, str) - assert isinstance(value, str) - self.name = name - if ' ' in value: # Values containing spaces need quoting. - self.value = f'"{value}"' - else: - self.value = value - - -class BooleanCMakeFlag(CMakeFlag): - def __init__(self, name: str, enabled: bool): - assert isinstance(name, str) - assert isinstance(enabled, bool) - self.super().__init__(name=name, value='ON' if enabled else 'OFF') diff --git a/steps/cmake/generator.py b/steps/cmake/generator.py index 6c123fbc..426abde5 100644 --- a/steps/cmake/generator.py +++ b/steps/cmake/generator.py @@ -1,53 +1,94 @@ from typing import Iterable -from .cmake_options import ( - CMAKE, -) from .compilers import CompilerCommand -from .options import CMakeFlag, CMakeOption +from .options import CMAKE, OTHER, BuildConfig, CMakeOption class DuplicateFlagException(Exception): - pass + def __init__(self, flag_name: str, existing_value: str, new_value: str): + super().__init__( + f"Duplicate flag detected: {flag_name}" + f"(existing: {existing_value}, new: {new_value})" + ) + super().__init__(f"Duplicate flag detected: {flag_name}") class CMakeGenerator: - # Default path is "in source build" - def __init__(self, flags: Iterable[CMakeFlag], + ''' + Generates a CMake command with specified flags. + ''' + + def __init__(self, flags: Iterable[CMakeOption], source_path: str = '.'): - # Use a dictionary to only allow flags to specified once. - self.flags: dict[str, CMakeFlag] = {} + ''' + Initializes the CMakeGenerator with an optional list of flags. + + Args: + flags: An iterable of CMakeFlag objects. + source_path: The source path to the base CMakeLists.txt file. + Default path is "in source build". + ''' + self.flags: dict[str, CMakeOption] = {} self.source_path = source_path - for flag in flags: - self.flags[flag.name] = self.flags[flag] + self.append_flags(flags) def set_compiler(self, compiler: CompilerCommand): + ''' + Sets the compiler options for C and C++ compilers. + + Args: + compiler: An instance of CompilerCommand. + ''' assert isinstance(compiler, CompilerCommand) self.append_flags([ CMakeOption(CMAKE.C_COMPILER, compiler.cc), - CMakeOption(CMAKE.CXX_COMPILER, compiler.cc), + CMakeOption(CMAKE.CXX_COMPILER, compiler.cxx), ]) def use_ccache(self): + ''' + Configures CMake to use ccache for faster builds. + ''' self.append_flags([ CMakeOption(CMAKE.C_COMPILER_LAUNCHER, 'ccache'), - CMakeOption(CMAKE.CXX_COMPILER_LAUNCHER, 'ccache') + CMakeOption(CMAKE.CXX_COMPILER_LAUNCHER, 'ccache'), ]) - def append_flags(self, flags: Iterable[CMakeFlag]): + # TODO(cvicentiu) write unit test. + def set_build_config(self, config: BuildConfig): + ''' + Set the build config flag. This is separate because of it being a + "one-off" special flag. + ''' + self.append_flags([CMakeOption(OTHER.BUILD_CONFIG, config)]) + + def append_flags(self, flags: Iterable[CMakeOption]): + ''' + Appends new flags to the generator. + + Raises: + DuplicateFlagException: If a flag with the same name already + exists. + ''' for flag in flags: # Do not allow duplicate flags being set. # Flags should only be set once to avoid confusion about them # being overwritten. - if flag.name in self.flags[flag.name]: - raise DuplicateFlagException(flag.name) + if flag.name in self.flags: + existing_flag = self.flags[flag.name] + raise DuplicateFlagException(flag.name, + existing_flag.value, + flag.value) self.flags[flag.name] = flag def generate(self) -> list[str]: + ''' + Generates the CMake command as a list of strings. + ''' result = [ 'cmake', + self.source_path ] - for flag in sorted(list(self.flags.values), lambda x: x.name): - result.append(f'-D{flag.name}={flag.value}') - + for flag in sorted(list(self.flags.values()), key=lambda x: x.name): + result.append(flag.as_cmd_arg()) return result diff --git a/steps/cmake/mariadb_options.py b/steps/cmake/mariadb_options.py deleted file mode 100644 index d8303e2d..00000000 --- a/steps/cmake/mariadb_options.py +++ /dev/null @@ -1,61 +0,0 @@ -from enum import Enum - -from .flags import BooleanCMakeFlag, CMakeFlag - - -class BuildType(Enum): - RELEASE = 'Release', - DEBUG = 'Debug', - RELWITHDEBUG = 'RelWithDebInfo', - - -# Used for -DBUILD_CONFIG= of cmake. -# Currently MariaDB's CMake only supports mysql_release. -class BuildConfig(Enum): - MYSQL_RELEASE = 'mysql_release' - - -class MariaDBPluginOption(Enum): - ARCHIVE_STORAGE_ENGINE = 'ARCHIVE' - CONNECT_STORAGE_ENGINE = 'CONNECT' - ROCKSDB_STORAGE_ENGINE = 'ROCKSDB' - TOKUDB_STORAGE_ENGINE = 'TOKUDB' - - -class MariaDBWithOption(Enum): - ASAN = 'ASAN' - DBUG_TRACE = 'DBUG_TRACE' - EMBEDDED_SERVER = 'EMBEDDED_SERVER' - JEMALLOC = 'JEMALLOC' - SAFEMALLOC = 'SAFEMALLOC' - UBSAN = 'UBSAN' - UNIT_TESTS = 'UNIT_TESTS' - VALGRIND = 'VALGRIND' - - -class PluginOption(CMakeFlag): - def __init__(self, name: MariaDBPluginOption, value: str): - assert isinstance(name, MariaDBPluginOption) - assert isinstance(value, str) - self.super().__init__(name=f'PLUGIN_{name.value}', value=value) - - -class BooleanPluginOption(BooleanCMakeFlag): - def __init__(self, name: MariaDBPluginOption, enabled: bool): - assert isinstance(name, MariaDBPluginOption) - assert isinstance(enabled, bool) - self.super().__init__(name=f'PLUGIN_{name.value}', enabled=enabled) - - -class WithOption(CMakeFlag): - def __init__(self, name: MariaDBWithOption, value: str): - assert isinstance(name, MariaDBWithOption) - assert isinstance(value, str) - self.super().__init__(name=f'WITH_{name}', value=value) - - -class BooleanWithOption(BooleanCMakeFlag): - def __init__(self, name: MariaDBWithOption, enabled: bool): - assert isinstance(name, MariaDBWithOption) - assert isinstance(enabled, bool) - self.super().__init__(name=f'WITH_{name}', enabled=enabled) diff --git a/steps/cmake/options.py b/steps/cmake/options.py new file mode 100644 index 00000000..ab83e214 --- /dev/null +++ b/steps/cmake/options.py @@ -0,0 +1,113 @@ +from enum import StrEnum + + +# Flag names use UPPER_CASE +class CMAKE(StrEnum): + ''' + Explicitly enumerates valid CMake flags to enforce type safety + and avoid typos in flag names. + ''' + AR = 'AR' + BUILD_TYPE = 'BUILD_TYPE' + CXX_COMPILER = 'CXX_COMPILER' + CXX_FLAGS = 'CXX_FLAGS' + C_COMPILER = 'C_COMPILER' + C_FLAGS = 'C_FLAGS' + C_COMPILER_LAUNCHER = 'C_COMPILER_LAUNCHER' + CXX_COMPILER_LAUNCHER = 'CXX_COMPILER_LAUNCHER' + INSTALL_PREFIX = 'INSTALL_PREFIX' + LIBRARY_PATH = 'LIBRARY_PATH' + + def __str__(self): + return f'CMAKE_{self.value}' + + +class PLUGIN(StrEnum): + """ + Enumerates valid plugin options for MariaDB's CMake configuration. + """ + ARCHIVE_STORAGE_ENGINE = 'ARCHIVE' + CONNECT_STORAGE_ENGINE = 'CONNECT' + ROCKSDB_STORAGE_ENGINE = 'ROCKSDB' + TOKUDB_STORAGE_ENGINE = 'TOKUDB' + + def __str__(self): + return f'PLUGIN_{self.value}' + + +class WITH(StrEnum): + """ + Enumerates valid options for MariaDB's CMake configuration. Each + option starts with WITH_. + """ + ASAN = 'ASAN' + DBUG_TRACE = 'DBUG_TRACE' + EMBEDDED_SERVER = 'EMBEDDED_SERVER' + JEMALLOC = 'JEMALLOC' + SAFEMALLOC = 'SAFEMALLOC' + UBSAN = 'UBSAN' + UNIT_TESTS = 'UNIT_TESTS' + VALGRIND = 'VALGRIND' + + def __str__(self): + return f'WITH_{self.value}' + + +class OTHER(StrEnum): + """ + Enumerates other valid options for MariaDB's + """ + BUILD_CONFIG = 'BUILD_CONFIG' + + +# Flag values use CapitalCase +class BuildType(StrEnum): + """ + Enumerates build types for CMake. + """ + RELEASE = 'Release' + DEBUG = 'Debug' + RELWITHDEBUG = 'RelWithDebInfo' + + +class BuildConfig(StrEnum): + """ + Used for -DBUILD_CONFIG= of cmake. + Enumerates build configurations for MariaDB's CMake. + """ + MYSQL_RELEASE = 'mysql_release' + + +class CMakeOption: + ''' + Represents a CMake option in the form `-D=`. + ''' + + @staticmethod + def _quote_value(value: str): + ''' + Quote the value if it contains spaces or special characters. + ''' + if ' ' in value or '"' in value: + return f'"{value.replace('"', '\\\"')}"' + return value + + def __init__(self, name: StrEnum, value: str | bool): + assert isinstance(name, StrEnum) + assert isinstance(value, str) or isinstance(value, bool) + self.name = str(name) + if isinstance(value, bool): + self.value = 'ON' if value else 'OFF' + elif isinstance(value, str): + self.value = value + # Quote if necessary. + self.value = self._quote_value(self.value) + + def as_cmd_arg(self) -> str: + return f"-D{self.name}={self.value}" + + def __str__(self) -> str: + return self.as_cmd_arg() + + def __repr__(self) -> str: + return f'CMakeOption({self.name}, {self.value})' diff --git a/steps/configure.py b/steps/configure.py index 66fe3102..395a0b94 100644 --- a/steps/configure.py +++ b/steps/configure.py @@ -1,10 +1,9 @@ from buildbot import interfaces, steps from .base import BuildStep -from .cmake.cmake_options import CMakeOption, CMAKE +from .cmake.cmake_options import CMakeOption, BuildType, CMAKE from .cmake.compilers import CompilerCommand from .cmake.generator import CMakeGenerator -from .cmake.mariadb_options import BuildType class ConfigureMariaDBCMakeStep(BuildStep): diff --git a/tests/test_cmake_generator.py b/tests/test_cmake_generator.py new file mode 100644 index 00000000..c4cc27ac --- /dev/null +++ b/tests/test_cmake_generator.py @@ -0,0 +1,138 @@ +import unittest +from steps.cmake.generator import CMakeGenerator, DuplicateFlagException +from steps.cmake.options import CMAKE, PLUGIN, WITH, CMakeOption, BuildType, BuildConfig +from steps.cmake.compilers import CompilerCommand + + +class TestCMakeGenerator(unittest.TestCase): + def test_initialization_with_flags(self): + '''Test that the generator initializes with provided flags.''' + flags = [ + CMakeOption(CMAKE.BUILD_TYPE, BuildType.RELWITHDEBUG), + CMakeOption(CMAKE.INSTALL_PREFIX, "/usr/local"), + CMakeOption(PLUGIN.ARCHIVE_STORAGE_ENGINE, True), + CMakeOption(WITH.ASAN, True), + ] + generator = CMakeGenerator(flags=flags) + command = generator.generate() + self.assertEqual(command, [ + 'cmake', + '.', + '-DCMAKE_BUILD_TYPE=RelWithDebInfo', + '-DCMAKE_INSTALL_PREFIX=/usr/local', + '-DPLUGIN_ARCHIVE=ON', + '-DWITH_ASAN=ON', + ]) + + def test_append_flags_successful(self): + ''' + Test that flags are appended successfully. + ''' + generator = CMakeGenerator(flags=[]) + generator.append_flags([ + CMakeOption(CMAKE.LIBRARY_PATH, "/usr/lib"), + CMakeOption(CMAKE.AR, "ar"), + ]) + command = generator.generate() + self.assertEqual(command, [ + 'cmake', + '.', + '-DCMAKE_AR=ar', + '-DCMAKE_LIBRARY_PATH=/usr/lib', + ]) + + def test_append_flags_duplicate(self): + ''' + Test that appending a duplicate flag raises an exception. + ''' + flags = [CMakeOption(CMAKE.BUILD_TYPE, "Release")] + generator = CMakeGenerator(flags=flags) + duplicate_flag = CMakeOption(CMAKE.BUILD_TYPE, "Debug") + with self.assertRaises(DuplicateFlagException): + generator.append_flags([duplicate_flag]) + + def test_set_compiler(self): + ''' + Test that set_compiler adds the correct flags. + ''' + generator = CMakeGenerator(flags=[]) + compiler = CompilerCommand(cc="gcc", cxx="g++") + generator.set_compiler(compiler) + command = generator.generate() + self.assertEqual(command, [ + 'cmake', + '.', + '-DCMAKE_CXX_COMPILER=g++', + '-DCMAKE_C_COMPILER=gcc', + ]) + + def test_use_ccache(self): + ''' + Test that use_ccache sets the correct flags. + ''' + generator = CMakeGenerator(flags=[]) + generator.use_ccache() + command = generator.generate() + self.assertEqual(command, [ + 'cmake', + '.', + '-DCMAKE_CXX_COMPILER_LAUNCHER=ccache', + '-DCMAKE_C_COMPILER_LAUNCHER=ccache', + ]) + + def test_generate_with_no_flags(self): + ''' + Test that generate produces only the 'cmake' command if no flags are + set. + ''' + generator = CMakeGenerator(flags=[]) + command = generator.generate() + self.assertEqual(command, ['cmake', '.']) + + def test_set_build_config(self): + ''' + Test that set_build_config correctly adds the BUILD_CONFIG flag. + ''' + generator = CMakeGenerator(flags=[]) + + # Set the build config to MYSQL_RELEASE + generator.set_build_config(BuildConfig.MYSQL_RELEASE) + command = generator.generate() + + self.assertEqual(command, [ + 'cmake', + '.', + '-DBUILD_CONFIG=mysql_release', + ]) + + def test_set_build_config_duplicate(self): + ''' + Test that setting BUILD_CONFIG twice raises a DuplicateFlagException. + ''' + generator = CMakeGenerator(flags=[]) + + # Set the build config the first time + generator.set_build_config(BuildConfig.MYSQL_RELEASE) + + # Attempt to set it again should raise DuplicateFlagException + with self.assertRaises(DuplicateFlagException): + generator.set_build_config(BuildConfig.MYSQL_RELEASE) + + def test_set_build_config_with_other_flags(self): + ''' + Test that set_build_config works alongside other flags. + ''' + generator = CMakeGenerator(flags=[ + CMakeOption(CMAKE.INSTALL_PREFIX, '/usr/lib/test') + ]) + + # Set the build config + generator.set_build_config(BuildConfig.MYSQL_RELEASE) + command = generator.generate() + + self.assertEqual(command, [ + 'cmake', + '.', + '-DBUILD_CONFIG=mysql_release', + '-DCMAKE_INSTALL_PREFIX=/usr/lib/test', + ])