diff --git a/.gitignore b/.gitignore index 6e5f29e69b..1d17a73e14 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ docs/ui/build docs/ui/public share/mrdocs/libcxx/ share/mrdocs/clang/ +docs/modules/reference \ No newline at end of file diff --git a/CMakeUserPresets.json.example b/CMakeUserPresets.json.example index 67824e6e58..93044ba11e 100644 --- a/CMakeUserPresets.json.example +++ b/CMakeUserPresets.json.example @@ -181,7 +181,6 @@ "Clang_ROOT": "$env{HOME}/Developer/cpp-libs/llvm-project/install/debug", "duktape_ROOT": "$env{HOME}/Developer/cpp-libs/duktape/install/debug", "Duktape_ROOT": "$env{HOME}/Developer/cpp-libs/duktape/install/debug", - "fmt_ROOT": "$env{HOME}/Developer/cpp-libs/fmt/install/debug", "libxml2_ROOT": "$env{HOME}/Developer/cpp-libs/libxml2/install/release", "LibXml2_ROOT": "$env{HOME}/Developer/cpp-libs/libxml2/install/release", "MRDOCS_BUILD_TESTS": true, @@ -238,7 +237,6 @@ "Clang_ROOT": "$env{HOME}/Developer/cpp-libs/llvm-project/install/release", "duktape_ROOT": "$env{HOME}/Developer/cpp-libs/duktape/install/release", "Duktape_ROOT": "$env{HOME}/Developer/cpp-libs/duktape/install/release", - "fmt_ROOT": "$env{HOME}/Developer/cpp-libs/fmt/install/release", "libxml2_ROOT": "$env{HOME}/Developer/cpp-libs/libxml2/install/release", "LibXml2_ROOT": "$env{HOME}/Developer/cpp-libs/libxml2/install/release", "MRDOCS_BUILD_TESTS": true, @@ -254,6 +252,34 @@ "warnings": { "unusedCli": false } + }, + { + "name": "release-macos-gcc", + "displayName": "Release macOS (gcc)", + "description": "Preset for building MrDocs in Release mode with the gcc compiler in macOS.", + "inherits": "release", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "LLVM_ROOT": "$env{HOME}/Developer/cpp-libs/llvm-project/install/release-gcc", + "Clang_ROOT": "$env{HOME}/Developer/cpp-libs/llvm-project/install/release-gcc", + "duktape_ROOT": "$env{HOME}/Developer/cpp-libs/duktape/install/release-gcc", + "Duktape_ROOT": "$env{HOME}/Developer/cpp-libs/duktape/install/release-gcc", + "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 + }, + "warnings": { + "unusedCli": false + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } } ] } \ No newline at end of file diff --git a/bootstrap.py b/bootstrap.py index 085017d69c..e5d65bb8e6 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -9,7 +9,6 @@ # import argparse -import platform import subprocess import os import sys @@ -19,9 +18,11 @@ import urllib.request import tarfile import json +import shlex import re from functools import lru_cache + @lru_cache(maxsize=1) def running_from_mrdocs_source_dir(): """ @@ -32,6 +33,7 @@ def running_from_mrdocs_source_dir(): cwd = os.getcwd() return cwd == script_dir + @dataclass class InstallOptions: """ @@ -45,21 +47,29 @@ class InstallOptions: In InstallOptions, it allows easy initialization and management of configuration options with default values and type hints. """ + # Compiler + cc: str = '' + cxx: str = '' + # Tools git_path: str = '' cmake_path: str = '' + java_path: str = '' # MrDocs - mrdocs_src_dir: str = field(default_factory=lambda: os.getcwd() if running_from_mrdocs_source_dir() else os.path.join(os.getcwd(), "mrdocs")) + mrdocs_src_dir: str = field( + default_factory=lambda: os.getcwd() if running_from_mrdocs_source_dir() else os.path.join(os.getcwd(), + "mrdocs")) mrdocs_build_type: str = "Release" mrdocs_repo: str = "https://github.com/cppalliance/mrdocs" mrdocs_branch: str = "develop" mrdocs_use_user_presets: bool = True - mrdocs_preset_name: str = "-" - mrdocs_build_dir: str = "/build/-" + mrdocs_preset_name: str = "-<\"-\":if(cc)>" + mrdocs_build_dir: str = "/build/-<\"-\":if(cc)>" 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 running_from_mrdocs_source_dir() else "") + mrdocs_install_dir: str = field( + default_factory=lambda: "/install/--" if running_from_mrdocs_source_dir() else "") mrdocs_run_tests: bool = True # Third-party dependencies @@ -69,23 +79,23 @@ 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/" - duktape_install_dir: str = "/install/" + duktape_build_dir: str = "/build/<\"-\":if(cc)>" + duktape_install_dir: str = "/install/<\"-\":if(cc)>" # Libxml2 libxml2_src_dir: str = "/libxml2" # purposefully does not depend on mrdocs-build-type because we only need the executable libxml2_build_type: str = "Release" - libxml2_build_dir: str = "/build/" - libxml2_install_dir: str = "/install/" + libxml2_build_dir: str = "/build/<\"-\":if(cc)>" + libxml2_install_dir: str = "/install/<\"-\":if(cc)>" libxml2_repo: str = "https://github.com/GNOME/libxml2" libxml2_branch: str = "v2.12.6" # LLVM llvm_src_dir: str = "/llvm-project" llvm_build_type: str = "" - llvm_build_dir: str = "/build/" - llvm_install_dir: str = "/install/" + llvm_build_dir: str = "/build/<\"-\":if(cc)>" + llvm_install_dir: str = "/install/<\"-\":if(cc)>" llvm_repo: str = "https://github.com/llvm/llvm-project.git" llvm_commit: str = "dd7a3d4d798e30dfe53b5bbbbcd9a23c24ea1af9" @@ -99,8 +109,11 @@ class InstallOptions: # Constant for option descriptions 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.", "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.", "mrdocs_src_dir": "MrDocs source directory.", "mrdocs_repo": "URL of the MrDocs repository to clone.", "mrdocs_branch": "Branch or tag of the MrDocs repository to use.", @@ -136,6 +149,7 @@ class InstallOptions: "non_interactive": "Whether to use all default options without interactive prompts." } + class MrDocsInstaller: """ Handles the installation workflow for MrDocs and its third-party dependencies. @@ -180,7 +194,7 @@ def prompt_string(self, prompt, default): result = inp.strip() or default return result - def prompt_boolean(self, prompt, default = None): + def prompt_boolean(self, prompt, default=None): """ Prompts the user for a boolean value (yes/no). @@ -248,33 +262,45 @@ def prompt_option(self, name, force_prompt=False): constains_placeholder = "<" in default_value and ">" in default_value if constains_placeholder: has_dir_key = False + def repl(match): nonlocal has_dir_key key = match.group(1) - has_dir_key = has_dir_key or key.endswith("-dir") - key = key.replace("-", "_") transform_fn = match.group(2) - if key == 'os': - if self.is_windows(): - val = "windows" - elif self.is_linux(): - val = "linux" - elif self.is_macos(): - val = "macos" - else: - raise ValueError("Unsupported operating system.") + has_dir_key = has_dir_key or key.endswith("-dir") + key_surrounded_by_quotes = key.startswith('"') and key.endswith('"') + if key_surrounded_by_quotes: + val = key[1:-1] else: - val = getattr(self.options, key, None) + if key == 'os': + if self.is_windows(): + val = "windows" + elif self.is_linux(): + val = "linux" + elif self.is_macos(): + val = "macos" + else: + raise ValueError("Unsupported operating system.") + else: + key = key.replace("-", "_") + val = getattr(self.options, key, None) + if transform_fn: if transform_fn == "lower": val = val.lower() elif transform_fn == "upper": val = val.upper() - # Add more formats as needed + elif transform_fn == "basename": + val = os.path.basename(val) + elif transform_fn == "if(cc)": + if self.options.cc: + val = val.lower() + else: + val = "" return val # Regex: or - pattern = r"<([a-zA-Z0-9_\-]+)(?::([a-zA-Z0-9_\-]+))?>" + pattern = r"<([\"a-zA-Z0-9_\-]+)(?::([a-zA-Z0-9_\-\(\)]+))?>" default_value = re.sub(pattern, repl, default_value) if has_dir_key: default_value = os.path.abspath(default_value) @@ -340,12 +366,14 @@ def run_cmd(self, cmd, cwd=None): color = BLUE if self.supports_ansi() else "" reset = RESET if self.supports_ansi() else "" if isinstance(cmd, list): - print(f"{color}{cwd}> {' '.join(cmd)}{reset}") + cmd_str = ' '.join(shlex.quote(arg) for arg in cmd) + print(f"{color}{cwd}> {cmd_str}{reset}") else: print(f"{color}{cwd}> {cmd}{reset}") r = subprocess.run(cmd, shell=isinstance(cmd, str), check=True, cwd=cwd) if r.returncode != 0: raise RuntimeError(f"Command '{cmd}' failed with return code {r.returncode}.") + def clone_repo(self, repo, dest, branch=None, depth=None): """ Clones a Git repository into the specified destination directory. @@ -379,25 +407,25 @@ def download_file(self, url, dest): urllib.request.urlretrieve(url, dest) def is_windows(self): - """ - Checks if the current operating system is Windows. - :return: bool: True if the OS is Windows, False otherwise. - """ - return os.name == "nt" + """ + Checks if the current operating system is Windows. + :return: bool: True if the OS is Windows, False otherwise. + """ + return os.name == "nt" def is_linux(self): - """ - Checks if the current operating system is Linux. - :return: bool: True if the OS is Linux, False otherwise. - """ - return os.name == "posix" and sys.platform.startswith("linux") + """ + Checks if the current operating system is Linux. + :return: bool: True if the OS is Linux, False otherwise. + """ + return os.name == "posix" and sys.platform.startswith("linux") def is_macos(self): - """ - Checks if the current operating system is macOS. - :return: bool: True if the OS is macOS, False otherwise. - """ - return os.name == "posix" and sys.platform.startswith("darwin") + """ + Checks if the current operating system is macOS. + :return: bool: True if the OS is macOS, False otherwise. + """ + return os.name == "posix" and sys.platform.startswith("darwin") def cmake_workflow(self, src_dir, build_type, build_dir, install_dir, extra_args=None): """ @@ -409,6 +437,10 @@ def cmake_workflow(self, src_dir, build_type, build_dir, install_dir, extra_args """ config_args = [self.options.cmake_path, "-S", src_dir] + if self.options.cc and self.options.cxx: + config_args.extend(["-DCMAKE_C_COMPILER=" + self.options.cc, + "-DCMAKE_CXX_COMPILER=" + self.options.cxx]) + # "DebWithOpt" is not a valid type. However, we interpret it as a special case # where the build type is Debug and optimizations are enabled. # This is not very different from RelWithDebInfo on Unix, but ensures @@ -416,12 +448,13 @@ def cmake_workflow(self, src_dir, build_type, build_dir, install_dir, extra_args build_type_is_debwithopt = build_type.lower() == 'debwithopt' cmake_build_type = build_type if not build_type_is_debwithopt else "Debug" if build_dir: - config_args.extend(["-B", build_dir]) + config_args.extend(["-B", build_dir]) if build_type: config_args.extend([f"-DCMAKE_BUILD_TYPE={cmake_build_type}"]) if build_type_is_debwithopt: if self.is_windows(): - config_args.extend(["-DCMAKE_CXX_FLAGS=/DWIN32 /D_WINDOWS /Ob1 /O2 /Zi", "-DCMAKE_C_FLAGS=/DWIN32 /D_WINDOWS /Ob1 /O2 /Zi"]) + config_args.extend(["-DCMAKE_CXX_FLAGS=/DWIN32 /D_WINDOWS /Ob1 /O2 /Zi", + "-DCMAKE_C_FLAGS=/DWIN32 /D_WINDOWS /Ob1 /O2 /Zi"]) else: config_args.extend(["-DCMAKE_CXX_FLAGS=-Og -g", "-DCMAKE_C_FLAGS=-Og -g"]) if isinstance(extra_args, str): @@ -432,7 +465,7 @@ def cmake_workflow(self, src_dir, build_type, build_dir, install_dir, extra_args build_args = [self.options.cmake_path, "--build", build_dir, "--config", cmake_build_type] num_cores = os.cpu_count() or 1 - max_safe_parallel = 4 # Ideally 4GB per job + max_safe_parallel = 4 # Ideally 4GB per job build_args.extend(["--parallel", str(min(num_cores, max_safe_parallel))]) self.run_cmd(build_args) @@ -474,6 +507,18 @@ def check_tool(self, tool): if not self.is_executable(tool_path): raise FileNotFoundError(f"{tool} executable not found at {tool_path}.") + def check_compilers(self): + for option in ["cc", "cxx"]: + self.prompt_option(option) + if getattr(self.options, option): + if not os.path.isabs(getattr(self.options, option)): + exec = shutil.which(getattr(self.options, option)) + if exec is None: + raise FileNotFoundError(f"{option} executable '{getattr(self.options, option)}' not found in PATH.") + setattr(self.options, option, exec) + if not self.is_executable(getattr(self.options, option)): + raise FileNotFoundError(f"{option} executable not found at {getattr(self.options, option)}.") + def check_tools(self): tools = ["git", "cmake"] for tool in tools: @@ -484,7 +529,9 @@ def setup_mrdocs_dir(self): if not os.path.isabs(self.options.mrdocs_src_dir): self.options.mrdocs_src_dir = os.path.abspath(self.options.mrdocs_src_dir) if not os.path.exists(self.options.mrdocs_src_dir): - if not self.prompt_boolean(f"Source directory '{self.options.mrdocs_src_dir}' does not exist. Create and clone MrDocs there?", True): + if not self.prompt_boolean( + f"Source directory '{self.options.mrdocs_src_dir}' does not exist. Create and clone MrDocs there?", + True): print("Installation aborted by user.") return self.prompt_option("mrdocs_branch") @@ -492,11 +539,13 @@ def setup_mrdocs_dir(self): self.clone_repo(self.options.mrdocs_repo, self.options.mrdocs_src_dir, branch=self.options.mrdocs_branch) else: if not os.path.isdir(self.options.mrdocs_src_dir): - raise NotADirectoryError(f"Specified mrdocs_src_dir '{self.options.mrdocs_src_dir}' is not a directory.") + raise NotADirectoryError( + f"Specified mrdocs_src_dir '{self.options.mrdocs_src_dir}' is not a directory.") # MrDocs build type self.prompt_build_type_option("mrdocs_build_type") - self.prompt_option("mrdocs_build_tests") + if self.prompt_option("mrdocs_build_tests"): + self.check_tool("java") def is_inside_mrdocs_dir(self, path): """ @@ -567,14 +616,16 @@ def install_duktape(self): self.prompt_build_type_option("duktape_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) + self.cmake_workflow(self.options.duktape_src_dir, self.options.duktape_build_type, + self.options.duktape_build_dir, self.options.duktape_install_dir) def install_libxml2(self): self.prompt_dependency_path_option("libxml2_src_dir") if not os.path.exists(self.options.libxml2_src_dir): self.prompt_option("libxml2_repo") self.prompt_option("libxml2_branch") - self.clone_repo(self.options.libxml2_repo, self.options.libxml2_src_dir, branch=self.options.libxml2_branch, depth=1) + self.clone_repo(self.options.libxml2_repo, self.options.libxml2_src_dir, branch=self.options.libxml2_branch, + depth=1) self.prompt_build_type_option("libxml2_build_type") self.prompt_dependency_path_option("libxml2_build_dir") self.prompt_dependency_path_option("libxml2_install_dir") @@ -614,7 +665,8 @@ def install_libxml2(self): "-DLIBXML2_WITH_XPATH=ON", "-DLIBXML2_WITH_XPTR=ON" ] - self.cmake_workflow(self.options.libxml2_src_dir, self.options.libxml2_build_type, self.options.libxml2_build_dir, self.options.libxml2_install_dir, extra_args) + self.cmake_workflow(self.options.libxml2_src_dir, self.options.libxml2_build_type, + self.options.libxml2_build_dir, self.options.libxml2_install_dir, extra_args) def install_llvm(self): self.prompt_dependency_path_option("llvm_src_dir") @@ -642,7 +694,8 @@ def install_llvm(self): cmake_extra_args.append("-DLLVM_ENABLE_RUNTIMES=libcxx") else: cmake_extra_args.append("-DLLVM_ENABLE_RUNTIMES=libcxx;libcxxabi;libunwind") - self.cmake_workflow(llvm_subproject_dir, self.options.llvm_build_type, self.options.llvm_build_dir, self.options.llvm_install_dir, cmake_extra_args) + 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 @@ -686,10 +739,13 @@ def create_cmake_presets(self): parent_preset_name = "relwithdebinfo" build_type_is_debwithopt = self.options.mrdocs_build_type.lower() == 'debwithopt' cmake_build_type = self.options.mrdocs_build_type if not build_type_is_debwithopt else "Debug" + display_name = f"{self.options.mrdocs_build_type} {OSDisplayName}" + if self.options.cc: + display_name += f" ({os.path.basename(self.options.cc)})" new_preset = { "name": self.options.mrdocs_preset_name, - "displayName": f"{self.options.mrdocs_build_type} {OSDisplayName}", - "description": f"Preset for building MrDocs in {self.options.mrdocs_build_type} mode with the default compiler in {OSDisplayName}.", + "displayName": display_name, + "description": f"Preset for building MrDocs in {self.options.mrdocs_build_type} mode with the {os.path.basename(self.options.cc) if self.options.cc else 'default'} compiler in {OSDisplayName}.", "inherits": parent_preset_name, "binaryDir": "${sourceDir}/build/${presetName}", "cacheVariables": { @@ -759,7 +815,9 @@ def install_mrdocs(self): # Build directory specified in the preset self.prompt_option("mrdocs_build_dir") else: - self.options.mrdocs_build_dir = os.path.join(self.options.mrdocs_src_dir, "build", self.options.mrdocs_preset_name) + self.options.mrdocs_build_dir = os.path.join(self.options.mrdocs_src_dir, "build", + self.options.mrdocs_preset_name) + self.default_options.mrdocs_build_dir = self.options.mrdocs_build_dir if not self.prompt_option("mrdocs_system_install"): # Build directory specified in the preset @@ -786,14 +844,17 @@ def install_mrdocs(self): "-D", f"LibXml2_ROOT={self.options.libxml2_install_dir}" ]) extra_args.extend(["-D", "MRDOCS_BUILD_TESTS=ON"]) - extra_args.extend(["-DMRDOCS_BUILD_DOCS=OFF", "-DMRDOCS_GENERATE_REFERENCE=OFF", "-DMRDOCS_GENERATE_ANTORA_REFERENCE=OFF"]) + extra_args.extend(["-DMRDOCS_BUILD_DOCS=OFF", "-DMRDOCS_GENERATE_REFERENCE=OFF", + "-DMRDOCS_GENERATE_ANTORA_REFERENCE=OFF"]) - 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) + 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") if not os.path.exists(ctest_path): - raise FileNotFoundError(f"ctest executable not found at {ctest_path}. Please ensure CMake is installed correctly.") + raise FileNotFoundError( + f"ctest executable not found at {ctest_path}. Please ensure CMake is installed correctly.") test_args = [ctest_path, "--test-dir", self.options.mrdocs_build_dir, "--output-on-failure", "--progress", "--no-tests=error", "--output-on-failure", "--parallel", str(os.cpu_count() or 1)] self.run_cmd(test_args) @@ -805,7 +866,6 @@ def install_mrdocs(self): else: print(f"\nMrDocs has been successfully installed in {self.options.mrdocs_install_dir}.\n") - def generate_clion_run_configs(self, configs): import xml.etree.ElementTree as ET @@ -818,25 +878,154 @@ def generate_clion_run_configs(self, configs): config_name = config["name"] run_config_path = os.path.join(run_dir, f"{config_name}.run.xml") root = ET.Element("component", name="ProjectRunConfigurationManager") - config = ET.SubElement(root, "configuration", { - "default": "false", - "name": config["name"], - "type": "CMakeRunConfiguration", - "factoryName": "Application", - "PROGRAM_PARAMS": " ".join(config["args"]), - "REDIRECT_INPUT": "false", - "ELEVATE": "false", - "USE_EXTERNAL_CONSOLE": "false", - "EMULATE_TERMINAL": "false", - "PASS_PARENT_ENVS_2": "true", - "PROJECT_NAME": "MrDocs", - "TARGET_NAME": config["target"], - "CONFIG_NAME": self.options.mrdocs_preset_name or "debug", - "RUN_TARGET_PROJECT_NAME": "MrDocs", - "RUN_TARGET_NAME": config["target"] - }) - method = ET.SubElement(config, "method", v="2") - ET.SubElement(method, "option", name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask", enabled="true") + if 'target' in config: + attrib = { + "default": "false", + "name": config["name"], + "type": "CMakeRunConfiguration", + "factoryName": "Application", + "PROGRAM_PARAMS": ' '.join(shlex.quote(arg) for arg in config["args"]), + "REDIRECT_INPUT": "false", + "ELEVATE": "false", + "USE_EXTERNAL_CONSOLE": "false", + "EMULATE_TERMINAL": "false", + "PASS_PARENT_ENVS_2": "true", + "PROJECT_NAME": "MrDocs", + "TARGET_NAME": config["target"], + "CONFIG_NAME": self.options.mrdocs_preset_name or "debug", + "RUN_TARGET_PROJECT_NAME": "MrDocs", + "RUN_TARGET_NAME": config["target"] + } + if 'folder' in config: + attrib["folderName"] = config["folder"] + clion_config = ET.SubElement(root, "configuration", attrib) + if 'env' in config: + envs = ET.SubElement(clion_config, "envs") + for key, value in config['env'].items(): + ET.SubElement(envs, "env", name=key, value=value) + method = ET.SubElement(clion_config, "method", v="2") + ET.SubElement(method, "option", + name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask", + enabled="true") + elif 'script' in config: + if config["script"].endswith(".py"): + attrib = { + "default": "false", + "name": config["name"], + "type": "PythonConfigurationType", + "factoryName": "Python", + "nameIsGenerated": "false" + } + if 'folder' in config: + attrib["folderName"] = config["folder"] + clion_config = ET.SubElement(root, "configuration", attrib) + ET.SubElement(clion_config, "module", name="mrdocs") + ET.SubElement(clion_config, "option", name="ENV_FILES", value="") + ET.SubElement(clion_config, "option", name="INTERPRETER_OPTIONS", value="") + ET.SubElement(clion_config, "option", name="PARENT_ENVS", value="true") + envs = ET.SubElement(clion_config, "envs") + ET.SubElement(envs, "env", name="PYTHONUNBUFFERED", value="1") + ET.SubElement(clion_config, "option", name="SDK_HOME", value="") + if 'cwd' in config and config["cwd"] != self.options.mrdocs_src_dir: + ET.SubElement(clion_config, "option", name="WORKING_DIRECTORY", value=config["cwd"]) + else: + ET.SubElement(clion_config, "option", name="WORKING_DIRECTORY", value="$PROJECT_DIR$") + ET.SubElement(clion_config, "option", name="IS_MODULE_SDK", value="true") + ET.SubElement(clion_config, "option", name="ADD_CONTENT_ROOTS", value="true") + ET.SubElement(clion_config, "option", name="ADD_SOURCE_ROOTS", value="true") + ET.SubElement(clion_config, "option", name="SCRIPT_NAME", value=config["script"]) + ET.SubElement(clion_config, "option", name="PARAMETERS", + value=' '.join(shlex.quote(arg) for arg in config["args"])) + ET.SubElement(clion_config, "option", name="SHOW_COMMAND_LINE", value="false") + ET.SubElement(clion_config, "option", name="EMULATE_TERMINAL", value="false") + ET.SubElement(clion_config, "option", name="MODULE_MODE", value="false") + ET.SubElement(clion_config, "option", name="REDIRECT_INPUT", value="false") + ET.SubElement(clion_config, "option", name="INPUT_FILE", value="") + ET.SubElement(clion_config, "method", v="2") + elif config["script"].endswith(".sh"): + attrib = { + "default": "false", + "name": config["name"], + "type": "ShConfigurationType" + } + if 'folder' in config: + attrib["folderName"] = config["folder"] + clion_config = ET.SubElement(root, "configuration", attrib) + ET.SubElement(clion_config, "option", name="SCRIPT_TEXT", value=f"bash {shlex.quote(config['script'])}") + ET.SubElement(clion_config, "option", name="INDEPENDENT_SCRIPT_PATH", value="true") + ET.SubElement(clion_config, "option", name="SCRIPT_PATH", value=config["script"]) + ET.SubElement(clion_config, "option", name="SCRIPT_OPTIONS", value="") + ET.SubElement(clion_config, "option", name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY", value="true") + if 'cwd' in config and config["cwd"] != self.options.mrdocs_src_dir: + ET.SubElement(clion_config, "option", name="SCRIPT_WORKING_DIRECTORY", value=config["cwd"]) + else: + ET.SubElement(clion_config, "option", name="SCRIPT_WORKING_DIRECTORY", value="$PROJECT_DIR$") + ET.SubElement(clion_config, "option", name="INDEPENDENT_INTERPRETER_PATH", value="true") + ET.SubElement(clion_config, "option", name="INTERPRETER_PATH", value="") + ET.SubElement(clion_config, "option", name="INTERPRETER_OPTIONS", value="") + ET.SubElement(clion_config, "option", name="EXECUTE_IN_TERMINAL", value="true") + ET.SubElement(clion_config, "option", name="EXECUTE_SCRIPT_FILE", value="false") + ET.SubElement(clion_config, "envs") + ET.SubElement(clion_config, "method", v="2") + elif config["script"].endswith(".js"): + attrb = { + "default": "false", + "name": config["name"], + "type": "NodeJSConfigurationType", + "path-to-js-file": config["script"], + "working-dir": config.get("cwd", "$PROJECT_DIR$") + } + if 'folder' in config: + attrb["folderName"] = config["folder"] + clion_config = ET.SubElement(root, "configuration", attrb) + envs = ET.SubElement(clion_config, "envs") + if 'env' in config: + for key, value in config['env'].items(): + ET.SubElement(envs, "env", name=key, value=value) + ET.SubElement(clion_config, "method", v="2") + elif config["script"] == "npm": + attrib = { + "default": "false", + "name": config["name"], + "type": "js.build_tools.npm" + } + if 'folder' in config: + attrib["folderName"] = config["folder"] + clion_config = ET.SubElement(root, "configuration", attrib) + ET.SubElement(clion_config, "package-json", value=os.path.join(config["cwd"], "package.json")) + ET.SubElement(clion_config, "command", value=config["args"][0] if config["args"] else "ci") + ET.SubElement(clion_config, "node-interpreter", value="project") + envs = ET.SubElement(clion_config, "envs") + if 'env' in config: + for key, value in config['env'].items(): + ET.SubElement(envs, "env", name=key, value=value) + ET.SubElement(clion_config, "method", v="2") + else: + attrib = { + "default": "false", + "name": config["name"], + "type": "ShConfigurationType" + } + if 'folder' in config: + attrib["folderName"] = config["folder"] + clion_config = ET.SubElement(root, "configuration", attrib) + ET.SubElement(clion_config, "option", name="SCRIPT_TEXT", value=f"{shlex.quote(config['script'])} {' '.join(shlex.quote(arg) for arg in config['args'])}") + ET.SubElement(clion_config, "option", name="INDEPENDENT_SCRIPT_PATH", value="true") + ET.SubElement(clion_config, "option", name="SCRIPT_PATH", value=config["script"]) + ET.SubElement(clion_config, "option", name="SCRIPT_OPTIONS", value="") + ET.SubElement(clion_config, "option", name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY", value="true") + if 'cwd' in config and config["cwd"] != self.options.mrdocs_src_dir: + ET.SubElement(clion_config, "option", name="SCRIPT_WORKING_DIRECTORY", value=config["cwd"]) + else: + ET.SubElement(clion_config, "option", name="SCRIPT_WORKING_DIRECTORY", value="$PROJECT_DIR$") + ET.SubElement(clion_config, "option", name="INDEPENDENT_INTERPRETER_PATH", value="true") + ET.SubElement(clion_config, "option", name="INTERPRETER_PATH", value="") + ET.SubElement(clion_config, "option", name="INTERPRETER_OPTIONS", value="") + ET.SubElement(clion_config, "option", name="EXECUTE_IN_TERMINAL", value="true") + ET.SubElement(clion_config, "option", name="EXECUTE_SCRIPT_FILE", value="false") + ET.SubElement(clion_config, "envs") + ET.SubElement(clion_config, "method", v="2") + tree = ET.ElementTree(root) tree.write(run_config_path, encoding="utf-8", xml_declaration=False) @@ -854,24 +1043,30 @@ def generate_visual_studio_run_configs(self, configs): launch_data = {"version": "0.2.1", "configurations": []} # Build a dict for quick lookup by name - configs_by_name = {cfg.get("name"): cfg for cfg in launch_data.get("configurations", [])} + vs_configs_by_name = {cfg.get("name"): cfg for cfg in launch_data.get("configurations", [])} for config in configs: new_cfg = { "name": config["name"], "type": "default", "project": "MrDocs", - "projectTarget": config["target"], "args": config["args"], - "cwd": self.options.mrdocs_build_dir, + "cwd": config.get('cwd', self.options.mrdocs_build_dir), "env": {}, - "stopAtEntry": False + "stopAtEntry": False, + "console": "integratedTerminal" } + + if 'target' in config: + new_cfg["projectTarget"] = config["target"] + if 'script' in config: + new_cfg["program"] = config["script"] + # Replace or add - configs_by_name[config["name"]] = new_cfg + vs_configs_by_name[config["name"]] = new_cfg # Write back all configs - launch_data["configurations"] = list(configs_by_name.values()) + launch_data["configurations"] = list(vs_configs_by_name.values()) with open(launch_path, "w") as f: json.dump(launch_data, f, indent=4) @@ -903,6 +1098,7 @@ def generate_run_configs(self): configs.append({ "name": f"MrDocs {verb.title()} Test Fixtures ({generator.upper()})", "target": "mrdocs-test", + "folder": "MrDocs Test Fixtures", "args": [ f'"{self.options.mrdocs_src_dir}/test-files/golden-tests"', '--unit=false', @@ -916,6 +1112,7 @@ def generate_run_configs(self): ] }) + num_cores = os.cpu_count() or 1 self.prompt_option("boost_src_dir") if self.options.boost_src_dir and os.path.exists(self.options.boost_src_dir): boost_libs = os.path.join(self.options.boost_src_dir, 'libs') @@ -928,27 +1125,229 @@ def generate_run_configs(self): "name": f"Boost.{lib.title()} Documentation", "target": "mrdocs", "args": [ - '"../CMakeLists.txt"', - f'--config="{self.options.boost_src_dir}/libs/{lib}/doc/mrdocs.yml"', - f'--output="{self.options.boost_src_dir}/libs/{lib}/doc/modules/reference/pages"', + '../CMakeLists.txt', + f'--config={self.options.boost_src_dir}/libs/{lib}/doc/mrdocs.yml', + f'--output={self.options.boost_src_dir}/libs/{lib}/doc/modules/reference/pages', f'--generator=adoc', - f'--addons="{self.options.mrdocs_src_dir}/share/mrdocs/addons"', - f'--stdlib-includes="{self.options.llvm_install_dir}/include/c++/v1"', - f'--stdlib-includes="{self.options.llvm_install_dir}/lib/clang/20/include"', - f'--libc-includes="{self.options.mrdocs_src_dir}/share/mrdocs/headers/libc-stubs"', + f'--addons={self.options.mrdocs_src_dir}/share/mrdocs/addons', + f'--stdlib-includes={self.options.llvm_install_dir}/include/c++/v1', + f'--stdlib-includes={self.options.llvm_install_dir}/lib/clang/20/include', + f'--libc-includes={self.options.mrdocs_src_dir}/share/mrdocs/headers/libc-stubs', f'--tagfile=reference.tag.xml', '--multipage=true', - '--concurrency=32', + f'--concurrency={num_cores}', '--log-level=debug' ] }) else: - print(f"Warning: Boost source directory '{self.options.boost_src_dir}' does not contain 'libs' directory. Skipping Boost documentation target generation.") + print( + f"Warning: Boost source directory '{self.options.boost_src_dir}' does not contain 'libs' directory. Skipping Boost documentation target generation.") + + # Target to generate the documentation for MrDocs itself + configs.append({ + "name": f"MrDocs Self-Reference", + "target": "mrdocs", + "args": [ + '../CMakeLists.txt', + f'--config={self.options.mrdocs_src_dir}/docs/mrdocs.yml', + f'--output={self.options.mrdocs_src_dir}/docs/modules/reference/pages', + f'--generator=adoc', + f'--addons={self.options.mrdocs_src_dir}/share/mrdocs/addons', + f'--stdlib-includes={self.options.llvm_install_dir}/include/c++/v1', + f'--stdlib-includes={self.options.llvm_install_dir}/lib/clang/20/include', + f'--libc-includes={self.options.mrdocs_src_dir}/share/mrdocs/headers/libc-stubs', + f'--tagfile=reference.tag.xml', + '--multipage=true', + f'--concurrency={num_cores}', + '--log-level=debug' + ], + "env": { + "LLVM_ROOT": self.options.llvm_install_dir, + "Clang_ROOT": self.options.llvm_install_dir, + "duktape_ROOT": self.options.duktape_install_dir, + "Duktape_ROOT": self.options.duktape_install_dir, + "libxml2_ROOT": self.options.libxml2_install_dir, + "LibXml2_ROOT": self.options.libxml2_install_dir + } + }) + + # bootstrap.py targets + configs.append({ + "name": f"MrDocs Bootstrap Help", + "script": os.path.join(self.options.mrdocs_src_dir, "bootstrap.py"), + "args": ["--help"], + "cwd": self.options.mrdocs_src_dir + }) + + bootstrap_args = [] + for field in dataclasses.fields(InstallOptions): + value = getattr(self.options, field.name) + default_value = getattr(self.default_options, field.name, None) + if value is not None and (value != default_value or field.name == 'mrdocs_build_type'): + if field.name == 'non_interactive': + # Skip non_interactive as it is handled separately, + continue + if field.type is bool: + if value: + bootstrap_args.append(f"--{field.name.replace('_', '-')}") + else: + bootstrap_args.append(f"--no-{field.name.replace('_', '-')}") + elif field.type is str: + if value != '': + bootstrap_args.append(f"--{field.name.replace('_', '-')}") + bootstrap_args.append(value) + else: + raise TypeError(f"Unsupported type {field.type} for field '{field.name}' in InstallOptions.") + bootstrap_refresh_config_name = self.options.mrdocs_preset_name or self.options.mrdocs_build_type or "debug" + configs.append({ + "name": f"MrDocs Bootstrap Update ({bootstrap_refresh_config_name})", + "script": os.path.join(self.options.mrdocs_src_dir, "bootstrap.py"), + "folder": "MrDocs Bootstrap Update", + "args": bootstrap_args, + "cwd": self.options.mrdocs_src_dir + }) + bootstrap_refresh_args = bootstrap_args.copy() + bootstrap_refresh_args.append("--non-interactive") + configs.append({ + "name": f"MrDocs Bootstrap Refresh ({bootstrap_refresh_config_name})", + "script": os.path.join(self.options.mrdocs_src_dir, "bootstrap.py"), + "folder": "MrDocs Bootstrap Refresh", + "args": bootstrap_refresh_args, + "cwd": self.options.mrdocs_src_dir + }) + + # Targets for the pre-build steps + configs.append({ + "name": f"MrDocs Generate Config Info ({bootstrap_refresh_config_name})", + "script": os.path.join(self.options.mrdocs_src_dir, 'util', 'generate-config-info.py'), + "folder": "MrDocs Generate Config Info", + "args": [os.path.join(self.options.mrdocs_src_dir, 'src', 'lib', 'ConfigOptions.json'), + os.path.join(self.options.mrdocs_build_dir)], + "cwd": self.options.mrdocs_src_dir + }) + configs.append({ + "name": f"MrDocs Generate YAML Schema", + "script": os.path.join(self.options.mrdocs_src_dir, 'util', 'generate-yaml-schema.py'), + "args": [], + "cwd": self.options.mrdocs_src_dir + }) + + # Documentation generation targets + mrdocs_docs_dir = os.path.join(self.options.mrdocs_src_dir, "docs") + mrdocs_docs_ui_dir = os.path.join(mrdocs_docs_dir, "ui") + mrdocs_docs_script_ext = "bat" if self.is_windows() else "sh" + configs.append({ + "name": "MrDocs Build Local Docs", + "script": os.path.join(mrdocs_docs_dir, f"build_local_docs.{mrdocs_docs_script_ext}"), + "args": [], + "cwd": mrdocs_docs_dir + }) + configs.append({ + "name": "MrDocs Build Docs", + "script": os.path.join(mrdocs_docs_dir, f"build_docs.{mrdocs_docs_script_ext}"), + "args": [], + "cwd": mrdocs_docs_dir + }) + configs.append({ + "name": "MrDocs Build UI Bundle", + "script": os.path.join(mrdocs_docs_ui_dir, f"build.{mrdocs_docs_script_ext}"), + "args": [], + "cwd": mrdocs_docs_ui_dir + }) + + # Remove bad test files + test_files_dir = os.path.join(self.options.mrdocs_src_dir, "test-files", "golden-tests") + configs.append({ + "name": "MrDocs Remove Bad Test Files", + "script": os.path.join(test_files_dir, f"remove_bad_files.{mrdocs_docs_script_ext}"), + "args": [], + "cwd": test_files_dir + }) + + # Render landing page + mrdocs_website_dir = os.path.join(mrdocs_docs_dir, "website") + configs.append({ + "name": f"MrDocs Render Landing Page ({bootstrap_refresh_config_name})", + "script": os.path.join(mrdocs_website_dir, "render.js"), + "folder": "MrDocs Render Landing Page", + "args": [], + "cwd": mrdocs_website_dir, + "env": { + "NODE_ENV": "production", + "MRDOCS_ROOT": self.options.mrdocs_install_dir + } + }) + configs.append({ + "name": f"MrDocs Clean Install Website Dependencies", + "script": "npm", + "args": ["ci"], + "cwd": mrdocs_website_dir + }) + configs.append({ + "name": f"MrDocs Install Website Dependencies", + "script": "npm", + "args": ["install"], + "cwd": mrdocs_website_dir + }) + + # XML schema tests + if self.options.java_path: + configs.append({ + "name": "MrDocs Generate RelaxNG Schema", + "script": self.options.java_path, + "args": [ + "-jar", + os.path.join(self.options.mrdocs_src_dir, "util", "trang.jar"), + os.path.join(self.options.mrdocs_src_dir, "mrdocs.rnc"), + os.path.join(self.options.mrdocs_build_dir, "mrdocs.rng") + ], + "cwd": self.options.mrdocs_src_dir + }) + libxml2_xmllint_executable = os.path.join(self.options.libxml2_install_dir, "bin", "xmllint") + xml_sources_dir = os.path.join(self.options.mrdocs_src_dir, "test-files", "golden-tests") + + if self.is_windows(): + xml_sources = [] + for root, _, files in os.walk(xml_sources_dir): + for file in files: + if file.endswith(".xml") and not file.endswith(".bad.xml"): + xml_sources.append(os.path.join(root, file)) + configs.append({ + "name": "MrDocs XML Lint with RelaxNG Schema", + "script": libxml2_xmllint_executable, + "args": [ + "--dropdtd", + "--noout", + "--relaxng", + os.path.join(self.options.mrdocs_build_dir, "mrdocs.rng") + ].extend(xml_sources), + "cwd": self.options.mrdocs_src_dir + }) + else: + configs.append({ + "name": "MrDocs XML Lint with RelaxNG Schema", + "script": "find", + "args": [ + xml_sources_dir, + "-type", "f", + "-name", "*.xml", + "!", "-name", "*.bad.xml", + "-exec", libxml2_xmllint_executable, + "--dropdtd", "--noout", + "--relaxng", os.path.join(self.options.mrdocs_build_dir, "mrdocs.rng"), + "{}", "+" + ], + "cwd": self.options.mrdocs_src_dir + }) + + print("Generating CLion run configurations for MrDocs...") self.generate_clion_run_configs(configs) + print("Generating Visual Studio run configurations for MrDocs...") self.generate_visual_studio_run_configs(configs) def install_all(self): + self.check_compilers() self.check_tools() self.setup_mrdocs_dir() self.setup_third_party_dir() @@ -960,6 +1359,9 @@ def install_all(self): self.install_mrdocs() if self.prompt_option("generate_run_configs"): self.generate_run_configs() + else: + print("Skipping run configurations generation as per user preference.") + def get_command_line_args(): """ @@ -970,25 +1372,41 @@ def get_command_line_args(): :return: dict: Dictionary of command line arguments. """ - parser = argparse.ArgumentParser(description="Download and install MrDocs from source.") + parser = argparse.ArgumentParser( + description="Download and install MrDocs from source.", + formatter_class=argparse.RawTextHelpFormatter + ) for field in dataclasses.fields(InstallOptions): arg_name = f"--{field.name.replace('_', '-')}" help_text = INSTALL_OPTION_DESCRIPTIONS.get(field.name) if help_text is None: raise ValueError(f"Missing description for option '{field.name}' in INSTALL_OPTION_DESCRIPTIONS.") - help_text += f" (default: {field.default})" if (field.default is not dataclasses.MISSING and field.default is not None) else "" + if field.default is not dataclasses.MISSING and field.default is not None: + # if string + if isinstance(field.default, str) and field.default: + help_text += f" (default: '{field.default}')" + elif field.default is True: + help_text += " (default: true)" + elif field.default is False: + help_text += " (default: false)" + else: + help_text += f" (default: {field.default})" if field.type is bool: - parser.add_argument(arg_name, action="store_const", const=True, default=None, help=help_text) + parser.add_argument(arg_name, dest=field.name, action='store_true', help=help_text, default=None) + parser.add_argument(f"--no-{field.name.replace('_', '-')}", dest=field.name, action='store_false', + help=f'Set {arg_name} to false', default=None) elif field.type is str: parser.add_argument(arg_name, type=field.type, help=help_text, default=None) else: raise TypeError(f"Unsupported type {field.type} for field '{field.name}' in InstallOptions.") return {k: v for k, v in vars(parser.parse_args()).items() if v is not None} + def main(): args = get_command_line_args() installer = MrDocsInstaller(args) installer.install_all() + if __name__ == "__main__": main() diff --git a/docs/build_docs.bat b/docs/build_docs.bat new file mode 100644 index 0000000000..a668ea5d70 --- /dev/null +++ b/docs/build_docs.bat @@ -0,0 +1,28 @@ +@echo off +REM +REM Licensed under the Apache License v2.0 with LLVM Exceptions. +REM See https://llvm.org/LICENSE.txt for license information. +REM SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +REM +REM Copyright (c) 2025 Alan de Freitas (alandefreitas@gmail.com) +REM +REM Official repository: https://github.com/cppalliance/mrdocs +REM + +echo Building documentation UI +set "cwd=%CD%" +set "script_dir=%~dp0" +if not exist "%script_dir%ui\build\ui-bundle.zip" ( + echo Building antora-ui + pushd "%script_dir%ui" + call build.bat + popd +) + +echo Building documentation with Antora... +echo Installing npm dependencies... +call npm ci + +echo Building docs in custom dir... +call npx antora --clean --fetch antora-playbook.yml +echo Done \ No newline at end of file diff --git a/docs/build_local_docs.bat b/docs/build_local_docs.bat new file mode 100644 index 0000000000..f233f3ad9c --- /dev/null +++ b/docs/build_local_docs.bat @@ -0,0 +1,28 @@ +@echo off +REM +REM Licensed under the Apache License v2.0 with LLVM Exceptions. +REM See https://llvm.org/LICENSE.txt for license information. +REM SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +REM +REM Copyright (c) 2025 Alan de Freitas (alandefreitas@gmail.com) +REM +REM Official repository: https://github.com/cppalliance/mrdocs +REM + +echo Building documentation UI +set "cwd=%CD%" +set "script_dir=%~dp0" +if not exist "%script_dir%ui\build\ui-bundle.zip" ( + echo Building antora-ui + pushd "%script_dir%ui" + call build.bat + popd +) + +echo Building documentation with Antora... +echo Installing npm dependencies... +call npm ci + +echo Building docs in custom dir... +call npx antora --clean --fetch antora-playbook.yml --attribute branchesarray=HEAD +echo Done \ No newline at end of file diff --git a/docs/ui/build.bat b/docs/ui/build.bat new file mode 100644 index 0000000000..807719b77d --- /dev/null +++ b/docs/ui/build.bat @@ -0,0 +1,50 @@ +@echo off +REM +REM Licensed under the Apache License v2.0 with LLVM Exceptions. +REM See https://llvm.org/LICENSE.txt for license information. +REM SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +REM +REM Copyright (c) 2025 Alan de Freitas (alandefreitas@gmail.com) +REM +REM Official repository: https://github.com/cppalliance/mrdocs +REM + +REM Check for npm +where npm >nul 2>nul +if errorlevel 1 ( + echo npm is not installed + exit /b 1 +) + +REM Check for npx +where npx >nul 2>nul +if errorlevel 1 ( + echo npx is not installed + exit /b 1 +) + +REM Install modules if needed +if not exist node_modules ( + call npm ci +) else ( + for %%F in (package.json) do set package_json_time=%%~tF + for %%F in (node_modules) do set node_modules_time=%%~tF + if "%package_json_time%" GTR "%node_modules_time%" ( + call npm ci + ) +) + +REM Lint +call npx gulp lint +if errorlevel 1 ( + set msg=Linting failed. Please run `npx gulp format` before pushing your code. + if "%GITHUB_ACTIONS%"=="true" ( + echo ::error::%msg% + ) else ( + echo %msg% + call npx gulp format + ) +) + +REM Build +call npx gulp \ No newline at end of file diff --git a/docs/website/render.js b/docs/website/render.js index f5c7724a6f..4bbebfcbeb 100644 --- a/docs/website/render.js +++ b/docs/website/render.js @@ -7,7 +7,6 @@ const assert = require('assert'); const path = require('path'); const {execSync} = require('child_process'); - // Read the template file const templateFile = 'index.html.hbs'; const templateSource = fs.readFileSync(templateFile, 'utf8'); diff --git a/test-files/golden-tests/remove_bad_files.bat b/test-files/golden-tests/remove_bad_files.bat new file mode 100644 index 0000000000..8544b897c6 --- /dev/null +++ b/test-files/golden-tests/remove_bad_files.bat @@ -0,0 +1,13 @@ +@echo off +REM This script deletes all files matching *.bad.* in the current directory and subdirectories. + +setlocal enabledelayedexpansion +set count=0 + +for /r %%F in (*.bad.*) do ( + del "%%F" + set /a count+=1 +) + +echo Deleted !count! files. +endlocal \ No newline at end of file