| # @file UncrustifyCheck.py | |
| # | |
| # An edk2-pytool based plugin wrapper for Uncrustify | |
| # | |
| # Copyright (c) Microsoft Corporation. | |
| # SPDX-License-Identifier: BSD-2-Clause-Patent | |
| ## | |
| import configparser | |
| import difflib | |
| import errno | |
| import logging | |
| import os | |
| import pathlib | |
| import shutil | |
| import timeit | |
| from edk2toolext.environment import version_aggregator | |
| from edk2toolext.environment.plugin_manager import PluginManager | |
| from edk2toolext.environment.plugintypes.ci_build_plugin import ICiBuildPlugin | |
| from edk2toolext.environment.plugintypes.uefi_helper_plugin import HelperFunctions | |
| from edk2toolext.environment.var_dict import VarDict | |
| from edk2toollib.gitignore_parser import parse_gitignore_lines | |
| from edk2toollib.log.junit_report_format import JunitReportTestCase | |
| from edk2toollib.uefi.edk2.path_utilities import Edk2Path | |
| from edk2toollib.utility_functions import RunCmd | |
| from io import StringIO | |
| from typing import Any, Dict, List, Tuple | |
| # | |
| # Provide more user friendly messages for certain scenarios | |
| # | |
| class UncrustifyException(Exception): | |
| def __init__(self, message, exit_code): | |
| super().__init__(message) | |
| self.exit_code = exit_code | |
| class UncrustifyAppEnvVarNotFoundException(UncrustifyException): | |
| def __init__(self, message): | |
| super().__init__(message, -101) | |
| class UncrustifyAppVersionErrorException(UncrustifyException): | |
| def __init__(self, message): | |
| super().__init__(message, -102) | |
| class UncrustifyAppExecutionException(UncrustifyException): | |
| def __init__(self, message): | |
| super().__init__(message, -103) | |
| class UncrustifyStalePluginFormattedFilesException(UncrustifyException): | |
| def __init__(self, message): | |
| super().__init__(message, -120) | |
| class UncrustifyInputFileCreationErrorException(UncrustifyException): | |
| def __init__(self, message): | |
| super().__init__(message, -121) | |
| class UncrustifyInvalidIgnoreStandardPathsException(UncrustifyException): | |
| def __init__(self, message): | |
| super().__init__(message, -122) | |
| class UncrustifyGitIgnoreFileException(UncrustifyException): | |
| def __init__(self, message): | |
| super().__init__(message, -140) | |
| class UncrustifyGitSubmoduleException(UncrustifyException): | |
| def __init__(self, message): | |
| super().__init__(message, -141) | |
| class UncrustifyCheck(ICiBuildPlugin): | |
| """ | |
| A CiBuildPlugin that uses Uncrustify to check the source files in the | |
| package being tested for coding standard issues. | |
| By default, the plugin runs against standard C source file extensions but | |
| its configuration can be modified through its configuration file. | |
| Configuration options: | |
| "UncrustifyCheck": { | |
| "AdditionalIncludePaths": [], # Additional paths to check formatting (wildcards supported). | |
| "AuditOnly": False, # Don't fail the build if there are errors. Just log them. | |
| "ConfigFilePath": "", # Custom path to an Uncrustify config file. | |
| "IgnoreStandardPaths": [], # Standard Plugin defined paths that should be ignored. | |
| "OutputFileDiffs": False, # Output chunks of formatting diffs in the test case log. | |
| # This can significantly slow down the plugin on very large packages. | |
| "SkipGitExclusions": False # Don't exclude git ignored files and files in git submodules. | |
| } | |
| """ | |
| # | |
| # By default, use an "uncrustify.cfg" config file in the plugin directory | |
| # A package can override this path via "ConfigFilePath" | |
| # | |
| # Note: Values specified via "ConfigFilePath" are relative to the package | |
| # | |
| DEFAULT_CONFIG_FILE_PATH = os.path.join( | |
| pathlib.Path(__file__).parent.resolve(), "uncrustify.cfg") | |
| # | |
| # The extension used for formatted files produced by this plugin | |
| # | |
| FORMATTED_FILE_EXTENSION = ".uncrustify_plugin" | |
| # | |
| # A package can add any additional paths with "AdditionalIncludePaths" | |
| # A package can remove any of these paths with "IgnoreStandardPaths" | |
| # | |
| STANDARD_PLUGIN_DEFINED_PATHS = ("*.c", "*.h", "*.cpp") | |
| # | |
| # The Uncrustify application path should set in this environment variable | |
| # | |
| UNCRUSTIFY_PATH_ENV_KEY = "UNCRUSTIFY_CI_PATH" | |
| def GetTestName(self, packagename: str, environment: VarDict) -> Tuple: | |
| """ Provide the testcase name and classname for use in reporting | |
| Args: | |
| packagename: string containing name of package to build | |
| environment: The VarDict for the test to run in | |
| Returns: | |
| A tuple containing the testcase name and the classname | |
| (testcasename, classname) | |
| testclassname: a descriptive string for the testcase can include whitespace | |
| classname: should be patterned <packagename>.<plugin>.<optionally any unique condition> | |
| """ | |
| return ("Check file coding standard compliance in " + packagename, packagename + ".UncrustifyCheck") | |
| def RunBuildPlugin(self, package_rel_path: str, edk2_path: Edk2Path, package_config: Dict[str, List[str]], environment_config: Any, plugin_manager: PluginManager, plugin_manager_helper: HelperFunctions, tc: JunitReportTestCase, output_stream=None) -> int: | |
| """ | |
| External function of plugin. This function is used to perform the task of the CiBuild Plugin. | |
| Args: | |
| - package_rel_path: edk2 workspace relative path to the package | |
| - edk2_path: Edk2Path object with workspace and packages paths | |
| - package_config: Dictionary with the package configuration | |
| - environment_config: Environment configuration | |
| - plugin_manager: Plugin Manager Instance | |
| - plugin_manager_helper: Plugin Manager Helper Instance | |
| - tc: JUnit test case | |
| - output_stream: The StringIO output stream from this plugin (logging) | |
| Returns | |
| >0 : Number of errors found | |
| 0 : Passed successfully | |
| -1 : Skipped for missing prereq | |
| """ | |
| try: | |
| # Initialize plugin and check pre-requisites. | |
| self._initialize_environment_info( | |
| package_rel_path, edk2_path, package_config, tc) | |
| self._initialize_configuration() | |
| self._check_for_preexisting_formatted_files() | |
| # Log important context information. | |
| self._log_uncrustify_app_info() | |
| # Get template file contents if specified | |
| self._get_template_file_contents() | |
| # Create meta input files & directories | |
| self._create_temp_working_directory() | |
| self._create_uncrustify_file_list_file() | |
| self._run_uncrustify() | |
| # Post-execution actions. | |
| self._process_uncrustify_results() | |
| except UncrustifyException as e: | |
| self._tc.LogStdError( | |
| f"Uncrustify error {e.exit_code}. Details:\n\n{str(e)}") | |
| logging.warning( | |
| f"Uncrustify error {e.exit_code}. Details:\n\n{str(e)}") | |
| return -1 | |
| else: | |
| if self._formatted_file_error_count > 0: | |
| if self._audit_only_mode: | |
| logging.info( | |
| "Setting test as skipped since AuditOnly is enabled") | |
| self._tc.SetSkipped() | |
| return -1 | |
| else: | |
| self._tc.SetFailed( | |
| f"{self._plugin_name} failed due to {self._formatted_file_error_count} incorrectly formatted files.", "CHECK_FAILED") | |
| else: | |
| self._tc.SetSuccess() | |
| return self._formatted_file_error_count | |
| finally: | |
| self._cleanup_temporary_formatted_files() | |
| self._cleanup_temporary_directory() | |
| def _initialize_configuration(self) -> None: | |
| """ | |
| Initializes plugin configuration. | |
| """ | |
| self._initialize_app_info() | |
| self._initialize_config_file_info() | |
| self._initialize_file_to_format_info() | |
| self._initialize_test_case_output_options() | |
| def _check_for_preexisting_formatted_files(self) -> None: | |
| """ | |
| Checks if any formatted files from prior execution are present. | |
| Existence of such files is an unexpected condition. This might result | |
| from an error that occurred during a previous run or a premature exit from a debug scenario. In any case, the package should be clean before starting a new run. | |
| """ | |
| pre_existing_formatted_file_count = len( | |
| [str(path.resolve()) for path in pathlib.Path(self._abs_package_path).rglob(f'*{UncrustifyCheck.FORMATTED_FILE_EXTENSION}')]) | |
| if pre_existing_formatted_file_count > 0: | |
| raise UncrustifyStalePluginFormattedFilesException( | |
| f"{pre_existing_formatted_file_count} formatted files already exist. To prevent overwriting these files, please remove them before running this plugin.") | |
| def _cleanup_temporary_directory(self) -> None: | |
| """ | |
| Cleans up the temporary directory used for this execution instance. | |
| This removes the directory and all files created during this instance. | |
| """ | |
| if hasattr(self, '_working_dir'): | |
| self._remove_tree(self._working_dir) | |
| def _cleanup_temporary_formatted_files(self) -> None: | |
| """ | |
| Cleans up the temporary formmatted files produced by Uncrustify. | |
| This will recursively remove all formatted files generated by Uncrustify | |
| during this execution instance. | |
| """ | |
| if hasattr(self, '_abs_package_path'): | |
| formatted_files = [str(path.resolve()) for path in pathlib.Path( | |
| self._abs_package_path).rglob(f'*{UncrustifyCheck.FORMATTED_FILE_EXTENSION}')] | |
| for formatted_file in formatted_files: | |
| os.remove(formatted_file) | |
| def _create_temp_working_directory(self) -> None: | |
| """ | |
| Creates the temporary directory used for this execution instance. | |
| """ | |
| self._working_dir = os.path.join( | |
| self._abs_workspace_path, "Build", ".pytool", "Plugin", f"{self._plugin_name}") | |
| try: | |
| pathlib.Path(self._working_dir).mkdir(parents=True, exist_ok=True) | |
| except OSError as e: | |
| raise UncrustifyInputFileCreationErrorException( | |
| f"Error creating plugin directory {self._working_dir}.\n\n{repr(e)}.") | |
| def _create_uncrustify_file_list_file(self) -> None: | |
| """ | |
| Creates the file with the list of source files for Uncrustify to process. | |
| """ | |
| self._app_input_file_path = os.path.join( | |
| self._working_dir, "uncrustify_file_list.txt") | |
| with open(self._app_input_file_path, 'w', encoding='utf8') as f: | |
| f.writelines(f"\n".join(self._abs_file_paths_to_format)) | |
| def _execute_uncrustify(self) -> None: | |
| """ | |
| Executes Uncrustify with the initialized configuration. | |
| """ | |
| output = StringIO() | |
| self._app_exit_code = RunCmd( | |
| self._app_path, | |
| f"-c {self._app_config_file} -F {self._app_input_file_path} --if-changed --suffix {UncrustifyCheck.FORMATTED_FILE_EXTENSION}", outstream=output) | |
| self._app_output = output.getvalue().strip().splitlines() | |
| def _get_files_ignored_in_config(self): | |
| """" | |
| Returns a function that returns true if a given file string path is ignored in the plugin configuration file and false otherwise. | |
| """ | |
| ignored_files = [] | |
| if "IgnoreFiles" in self._package_config: | |
| ignored_files = self._package_config["IgnoreFiles"] | |
| # Pass "Package configuration file" as the source file path since | |
| # the actual configuration file name is unknown to this plugin and | |
| # this provides a generic description of the file that provided | |
| # the ignore file content. | |
| # | |
| # This information is only used for reporting (not used here) and | |
| # the ignore lines are being passed directly as they are given to | |
| # this plugin. | |
| return parse_gitignore_lines(ignored_files, "Package configuration file", self._abs_package_path) | |
| def _get_git_ignored_paths(self) -> List[str]: | |
| """" | |
| Returns a list of file absolute path strings to all files ignored in this git repository. | |
| If git is not found, an empty list will be returned. | |
| """ | |
| if not shutil.which("git"): | |
| logging.warning( | |
| "Git is not found on this system. Git submodule paths will not be considered.") | |
| return [] | |
| outstream_buffer = StringIO() | |
| exit_code = RunCmd("git", "ls-files --other", | |
| workingdir=self._abs_workspace_path, outstream=outstream_buffer, logging_level=logging.NOTSET) | |
| if (exit_code != 0): | |
| raise UncrustifyGitIgnoreFileException( | |
| f"An error occurred reading git ignore settings. This will prevent Uncrustify from running against the expected set of files.") | |
| # Note: This will potentially be a large list, but at least sorted | |
| rel_paths = outstream_buffer.getvalue().strip().splitlines() | |
| abs_paths = [] | |
| for path in rel_paths: | |
| abs_paths.append( | |
| os.path.normpath(os.path.join(self._abs_workspace_path, path))) | |
| return abs_paths | |
| def _get_git_submodule_paths(self) -> List[str]: | |
| """ | |
| Returns a list of directory absolute path strings to the root of each submodule in the workspace repository. | |
| If git is not found, an empty list will be returned. | |
| """ | |
| if not shutil.which("git"): | |
| logging.warning( | |
| "Git is not found on this system. Git submodule paths will not be considered.") | |
| return [] | |
| if os.path.isfile(os.path.join(self._abs_workspace_path, ".gitmodules")): | |
| logging.info( | |
| f".gitmodules file found. Excluding submodules in {self._package_name}.") | |
| outstream_buffer = StringIO() | |
| exit_code = RunCmd("git", "config --file .gitmodules --get-regexp path", workingdir=self._abs_workspace_path, outstream=outstream_buffer, logging_level=logging.NOTSET) | |
| if (exit_code != 0): | |
| raise UncrustifyGitSubmoduleException( | |
| f".gitmodule file detected but an error occurred reading the file. Cannot proceed with unknown submodule paths.") | |
| submodule_paths = [] | |
| for line in outstream_buffer.getvalue().strip().splitlines(): | |
| submodule_paths.append( | |
| os.path.normpath(os.path.join(self._abs_workspace_path, line.split()[1]))) | |
| return submodule_paths | |
| else: | |
| return [] | |
| def _get_template_file_contents(self) -> None: | |
| """ | |
| Gets the contents of Uncrustify template files if they are specified | |
| in the Uncrustify configuration file. | |
| """ | |
| self._file_template_contents = None | |
| self._func_template_contents = None | |
| # Allow no value to allow "set" statements in the config file which do | |
| # not specify value assignment | |
| parser = configparser.ConfigParser(allow_no_value=True) | |
| with open(self._app_config_file, 'r') as cf: | |
| parser.read_string("[dummy_section]\n" + cf.read()) | |
| try: | |
| file_template_name = parser["dummy_section"]["cmt_insert_file_header"] | |
| file_template_path = pathlib.Path(file_template_name) | |
| if not file_template_path.is_file(): | |
| file_template_path = pathlib.Path(os.path.join(self._plugin_path, file_template_name)) | |
| self._file_template_contents = file_template_path.read_text() | |
| except KeyError: | |
| logging.warning("A file header template is not specified in the config file.") | |
| except FileNotFoundError: | |
| logging.warning("The specified file header template file was not found.") | |
| try: | |
| func_template_name = parser["dummy_section"]["cmt_insert_func_header"] | |
| func_template_path = pathlib.Path(func_template_name) | |
| if not func_template_path.is_file(): | |
| func_template_path = pathlib.Path(os.path.join(self._plugin_path, func_template_name)) | |
| self._func_template_contents = func_template_path.read_text() | |
| except KeyError: | |
| logging.warning("A function header template is not specified in the config file.") | |
| except FileNotFoundError: | |
| logging.warning("The specified function header template file was not found.") | |
| def _initialize_app_info(self) -> None: | |
| """ | |
| Initialize Uncrustify application information. | |
| This function will determine the application path and version. | |
| """ | |
| # Verify Uncrustify is specified in the environment. | |
| if UncrustifyCheck.UNCRUSTIFY_PATH_ENV_KEY not in os.environ: | |
| raise UncrustifyAppEnvVarNotFoundException( | |
| f"Uncrustify environment variable {UncrustifyCheck.UNCRUSTIFY_PATH_ENV_KEY} is not present.") | |
| self._app_path = shutil.which('uncrustify', path=os.environ[UncrustifyCheck.UNCRUSTIFY_PATH_ENV_KEY]) | |
| if self._app_path is None: | |
| raise FileNotFoundError( | |
| errno.ENOENT, os.strerror(errno.ENOENT), self._app_path) | |
| self._app_path = os.path.normcase(os.path.normpath(self._app_path)) | |
| if not os.path.isfile(self._app_path): | |
| raise FileNotFoundError( | |
| errno.ENOENT, os.strerror(errno.ENOENT), self._app_path) | |
| # Verify Uncrustify is present at the expected path. | |
| return_buffer = StringIO() | |
| ret = RunCmd(self._app_path, "--version", outstream=return_buffer) | |
| if (ret != 0): | |
| raise UncrustifyAppVersionErrorException( | |
| f"Error occurred executing --version: {ret}.") | |
| # Log Uncrustify version information. | |
| self._app_version = return_buffer.getvalue().strip() | |
| self._tc.LogStdOut(f"Uncrustify version: {self._app_version}") | |
| version_aggregator.GetVersionAggregator().ReportVersion( | |
| "Uncrustify", self._app_version, version_aggregator.VersionTypes.INFO) | |
| def _initialize_config_file_info(self) -> None: | |
| """ | |
| Initialize Uncrustify configuration file info. | |
| The config file path is relative to the package root. | |
| """ | |
| self._app_config_file = UncrustifyCheck.DEFAULT_CONFIG_FILE_PATH | |
| if "ConfigFilePath" in self._package_config: | |
| self._app_config_file = self._package_config["ConfigFilePath"].strip() | |
| self._app_config_file = os.path.normpath( | |
| os.path.join(self._abs_package_path, self._app_config_file)) | |
| if not os.path.isfile(self._app_config_file): | |
| raise FileNotFoundError( | |
| errno.ENOENT, os.strerror(errno.ENOENT), self._app_config_file) | |
| def _initialize_environment_info(self, package_rel_path: str, edk2_path: Edk2Path, package_config: Dict[str, List[str]], tc: JunitReportTestCase) -> None: | |
| """ | |
| Initializes plugin environment information. | |
| """ | |
| self._abs_package_path = edk2_path.GetAbsolutePathOnThisSystemFromEdk2RelativePath( | |
| package_rel_path) | |
| self._abs_workspace_path = edk2_path.WorkspacePath | |
| self._package_config = package_config | |
| self._package_name = os.path.basename( | |
| os.path.normpath(package_rel_path)) | |
| self._plugin_name = self.__class__.__name__ | |
| self._plugin_path = os.path.dirname(os.path.realpath(__file__)) | |
| self._rel_package_path = package_rel_path | |
| self._tc = tc | |
| def _initialize_file_to_format_info(self) -> None: | |
| """ | |
| Forms the list of source files for Uncrustify to process. | |
| """ | |
| # Create a list of all the package relative file paths in the package to run against Uncrustify. | |
| rel_file_paths_to_format = list( | |
| UncrustifyCheck.STANDARD_PLUGIN_DEFINED_PATHS) | |
| # Allow the ci.yaml to remove any of the pre-defined standard paths | |
| if "IgnoreStandardPaths" in self._package_config: | |
| for a in self._package_config["IgnoreStandardPaths"]: | |
| if a.strip() in rel_file_paths_to_format: | |
| self._tc.LogStdOut( | |
| f"Ignoring standard path due to ci.yaml ignore: {a}") | |
| rel_file_paths_to_format.remove(a.strip()) | |
| else: | |
| raise UncrustifyInvalidIgnoreStandardPathsException(f"Invalid IgnoreStandardPaths value: {a}") | |
| # Allow the ci.yaml to specify additional include paths for this package | |
| if "AdditionalIncludePaths" in self._package_config: | |
| rel_file_paths_to_format.extend( | |
| self._package_config["AdditionalIncludePaths"]) | |
| self._abs_file_paths_to_format = [] | |
| for path in rel_file_paths_to_format: | |
| self._abs_file_paths_to_format.extend( | |
| [str(path.resolve()) for path in pathlib.Path(self._abs_package_path).rglob(path)]) | |
| # Remove files ignore in the plugin configuration file | |
| plugin_ignored_files = list(filter(self._get_files_ignored_in_config(), self._abs_file_paths_to_format)) | |
| if plugin_ignored_files: | |
| logging.info( | |
| f"{self._package_name} file count before plugin ignore file exclusion: {len(self._abs_file_paths_to_format)}") | |
| for path in plugin_ignored_files: | |
| if path in self._abs_file_paths_to_format: | |
| logging.info(f" File ignored in plugin config file: {path}") | |
| self._abs_file_paths_to_format.remove(path) | |
| logging.info( | |
| f"{self._package_name} file count after plugin ignore file exclusion: {len(self._abs_file_paths_to_format)}") | |
| if not "SkipGitExclusions" in self._package_config or not self._package_config["SkipGitExclusions"]: | |
| # Remove files ignored by git | |
| logging.info( | |
| f"{self._package_name} file count before git ignore file exclusion: {len(self._abs_file_paths_to_format)}") | |
| ignored_paths = self._get_git_ignored_paths() | |
| self._abs_file_paths_to_format = list( | |
| set(self._abs_file_paths_to_format).difference(ignored_paths)) | |
| logging.info( | |
| f"{self._package_name} file count after git ignore file exclusion: {len(self._abs_file_paths_to_format)}") | |
| # Remove files in submodules | |
| logging.info( | |
| f"{self._package_name} file count before submodule exclusion: {len(self._abs_file_paths_to_format)}") | |
| submodule_paths = tuple(self._get_git_submodule_paths()) | |
| for path in submodule_paths: | |
| logging.info(f" submodule path: {path}") | |
| self._abs_file_paths_to_format = [ | |
| f for f in self._abs_file_paths_to_format if not f.startswith(submodule_paths)] | |
| logging.info( | |
| f"{self._package_name} file count after submodule exclusion: {len(self._abs_file_paths_to_format)}") | |
| # Sort the files for more consistent results | |
| self._abs_file_paths_to_format.sort() | |
| def _initialize_test_case_output_options(self) -> None: | |
| """ | |
| Initializes options that influence test case output. | |
| """ | |
| self._audit_only_mode = False | |
| self._output_file_diffs = True | |
| if "AuditOnly" in self._package_config and self._package_config["AuditOnly"]: | |
| self._audit_only_mode = True | |
| if "OutputFileDiffs" in self._package_config and not self._package_config["OutputFileDiffs"]: | |
| self._output_file_diffs = False | |
| def _log_uncrustify_app_info(self) -> None: | |
| """ | |
| Logs Uncrustify application information. | |
| """ | |
| self._tc.LogStdOut(f"Found Uncrustify at {self._app_path}") | |
| self._tc.LogStdOut(f"Uncrustify version: {self._app_version}") | |
| self._tc.LogStdOut('\n') | |
| logging.info(f"Found Uncrustify at {self._app_path}") | |
| logging.info(f"Uncrustify version: {self._app_version}") | |
| logging.info('\n') | |
| def _process_uncrustify_results(self) -> None: | |
| """ | |
| Process the results from Uncrustify. | |
| Determines whether formatting errors are present and logs failures. | |
| """ | |
| formatted_files = [str(path.resolve()) for path in pathlib.Path( | |
| self._abs_package_path).rglob(f'*{UncrustifyCheck.FORMATTED_FILE_EXTENSION}')] | |
| self._formatted_file_error_count = len(formatted_files) | |
| if self._formatted_file_error_count > 0: | |
| logging.error( | |
| "Visit the following instructions to learn " | |
| "how to find the detailed formatting errors in Azure " | |
| "DevOps CI: " | |
| "https://github.com/tianocore/tianocore.github.io/wiki/EDK-II-Code-Formatting#how-to-find-uncrustify-formatting-errors-in-continuous-integration-ci") | |
| self._tc.LogStdError("Files with formatting errors:\n") | |
| if self._output_file_diffs: | |
| logging.info("Calculating file diffs. This might take a while...") | |
| for formatted_file in formatted_files: | |
| pre_formatted_file = formatted_file[:- | |
| len(UncrustifyCheck.FORMATTED_FILE_EXTENSION)] | |
| logging.error(pre_formatted_file) | |
| if (self._output_file_diffs or | |
| self._file_template_contents is not None or | |
| self._func_template_contents is not None): | |
| self._tc.LogStdError( | |
| f"Formatting errors in {os.path.relpath(pre_formatted_file, self._abs_package_path)}\n") | |
| with open(formatted_file) as ff: | |
| formatted_file_text = ff.read() | |
| if (self._file_template_contents is not None and | |
| self._file_template_contents in formatted_file_text): | |
| self._tc.LogStdError(f"File header is missing in {os.path.relpath(pre_formatted_file, self._abs_package_path)}\n") | |
| if (self._func_template_contents is not None and | |
| self._func_template_contents in formatted_file_text): | |
| self._tc.LogStdError(f"A function header is missing in {os.path.relpath(pre_formatted_file, self._abs_package_path)}\n") | |
| if self._output_file_diffs: | |
| with open(pre_formatted_file) as pf: | |
| pre_formatted_file_text = pf.read() | |
| for line in difflib.unified_diff(pre_formatted_file_text.split('\n'), formatted_file_text.split('\n'), fromfile=pre_formatted_file, tofile=formatted_file, n=3): | |
| self._tc.LogStdError(line) | |
| self._tc.LogStdError('\n') | |
| else: | |
| self._tc.LogStdError(pre_formatted_file) | |
| def _remove_tree(self, dir_path: str, ignore_errors: bool = False) -> None: | |
| """ | |
| Helper for removing a directory. Over time there have been | |
| many private implementations of this due to reliability issues in the | |
| shutil implementations. To consolidate on a single function this helper is added. | |
| On error try to change file attributes. Also add retry logic. | |
| This function is temporarily borrowed from edk2toollib.utility_functions | |
| since the version used in edk2 is not recent enough to include the | |
| function. | |
| This function should be replaced by "RemoveTree" when it is available. | |
| Args: | |
| - dir_path: Path to directory to remove. | |
| - ignore_errors: Whether to ignore errors during removal | |
| """ | |
| def _remove_readonly(func, path, _): | |
| """ | |
| Private function to attempt to change permissions on file/folder being deleted. | |
| """ | |
| os.chmod(path, os.stat.S_IWRITE) | |
| func(path) | |
| for _ in range(3): # retry up to 3 times | |
| try: | |
| shutil.rmtree(dir_path, ignore_errors=ignore_errors, onerror=_remove_readonly) | |
| except OSError as err: | |
| logging.warning(f"Failed to fully remove {dir_path}: {err}") | |
| else: | |
| break | |
| else: | |
| raise RuntimeError(f"Failed to remove {dir_path}") | |
| def _run_uncrustify(self) -> None: | |
| """ | |
| Runs Uncrustify for this instance of plugin execution. | |
| """ | |
| logging.info("Executing Uncrustify. This might take a while...") | |
| start_time = timeit.default_timer() | |
| self._execute_uncrustify() | |
| end_time = timeit.default_timer() - start_time | |
| execution_summary = f"Uncrustify executed against {len(self._abs_file_paths_to_format)} files in {self._package_name} in {end_time:.2f} seconds.\n" | |
| self._tc.LogStdOut(execution_summary) | |
| logging.info(execution_summary) | |
| if self._app_exit_code != 0 and self._app_exit_code != 1: | |
| raise UncrustifyAppExecutionException( | |
| f"Error {str(self._app_exit_code)} returned from Uncrustify:\n\n{str(self._app_output)}") |