@@ -52,6 +52,7 @@ class InstallOptions:
5252 # Compiler
5353 cc : str = ''
5454 cxx : str = ''
55+ sanitizer : str = ''
5556
5657 # Required tools
5758 git_path : str = ''
@@ -69,12 +70,12 @@ class InstallOptions:
6970 mrdocs_repo : str = "https://github.com/cppalliance/mrdocs"
7071 mrdocs_branch : str = "develop"
7172 mrdocs_use_user_presets : bool = True
72- mrdocs_preset_name : str = "<mrdocs-build-type:lower>-<os:lower><\" -\" :if(cc)><cc:basename>"
73- mrdocs_build_dir : str = "<mrdocs-src-dir>/build/<mrdocs-build-type:lower>-<os:lower><\" -\" :if(cc)><cc:basename>"
73+ mrdocs_preset_name : str = "<mrdocs-build-type:lower>-<os:lower><\" -\" :if(cc)><cc:basename>< \" - \" :if(sanitizer)><sanitizer:lower> "
74+ mrdocs_build_dir : str = "<mrdocs-src-dir>/build/<mrdocs-build-type:lower>-<os:lower><\" -\" :if(cc)><cc:basename>< \" - \" :if(sanitizer)><sanitizer:lower>< \" - \" :if(sanitizer)><sanitizer:lower> "
7475 mrdocs_build_tests : bool = True
7576 mrdocs_system_install : bool = field (default_factory = lambda : not running_from_mrdocs_source_dir ())
7677 mrdocs_install_dir : str = field (
77- default_factory = lambda : "<mrdocs-src-dir>/install/<mrdocs-build-type:lower>-<os:lower><\" -\" :if(cc)><cc:basename>" if running_from_mrdocs_source_dir () else "" )
78+ default_factory = lambda : "<mrdocs-src-dir>/install/<mrdocs-build-type:lower>-<os:lower><\" -\" :if(cc)><cc:basename>< \" - \" :if(sanitizer)><sanitizer:lower> " if running_from_mrdocs_source_dir () else "" )
7879 mrdocs_run_tests : bool = True
7980
8081 # Third-party dependencies
@@ -84,14 +85,14 @@ class InstallOptions:
8485 duktape_src_dir : str = "<third-party-src-dir>/duktape"
8586 duktape_url : str = "https://github.com/svaarala/duktape/releases/download/v2.7.0/duktape-2.7.0.tar.xz"
8687 duktape_build_type : str = "<mrdocs-build-type>"
87- duktape_build_dir : str = "<duktape-src-dir>/build/<duktape-build-type:lower><\" -\" :if(cc)><cc:basename>"
88- duktape_install_dir : str = "<duktape-src-dir>/install/<duktape-build-type:lower><\" -\" :if(cc)><cc:basename>"
88+ duktape_build_dir : str = "<duktape-src-dir>/build/<duktape-build-type:lower><\" -\" :if(cc)><cc:basename>< \" - \" :if(sanitizer)><sanitizer:lower> "
89+ duktape_install_dir : str = "<duktape-src-dir>/install/<duktape-build-type:lower><\" -\" :if(cc)><cc:basename>< \" - \" :if(sanitizer)><sanitizer:lower> "
8990
9091 # LLVM
9192 llvm_src_dir : str = "<third-party-src-dir>/llvm-project"
9293 llvm_build_type : str = "<mrdocs-build-type>"
93- llvm_build_dir : str = "<llvm-src-dir>/build/<llvm-build-type:lower><\" -\" :if(cc)><cc:basename>"
94- llvm_install_dir : str = "<llvm-src-dir>/install/<llvm-build-type:lower><\" -\" :if(cc)><cc:basename>"
94+ llvm_build_dir : str = "<llvm-src-dir>/build/<llvm-build-type:lower><\" -\" :if(cc)><cc:basename>< \" - \" :if(sanitizer)><sanitizer:lower> "
95+ llvm_install_dir : str = "<llvm-src-dir>/install/<llvm-build-type:lower><\" -\" :if(cc)><cc:basename>< \" - \" :if(sanitizer)><sanitizer:lower> "
9596 llvm_repo : str = "https://github.com/llvm/llvm-project.git"
9697 llvm_commit : str = "dd7a3d4d798e30dfe53b5bbbbcd9a23c24ea1af9"
9798
@@ -116,6 +117,7 @@ class InstallOptions:
116117INSTALL_OPTION_DESCRIPTIONS = {
117118 "cc" : "Path to the C compiler executable. Leave empty for default." ,
118119 "cxx" : "Path to the C++ compiler executable. Leave empty for default." ,
120+ "sanitizer" : "Sanitizer to use for the build. Leave empty for no sanitizer. (ASan, UBSan, MSan, TSan)" ,
119121 "git_path" : "Path to the git executable, if not in system PATH." ,
120122 "cmake_path" : "Path to the cmake executable, if not in system PATH." ,
121123 "java_path" : "Path to the java executable, if not in system PATH." ,
@@ -301,8 +303,9 @@ def repl(match):
301303 val = val .upper ()
302304 elif transform_fn == "basename" :
303305 val = os .path .basename (val )
304- elif transform_fn == "if(cc)" :
305- if self .options .cc :
306+ elif transform_fn .startswith ("if(" ) and transform_fn .endswith (")" ):
307+ var_name = transform_fn [3 :- 1 ]
308+ if getattr (self .options , var_name , None ):
306309 val = val .lower ()
307310 else :
308311 val = ""
@@ -356,6 +359,25 @@ def prompt_build_type_option(self, name):
356359 print (f"Invalid build type '{ value } '. Must be one of: { ', ' .join (valid_build_types )} ." )
357360 raise ValueError (f"Invalid build type '{ value } '. Must be one of: { ', ' .join (valid_build_types )} ." )
358361
362+ def prompt_sanitizer_option (self , name ):
363+ value = self .prompt_option (name )
364+ if not value :
365+ value = ''
366+ return value
367+ valid_sanitizers = ["ASan" , "UBSan" , "MSan" , "TSan" ]
368+ for t in valid_sanitizers :
369+ if t .lower () == value .lower ():
370+ setattr (self .options , name , t )
371+ return value
372+ print (f"Invalid sanitizer '{ value } '. Must be one of: { ', ' .join (valid_sanitizers )} ." )
373+ value = self .reprompt_option (name )
374+ for t in valid_sanitizers :
375+ if t .lower () == value .lower ():
376+ setattr (self .options , name , t )
377+ return value
378+ print (f"Invalid sanitizer '{ value } '. Must be one of: { ', ' .join (valid_sanitizers )} ." )
379+ raise ValueError (f"Invalid sanitizer '{ value } '. Must be one of: { ', ' .join (valid_sanitizers )} ." )
380+
359381 def supports_ansi (self ):
360382 if os .name == "posix" :
361383 return True
@@ -552,9 +574,12 @@ def setup_mrdocs_src_dir(self):
552574
553575 # MrDocs build type
554576 self .prompt_build_type_option ("mrdocs_build_type" )
577+ self .prompt_sanitizer_option ("sanitizer" )
555578 if self .prompt_option ("mrdocs_build_tests" ):
556579 self .check_tool ("java" )
557580
581+
582+
558583 def is_inside_mrdocs_dir (self , path ):
559584 """
560585 Checks if the given path is inside the MrDocs source directory.
@@ -666,6 +691,23 @@ def install_ninja(self):
666691 os .chmod (ninja_path , 0o755 )
667692 self .options .ninja_path = ninja_path
668693
694+ def sanitizer_flag_name (self , sanitizer ):
695+ """
696+ Returns the flag name for the given sanitizer.
697+ :param sanitizer: The sanitizer name (ASan, UBSan, MSan, TSan).
698+ :return: str: The flag name for the sanitizer.
699+ """
700+ if sanitizer .lower () == "asan" :
701+ return "address"
702+ elif sanitizer .lower () == "ubsan" :
703+ return "undefined"
704+ elif sanitizer .lower () == "msan" :
705+ return "memory"
706+ elif sanitizer .lower () == "tsan" :
707+ return "thread"
708+ else :
709+ raise ValueError (f"Unknown sanitizer '{ sanitizer } '." )
710+
669711 def is_abi_compatible (self , build_type_a , build_type_b ):
670712 if not self .is_windows ():
671713 return True
@@ -719,8 +761,17 @@ def install_duktape(self):
719761 self .options .duktape_build_type = self .options .mrdocs_build_type
720762 self .prompt_dependency_path_option ("duktape_build_dir" )
721763 self .prompt_dependency_path_option ("duktape_install_dir" )
722- self .cmake_workflow (self .options .duktape_src_dir , self .options .duktape_build_type ,
723- self .options .duktape_build_dir , self .options .duktape_install_dir )
764+ extra_args = []
765+ if self .options .sanitizer :
766+ flag_name = self .sanitizer_flag_name (self .options .sanitizer )
767+ for arg in ["CMAKE_C_FLAGS" , "CMAKE_CXX_FLAGS" ]:
768+ extra_args .append (f"-D{ arg } =-fsanitize={ flag_name } -fno-sanitize-recover={ flag_name } -fno-omit-frame-pointer" )
769+
770+ self .cmake_workflow (
771+ self .options .duktape_src_dir ,
772+ self .options .duktape_build_type ,
773+ self .options .duktape_build_dir ,
774+ self .options .duktape_install_dir , extra_args )
724775
725776 def install_libxml2 (self ):
726777 self .prompt_dependency_path_option ("libxml2_src_dir" )
@@ -799,8 +850,23 @@ def install_llvm(self):
799850 self .prompt_dependency_path_option ("llvm_install_dir" )
800851 cmake_preset = f"{ self .options .llvm_build_type .lower ()} -win" if self .is_windows () else f"{ self .options .llvm_build_type .lower ()} -unix"
801852 cmake_extra_args = [f"--preset={ cmake_preset } " ]
802- self .cmake_workflow (llvm_subproject_dir , self .options .llvm_build_type , self .options .llvm_build_dir ,
803- self .options .llvm_install_dir , cmake_extra_args )
853+ if self .options .sanitizer :
854+ if self .options .sanitizer .lower () == "asan" :
855+ cmake_extra_args .append ("-DLLVM_USE_SANITIZER=Address" )
856+ elif self .options .sanitizer .lower () == "ubsan" :
857+ cmake_extra_args .append ("-DLLVM_USE_SANITIZER=Undefined" )
858+ elif self .options .sanitizer .lower () == "msan" :
859+ cmake_extra_args .append ("-DLLVM_USE_SANITIZER=Memory" )
860+ elif self .options .sanitizer .lower () == "tsan" :
861+ cmake_extra_args .append ("-DLLVM_USE_SANITIZER=Thread" )
862+ else :
863+ raise ValueError (f"Unknown LLVM sanitizer '{ self .options .sanitizer } '." )
864+ self .cmake_workflow (
865+ llvm_subproject_dir ,
866+ self .options .llvm_build_type ,
867+ self .options .llvm_build_dir ,
868+ self .options .llvm_install_dir ,
869+ cmake_extra_args )
804870
805871 def create_cmake_presets (self ):
806872 # Ask the user if they want to create CMake User presets referencing the installed dependencies
@@ -852,9 +918,13 @@ def create_cmake_presets(self):
852918 display_name = f"{ self .options .mrdocs_build_type } "
853919 if self .options .mrdocs_build_type .lower () == "debug" and self .options .llvm_build_type != self .options .mrdocs_build_type :
854920 display_name += " with Optimized Dependencies"
855- display_name += f" ({ OSDisplayName } ) "
921+ display_name += f" ({ OSDisplayName } "
856922 if self .options .cc :
857- display_name += f" ({ os .path .basename (self .options .cc )} )"
923+ display_name += f": { os .path .basename (self .options .cc )} "
924+ display_name += ")"
925+
926+ if self .options .sanitizer :
927+ display_name += f" with { self .options .sanitizer } "
858928
859929 new_preset = {
860930 "name" : self .options .mrdocs_preset_name ,
@@ -892,6 +962,10 @@ def create_cmake_presets(self):
892962 if self .options .ninja_path :
893963 new_preset ["generator" ] = "Ninja"
894964 new_preset ["cacheVariables" ]["CMAKE_MAKE_PROGRAM" ] = self .options .ninja_path
965+ if self .options .sanitizer :
966+ flag_name = self .sanitizer_flag_name (self .options .sanitizer )
967+ for arg in ["CMAKE_C_FLAGS" , "CMAKE_CXX_FLAGS" ]:
968+ new_preset ["cacheVariables" ][arg ] = f"-fsanitize={ flag_name } -fno-sanitize-recover={ flag_name } -fno-omit-frame-pointer"
895969
896970 # Update cache variables path prefixes with their relative equivalents
897971 mrdocs_src_dir_parent = os .path .dirname (self .options .mrdocs_src_dir )
@@ -967,8 +1041,14 @@ def install_mrdocs(self):
9671041 extra_args .extend (["-DMRDOCS_BUILD_DOCS=OFF" , "-DMRDOCS_GENERATE_REFERENCE=OFF" ,
9681042 "-DMRDOCS_GENERATE_ANTORA_REFERENCE=OFF" ])
9691043
1044+ if self .options .sanitizer :
1045+ flag_name = self .sanitizer_flag_name (self .options .sanitizer )
1046+ for arg in ["CMAKE_C_FLAGS" , "CMAKE_CXX_FLAGS" ]:
1047+ extra_args .append (f"-D{ arg } =-fsanitize={ flag_name } -fno-sanitize-recover={ flag_name } -fno-omit-frame-pointer" )
1048+
9701049 self .cmake_workflow (self .options .mrdocs_src_dir , self .options .mrdocs_build_type , self .options .mrdocs_build_dir ,
9711050 self .options .mrdocs_install_dir , extra_args )
1051+
9721052 if self .options .mrdocs_build_dir and self .prompt_option ("mrdocs_run_tests" ):
9731053 # Look for ctest path relative to the cmake path
9741054 ctest_path = os .path .join (os .path .dirname (self .options .cmake_path ), "ctest" )
0 commit comments