diff --git a/CMakeUserPresets.json.example b/CMakeUserPresets.json.example index 1beab242ec..8763339070 100644 --- a/CMakeUserPresets.json.example +++ b/CMakeUserPresets.json.example @@ -244,6 +244,74 @@ "rhs": "Darwin" }, "generator": "Ninja" + }, + { + "name": "debug-macos-gcc-asan", + "displayName": "Debug (macOS: gcc) with ASan", + "description": "Preset for building MrDocs in Debug mode with the gcc compiler in macOS.", + "inherits": "debug", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "LLVM_ROOT": "$env{HOME}/Developer/cpp-libs/llvm-project/install/debug-gcc-asan", + "Clang_ROOT": "$env{HOME}/Developer/cpp-libs/llvm-project/install/debug-gcc-asan", + "duktape_ROOT": "$env{HOME}/Developer/cpp-libs/duktape/install/debug-gcc-asan", + "Duktape_ROOT": "$env{HOME}/Developer/cpp-libs/duktape/install/debug-gcc-asan", + "libxml2_ROOT": "$env{HOME}/Developer/cpp-libs/libxml2/install/release-gcc", + "LibXml2_ROOT": "$env{HOME}/Developer/cpp-libs/libxml2/install/release-gcc", + "MRDOCS_BUILD_TESTS": true, + "MRDOCS_BUILD_DOCS": false, + "MRDOCS_GENERATE_REFERENCE": false, + "MRDOCS_GENERATE_ANTORA_REFERENCE": false, + "CMAKE_C_COMPILER": "/usr/bin/gcc", + "CMAKE_CXX_COMPILER": "/usr/bin/g++", + "CMAKE_MAKE_PROGRAM": "$env{HOME}/Developer/cpp-libs/ninja/ninja", + "CMAKE_C_FLAGS": "-fsanitize=address -fno-sanitize-recover=address -fno-omit-frame-pointer", + "CMAKE_CXX_FLAGS": "-fsanitize=address -fno-sanitize-recover=address -fno-omit-frame-pointer" + }, + "warnings": { + "unusedCli": false + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "generator": "Ninja" + }, + { + "name": "debug-macos-gcc-ubsan", + "displayName": "Debug (macOS: gcc) with UBSan", + "description": "Preset for building MrDocs in Debug mode with the gcc compiler in macOS.", + "inherits": "debug", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "LLVM_ROOT": "$env{HOME}/Developer/cpp-libs/llvm-project/install/debug-gcc-ubsan", + "Clang_ROOT": "$env{HOME}/Developer/cpp-libs/llvm-project/install/debug-gcc-ubsan", + "duktape_ROOT": "$env{HOME}/Developer/cpp-libs/duktape/install/debug-gcc-ubsan", + "Duktape_ROOT": "$env{HOME}/Developer/cpp-libs/duktape/install/debug-gcc-ubsan", + "libxml2_ROOT": "$env{HOME}/Developer/cpp-libs/libxml2/install/release-gcc", + "LibXml2_ROOT": "$env{HOME}/Developer/cpp-libs/libxml2/install/release-gcc", + "MRDOCS_BUILD_TESTS": true, + "MRDOCS_BUILD_DOCS": false, + "MRDOCS_GENERATE_REFERENCE": false, + "MRDOCS_GENERATE_ANTORA_REFERENCE": false, + "CMAKE_C_COMPILER": "/usr/bin/gcc", + "CMAKE_CXX_COMPILER": "/usr/bin/g++", + "CMAKE_MAKE_PROGRAM": "$env{HOME}/Developer/cpp-libs/ninja/ninja", + "CMAKE_C_FLAGS": "-fsanitize=undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer", + "CMAKE_CXX_FLAGS": "-fsanitize=undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer" + }, + "warnings": { + "unusedCli": false + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "generator": "Ninja" } ] } \ No newline at end of file diff --git a/bootstrap.py b/bootstrap.py index 76f2580371..0a888eb659 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -52,6 +52,7 @@ class InstallOptions: # Compiler cc: str = '' cxx: str = '' + sanitizer: str = '' # Required tools git_path: str = '' @@ -69,12 +70,12 @@ class InstallOptions: mrdocs_repo: str = "https://github.com/cppalliance/mrdocs" mrdocs_branch: str = "develop" mrdocs_use_user_presets: bool = True - mrdocs_preset_name: str = "-<\"-\":if(cc)>" - mrdocs_build_dir: str = "/build/-<\"-\":if(cc)>" + mrdocs_preset_name: str = "-<\"-\":if(cc)><\"-\":if(sanitizer)>" + mrdocs_build_dir: str = "/build/-<\"-\":if(cc)><\"-\":if(sanitizer)><\"-\":if(sanitizer)>" mrdocs_build_tests: bool = True mrdocs_system_install: bool = field(default_factory=lambda: not running_from_mrdocs_source_dir()) mrdocs_install_dir: str = field( - default_factory=lambda: "/install/-<\"-\":if(cc)>" if running_from_mrdocs_source_dir() else "") + default_factory=lambda: "/install/-<\"-\":if(cc)><\"-\":if(sanitizer)>" if running_from_mrdocs_source_dir() else "") mrdocs_run_tests: bool = True # Third-party dependencies @@ -84,14 +85,14 @@ class InstallOptions: duktape_src_dir: str = "/duktape" duktape_url: str = "https://github.com/svaarala/duktape/releases/download/v2.7.0/duktape-2.7.0.tar.xz" duktape_build_type: str = "" - duktape_build_dir: str = "/build/<\"-\":if(cc)>" - duktape_install_dir: str = "/install/<\"-\":if(cc)>" + duktape_build_dir: str = "/build/<\"-\":if(cc)><\"-\":if(sanitizer)>" + duktape_install_dir: str = "/install/<\"-\":if(cc)><\"-\":if(sanitizer)>" # LLVM llvm_src_dir: str = "/llvm-project" llvm_build_type: str = "" - llvm_build_dir: str = "/build/<\"-\":if(cc)>" - llvm_install_dir: str = "/install/<\"-\":if(cc)>" + llvm_build_dir: str = "/build/<\"-\":if(cc)><\"-\":if(sanitizer)>" + llvm_install_dir: str = "/install/<\"-\":if(cc)><\"-\":if(sanitizer)>" llvm_repo: str = "https://github.com/llvm/llvm-project.git" llvm_commit: str = "dd7a3d4d798e30dfe53b5bbbbcd9a23c24ea1af9" @@ -116,6 +117,7 @@ class InstallOptions: INSTALL_OPTION_DESCRIPTIONS = { "cc": "Path to the C compiler executable. Leave empty for default.", "cxx": "Path to the C++ compiler executable. Leave empty for default.", + "sanitizer": "Sanitizer to use for the build. Leave empty for no sanitizer. (ASan, UBSan, MSan, TSan)", "git_path": "Path to the git executable, if not in system PATH.", "cmake_path": "Path to the cmake executable, if not in system PATH.", "java_path": "Path to the java executable, if not in system PATH.", @@ -301,8 +303,9 @@ def repl(match): val = val.upper() elif transform_fn == "basename": val = os.path.basename(val) - elif transform_fn == "if(cc)": - if self.options.cc: + elif transform_fn.startswith("if(") and transform_fn.endswith(")"): + var_name = transform_fn[3:-1] + if getattr(self.options, var_name, None): val = val.lower() else: val = "" @@ -356,6 +359,25 @@ def prompt_build_type_option(self, name): print(f"Invalid build type '{value}'. Must be one of: {', '.join(valid_build_types)}.") raise ValueError(f"Invalid build type '{value}'. Must be one of: {', '.join(valid_build_types)}.") + def prompt_sanitizer_option(self, name): + value = self.prompt_option(name) + if not value: + value = '' + return value + valid_sanitizers = ["ASan", "UBSan", "MSan", "TSan"] + for t in valid_sanitizers: + if t.lower() == value.lower(): + setattr(self.options, name, t) + return value + print(f"Invalid sanitizer '{value}'. Must be one of: {', '.join(valid_sanitizers)}.") + value = self.reprompt_option(name) + for t in valid_sanitizers: + if t.lower() == value.lower(): + setattr(self.options, name, t) + return value + print(f"Invalid sanitizer '{value}'. Must be one of: {', '.join(valid_sanitizers)}.") + raise ValueError(f"Invalid sanitizer '{value}'. Must be one of: {', '.join(valid_sanitizers)}.") + def supports_ansi(self): if os.name == "posix": return True @@ -552,9 +574,12 @@ def setup_mrdocs_src_dir(self): # MrDocs build type self.prompt_build_type_option("mrdocs_build_type") + self.prompt_sanitizer_option("sanitizer") if self.prompt_option("mrdocs_build_tests"): self.check_tool("java") + + def is_inside_mrdocs_dir(self, path): """ Checks if the given path is inside the MrDocs source directory. @@ -666,6 +691,23 @@ def install_ninja(self): os.chmod(ninja_path, 0o755) self.options.ninja_path = ninja_path + def sanitizer_flag_name(self, sanitizer): + """ + Returns the flag name for the given sanitizer. + :param sanitizer: The sanitizer name (ASan, UBSan, MSan, TSan). + :return: str: The flag name for the sanitizer. + """ + if sanitizer.lower() == "asan": + return "address" + elif sanitizer.lower() == "ubsan": + return "undefined" + elif sanitizer.lower() == "msan": + return "memory" + elif sanitizer.lower() == "tsan": + return "thread" + else: + raise ValueError(f"Unknown sanitizer '{sanitizer}'.") + def is_abi_compatible(self, build_type_a, build_type_b): if not self.is_windows(): return True @@ -719,8 +761,17 @@ def install_duktape(self): self.options.duktape_build_type = self.options.mrdocs_build_type self.prompt_dependency_path_option("duktape_build_dir") self.prompt_dependency_path_option("duktape_install_dir") - self.cmake_workflow(self.options.duktape_src_dir, self.options.duktape_build_type, - self.options.duktape_build_dir, self.options.duktape_install_dir) + extra_args = [] + if self.options.sanitizer: + flag_name = self.sanitizer_flag_name(self.options.sanitizer) + for arg in ["CMAKE_C_FLAGS", "CMAKE_CXX_FLAGS"]: + extra_args.append(f"-D{arg}=-fsanitize={flag_name} -fno-sanitize-recover={flag_name} -fno-omit-frame-pointer") + + self.cmake_workflow( + self.options.duktape_src_dir, + self.options.duktape_build_type, + self.options.duktape_build_dir, + self.options.duktape_install_dir, extra_args) def install_libxml2(self): self.prompt_dependency_path_option("libxml2_src_dir") @@ -799,8 +850,23 @@ def install_llvm(self): self.prompt_dependency_path_option("llvm_install_dir") cmake_preset = f"{self.options.llvm_build_type.lower()}-win" if self.is_windows() else f"{self.options.llvm_build_type.lower()}-unix" cmake_extra_args = [f"--preset={cmake_preset}"] - self.cmake_workflow(llvm_subproject_dir, self.options.llvm_build_type, self.options.llvm_build_dir, - self.options.llvm_install_dir, cmake_extra_args) + if self.options.sanitizer: + if self.options.sanitizer.lower() == "asan": + cmake_extra_args.append("-DLLVM_USE_SANITIZER=Address") + elif self.options.sanitizer.lower() == "ubsan": + cmake_extra_args.append("-DLLVM_USE_SANITIZER=Undefined") + elif self.options.sanitizer.lower() == "msan": + cmake_extra_args.append("-DLLVM_USE_SANITIZER=Memory") + elif self.options.sanitizer.lower() == "tsan": + cmake_extra_args.append("-DLLVM_USE_SANITIZER=Thread") + else: + raise ValueError(f"Unknown LLVM sanitizer '{self.options.sanitizer}'.") + self.cmake_workflow( + llvm_subproject_dir, + self.options.llvm_build_type, + self.options.llvm_build_dir, + self.options.llvm_install_dir, + cmake_extra_args) def create_cmake_presets(self): # Ask the user if they want to create CMake User presets referencing the installed dependencies @@ -852,9 +918,13 @@ def create_cmake_presets(self): display_name = f"{self.options.mrdocs_build_type}" if self.options.mrdocs_build_type.lower() == "debug" and self.options.llvm_build_type != self.options.mrdocs_build_type: display_name += " with Optimized Dependencies" - display_name += f" ({OSDisplayName})" + display_name += f" ({OSDisplayName}" if self.options.cc: - display_name += f" ({os.path.basename(self.options.cc)})" + display_name += f": {os.path.basename(self.options.cc)}" + display_name += ")" + + if self.options.sanitizer: + display_name += f" with {self.options.sanitizer}" new_preset = { "name": self.options.mrdocs_preset_name, @@ -892,6 +962,10 @@ def create_cmake_presets(self): if self.options.ninja_path: new_preset["generator"] = "Ninja" new_preset["cacheVariables"]["CMAKE_MAKE_PROGRAM"] = self.options.ninja_path + if self.options.sanitizer: + flag_name = self.sanitizer_flag_name(self.options.sanitizer) + for arg in ["CMAKE_C_FLAGS", "CMAKE_CXX_FLAGS"]: + new_preset["cacheVariables"][arg] = f"-fsanitize={flag_name} -fno-sanitize-recover={flag_name} -fno-omit-frame-pointer" # Update cache variables path prefixes with their relative equivalents mrdocs_src_dir_parent = os.path.dirname(self.options.mrdocs_src_dir) @@ -967,8 +1041,14 @@ def install_mrdocs(self): extra_args.extend(["-DMRDOCS_BUILD_DOCS=OFF", "-DMRDOCS_GENERATE_REFERENCE=OFF", "-DMRDOCS_GENERATE_ANTORA_REFERENCE=OFF"]) + if self.options.sanitizer: + flag_name = self.sanitizer_flag_name(self.options.sanitizer) + for arg in ["CMAKE_C_FLAGS", "CMAKE_CXX_FLAGS"]: + extra_args.append(f"-D{arg}=-fsanitize={flag_name} -fno-sanitize-recover={flag_name} -fno-omit-frame-pointer") + self.cmake_workflow(self.options.mrdocs_src_dir, self.options.mrdocs_build_type, self.options.mrdocs_build_dir, self.options.mrdocs_install_dir, extra_args) + if self.options.mrdocs_build_dir and self.prompt_option("mrdocs_run_tests"): # Look for ctest path relative to the cmake path ctest_path = os.path.join(os.path.dirname(self.options.cmake_path), "ctest")