]> git.proxmox.com Git - mirror_edk2.git/blob - .pytool/Plugin/UncrustifyCheck/UncrustifyCheck.py
.pytool: UncrustifyCheck: Set IgnoreFiles path relative to package path
[mirror_edk2.git] / .pytool / Plugin / UncrustifyCheck / UncrustifyCheck.py
1 # @file UncrustifyCheck.py
2 #
3 # An edk2-pytool based plugin wrapper for Uncrustify
4 #
5 # Copyright (c) Microsoft Corporation.
6 # SPDX-License-Identifier: BSD-2-Clause-Patent
7 ##
8 import configparser
9 import difflib
10 import errno
11 import logging
12 import os
13 import pathlib
14 import shutil
15 import timeit
16 from edk2toolext.environment import version_aggregator
17 from edk2toolext.environment.plugin_manager import PluginManager
18 from edk2toolext.environment.plugintypes.ci_build_plugin import ICiBuildPlugin
19 from edk2toolext.environment.plugintypes.uefi_helper_plugin import HelperFunctions
20 from edk2toolext.environment.var_dict import VarDict
21 from edk2toollib.gitignore_parser import parse_gitignore_lines
22 from edk2toollib.log.junit_report_format import JunitReportTestCase
23 from edk2toollib.uefi.edk2.path_utilities import Edk2Path
24 from edk2toollib.utility_functions import RunCmd
25 from io import StringIO
26 from typing import Any, Dict, List, Tuple
27
28 #
29 # Provide more user friendly messages for certain scenarios
30 #
31 class UncrustifyException(Exception):
32 def __init__(self, message, exit_code):
33 super().__init__(message)
34 self.exit_code = exit_code
35
36
37 class UncrustifyAppEnvVarNotFoundException(UncrustifyException):
38 def __init__(self, message):
39 super().__init__(message, -101)
40
41
42 class UncrustifyAppVersionErrorException(UncrustifyException):
43 def __init__(self, message):
44 super().__init__(message, -102)
45
46
47 class UncrustifyAppExecutionException(UncrustifyException):
48 def __init__(self, message):
49 super().__init__(message, -103)
50
51
52 class UncrustifyStalePluginFormattedFilesException(UncrustifyException):
53 def __init__(self, message):
54 super().__init__(message, -120)
55
56
57 class UncrustifyInputFileCreationErrorException(UncrustifyException):
58 def __init__(self, message):
59 super().__init__(message, -121)
60
61 class UncrustifyInvalidIgnoreStandardPathsException(UncrustifyException):
62 def __init__(self, message):
63 super().__init__(message, -122)
64
65 class UncrustifyGitIgnoreFileException(UncrustifyException):
66 def __init__(self, message):
67 super().__init__(message, -140)
68
69
70 class UncrustifyGitSubmoduleException(UncrustifyException):
71 def __init__(self, message):
72 super().__init__(message, -141)
73
74
75 class UncrustifyCheck(ICiBuildPlugin):
76 """
77 A CiBuildPlugin that uses Uncrustify to check the source files in the
78 package being tested for coding standard issues.
79
80 By default, the plugin runs against standard C source file extensions but
81 its configuration can be modified through its configuration file.
82
83 Configuration options:
84 "UncrustifyCheck": {
85 "AdditionalIncludePaths": [], # Additional paths to check formatting (wildcards supported).
86 "AuditOnly": False, # Don't fail the build if there are errors. Just log them.
87 "ConfigFilePath": "", # Custom path to an Uncrustify config file.
88 "IgnoreStandardPaths": [], # Standard Plugin defined paths that should be ignored.
89 "OutputFileDiffs": False, # Output chunks of formatting diffs in the test case log.
90 # This can significantly slow down the plugin on very large packages.
91 "SkipGitExclusions": False # Don't exclude git ignored files and files in git submodules.
92 }
93 """
94
95 #
96 # By default, use an "uncrustify.cfg" config file in the plugin directory
97 # A package can override this path via "ConfigFilePath"
98 #
99 # Note: Values specified via "ConfigFilePath" are relative to the package
100 #
101 DEFAULT_CONFIG_FILE_PATH = os.path.join(
102 pathlib.Path(__file__).parent.resolve(), "uncrustify.cfg")
103
104 #
105 # The extension used for formatted files produced by this plugin
106 #
107 FORMATTED_FILE_EXTENSION = ".uncrustify_plugin"
108
109 #
110 # A package can add any additional paths with "AdditionalIncludePaths"
111 # A package can remove any of these paths with "IgnoreStandardPaths"
112 #
113 STANDARD_PLUGIN_DEFINED_PATHS = ("*.c", "*.h")
114
115 #
116 # The Uncrustify application path should set in this environment variable
117 #
118 UNCRUSTIFY_PATH_ENV_KEY = "UNCRUSTIFY_CI_PATH"
119
120 def GetTestName(self, packagename: str, environment: VarDict) -> Tuple:
121 """ Provide the testcase name and classname for use in reporting
122
123 Args:
124 packagename: string containing name of package to build
125 environment: The VarDict for the test to run in
126 Returns:
127 A tuple containing the testcase name and the classname
128 (testcasename, classname)
129 testclassname: a descriptive string for the testcase can include whitespace
130 classname: should be patterned <packagename>.<plugin>.<optionally any unique condition>
131 """
132 return ("Check file coding standard compliance in " + packagename, packagename + ".UncrustifyCheck")
133
134 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:
135 """
136 External function of plugin. This function is used to perform the task of the CiBuild Plugin.
137
138 Args:
139 - package_rel_path: edk2 workspace relative path to the package
140 - edk2_path: Edk2Path object with workspace and packages paths
141 - package_config: Dictionary with the package configuration
142 - environment_config: Environment configuration
143 - plugin_manager: Plugin Manager Instance
144 - plugin_manager_helper: Plugin Manager Helper Instance
145 - tc: JUnit test case
146 - output_stream: The StringIO output stream from this plugin (logging)
147
148 Returns
149 >0 : Number of errors found
150 0 : Passed successfully
151 -1 : Skipped for missing prereq
152 """
153 try:
154 # Initialize plugin and check pre-requisites.
155 self._initialize_environment_info(
156 package_rel_path, edk2_path, package_config, tc)
157 self._initialize_configuration()
158 self._check_for_preexisting_formatted_files()
159
160 # Log important context information.
161 self._log_uncrustify_app_info()
162
163 # Get template file contents if specified
164 self._get_template_file_contents()
165
166 # Create meta input files & directories
167 self._create_temp_working_directory()
168 self._create_uncrustify_file_list_file()
169
170 self._run_uncrustify()
171
172 # Post-execution actions.
173 self._process_uncrustify_results()
174
175 except UncrustifyException as e:
176 self._tc.LogStdError(
177 f"Uncrustify error {e.exit_code}. Details:\n\n{str(e)}")
178 logging.warning(
179 f"Uncrustify error {e.exit_code}. Details:\n\n{str(e)}")
180 return -1
181 else:
182 if self._formatted_file_error_count > 0:
183 if self._audit_only_mode:
184 logging.info(
185 "Setting test as skipped since AuditOnly is enabled")
186 self._tc.SetSkipped()
187 return -1
188 else:
189 self._tc.SetFailed(
190 f"{self._plugin_name} failed due to {self._formatted_file_error_count} incorrectly formatted files.", "CHECK_FAILED")
191 else:
192 self._tc.SetSuccess()
193 return self._formatted_file_error_count
194 finally:
195 self._cleanup_temporary_formatted_files()
196 self._cleanup_temporary_directory()
197
198 def _initialize_configuration(self) -> None:
199 """
200 Initializes plugin configuration.
201 """
202 self._initialize_app_info()
203 self._initialize_config_file_info()
204 self._initialize_file_to_format_info()
205 self._initialize_test_case_output_options()
206
207 def _check_for_preexisting_formatted_files(self) -> None:
208 """
209 Checks if any formatted files from prior execution are present.
210
211 Existence of such files is an unexpected condition. This might result
212 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.
213 """
214 pre_existing_formatted_file_count = len(
215 [str(path.resolve()) for path in pathlib.Path(self._abs_package_path).rglob(f'*{UncrustifyCheck.FORMATTED_FILE_EXTENSION}')])
216
217 if pre_existing_formatted_file_count > 0:
218 raise UncrustifyStalePluginFormattedFilesException(
219 f"{pre_existing_formatted_file_count} formatted files already exist. To prevent overwriting these files, please remove them before running this plugin.")
220
221 def _cleanup_temporary_directory(self) -> None:
222 """
223 Cleans up the temporary directory used for this execution instance.
224
225 This removes the directory and all files created during this instance.
226 """
227 if hasattr(self, '_working_dir'):
228 self._remove_tree(self._working_dir)
229
230 def _cleanup_temporary_formatted_files(self) -> None:
231 """
232 Cleans up the temporary formmatted files produced by Uncrustify.
233
234 This will recursively remove all formatted files generated by Uncrustify
235 during this execution instance.
236 """
237 if hasattr(self, '_abs_package_path'):
238 formatted_files = [str(path.resolve()) for path in pathlib.Path(
239 self._abs_package_path).rglob(f'*{UncrustifyCheck.FORMATTED_FILE_EXTENSION}')]
240
241 for formatted_file in formatted_files:
242 os.remove(formatted_file)
243
244 def _create_temp_working_directory(self) -> None:
245 """
246 Creates the temporary directory used for this execution instance.
247 """
248 self._working_dir = os.path.join(
249 self._abs_workspace_path, "Build", ".pytool", "Plugin", f"{self._plugin_name}")
250
251 try:
252 pathlib.Path(self._working_dir).mkdir(parents=True, exist_ok=True)
253 except OSError as e:
254 raise UncrustifyInputFileCreationErrorException(
255 f"Error creating plugin directory {self._working_dir}.\n\n{repr(e)}.")
256
257 def _create_uncrustify_file_list_file(self) -> None:
258 """
259 Creates the file with the list of source files for Uncrustify to process.
260 """
261 self._app_input_file_path = os.path.join(
262 self._working_dir, "uncrustify_file_list.txt")
263
264 with open(self._app_input_file_path, 'w', encoding='utf8') as f:
265 f.writelines(f"\n".join(self._abs_file_paths_to_format))
266
267 def _execute_uncrustify(self) -> None:
268 """
269 Executes Uncrustify with the initialized configuration.
270 """
271 output = StringIO()
272 self._app_exit_code = RunCmd(
273 self._app_path,
274 f"-c {self._app_config_file} -F {self._app_input_file_path} --if-changed --suffix {UncrustifyCheck.FORMATTED_FILE_EXTENSION}", outstream=output)
275 self._app_output = output.getvalue().strip().splitlines()
276
277 def _get_files_ignored_in_config(self):
278 """"
279 Returns a function that returns true if a given file string path is ignored in the plugin configuration file and false otherwise.
280 """
281 ignored_files = []
282 if "IgnoreFiles" in self._package_config:
283 ignored_files = self._package_config["IgnoreFiles"]
284
285 # Pass "Package configuration file" as the source file path since
286 # the actual configuration file name is unknown to this plugin and
287 # this provides a generic description of the file that provided
288 # the ignore file content.
289 #
290 # This information is only used for reporting (not used here) and
291 # the ignore lines are being passed directly as they are given to
292 # this plugin.
293 return parse_gitignore_lines(ignored_files, "Package configuration file", self._abs_package_path)
294
295 def _get_git_ignored_paths(self) -> List[str]:
296 """"
297 Returns a list of file absolute path strings to all files ignored in this git repository.
298
299 If git is not found, an empty list will be returned.
300 """
301 if not shutil.which("git"):
302 logging.warn(
303 "Git is not found on this system. Git submodule paths will not be considered.")
304 return []
305
306 outstream_buffer = StringIO()
307 exit_code = RunCmd("git", "ls-files --other",
308 workingdir=self._abs_workspace_path, outstream=outstream_buffer, logging_level=logging.NOTSET)
309 if (exit_code != 0):
310 raise UncrustifyGitIgnoreFileException(
311 f"An error occurred reading git ignore settings. This will prevent Uncrustify from running against the expected set of files.")
312
313 # Note: This will potentially be a large list, but at least sorted
314 rel_paths = outstream_buffer.getvalue().strip().splitlines()
315 abs_paths = []
316 for path in rel_paths:
317 abs_paths.append(
318 os.path.normpath(os.path.join(self._abs_workspace_path, path)))
319 return abs_paths
320
321 def _get_git_submodule_paths(self) -> List[str]:
322 """
323 Returns a list of directory absolute path strings to the root of each submodule in the workspace repository.
324
325 If git is not found, an empty list will be returned.
326 """
327 if not shutil.which("git"):
328 logging.warn(
329 "Git is not found on this system. Git submodule paths will not be considered.")
330 return []
331
332 if os.path.isfile(os.path.join(self._abs_workspace_path, ".gitmodules")):
333 logging.info(
334 f".gitmodules file found. Excluding submodules in {self._package_name}.")
335
336 outstream_buffer = StringIO()
337 exit_code = RunCmd("git", "config --file .gitmodules --get-regexp path", workingdir=self._abs_workspace_path, outstream=outstream_buffer, logging_level=logging.NOTSET)
338 if (exit_code != 0):
339 raise UncrustifyGitSubmoduleException(
340 f".gitmodule file detected but an error occurred reading the file. Cannot proceed with unknown submodule paths.")
341
342 submodule_paths = []
343 for line in outstream_buffer.getvalue().strip().splitlines():
344 submodule_paths.append(
345 os.path.normpath(os.path.join(self._abs_workspace_path, line.split()[1])))
346
347 return submodule_paths
348 else:
349 return []
350
351 def _get_template_file_contents(self) -> None:
352 """
353 Gets the contents of Uncrustify template files if they are specified
354 in the Uncrustify configuration file.
355 """
356
357 self._file_template_contents = None
358 self._func_template_contents = None
359
360 # Allow no value to allow "set" statements in the config file which do
361 # not specify value assignment
362 parser = configparser.ConfigParser(allow_no_value=True)
363 with open(self._app_config_file, 'r') as cf:
364 parser.read_string("[dummy_section]\n" + cf.read())
365
366 try:
367 file_template_name = parser["dummy_section"]["cmt_insert_file_header"]
368
369 file_template_path = pathlib.Path(file_template_name)
370
371 if not file_template_path.is_file():
372 file_template_path = pathlib.Path(os.path.join(self._plugin_path, file_template_name))
373 self._file_template_contents = file_template_path.read_text()
374 except KeyError:
375 logging.warn("A file header template is not specified in the config file.")
376 except FileNotFoundError:
377 logging.warn("The specified file header template file was not found.")
378 try:
379 func_template_name = parser["dummy_section"]["cmt_insert_func_header"]
380
381 func_template_path = pathlib.Path(func_template_name)
382
383 if not func_template_path.is_file():
384 func_template_path = pathlib.Path(os.path.join(self._plugin_path, func_template_name))
385 self._func_template_contents = func_template_path.read_text()
386 except KeyError:
387 logging.warn("A function header template is not specified in the config file.")
388 except FileNotFoundError:
389 logging.warn("The specified function header template file was not found.")
390
391 def _initialize_app_info(self) -> None:
392 """
393 Initialize Uncrustify application information.
394
395 This function will determine the application path and version.
396 """
397 # Verify Uncrustify is specified in the environment.
398 if UncrustifyCheck.UNCRUSTIFY_PATH_ENV_KEY not in os.environ:
399 raise UncrustifyAppEnvVarNotFoundException(
400 f"Uncrustify environment variable {UncrustifyCheck.UNCRUSTIFY_PATH_ENV_KEY} is not present.")
401
402 self._app_path = shutil.which('uncrustify', path=os.environ[UncrustifyCheck.UNCRUSTIFY_PATH_ENV_KEY])
403
404 if self._app_path is None:
405 raise FileNotFoundError(
406 errno.ENOENT, os.strerror(errno.ENOENT), self._app_path)
407
408 self._app_path = os.path.normcase(os.path.normpath(self._app_path))
409
410 if not os.path.isfile(self._app_path):
411 raise FileNotFoundError(
412 errno.ENOENT, os.strerror(errno.ENOENT), self._app_path)
413
414 # Verify Uncrustify is present at the expected path.
415 return_buffer = StringIO()
416 ret = RunCmd(self._app_path, "--version", outstream=return_buffer)
417 if (ret != 0):
418 raise UncrustifyAppVersionErrorException(
419 f"Error occurred executing --version: {ret}.")
420
421 # Log Uncrustify version information.
422 self._app_version = return_buffer.getvalue().strip()
423 self._tc.LogStdOut(f"Uncrustify version: {self._app_version}")
424 version_aggregator.GetVersionAggregator().ReportVersion(
425 "Uncrustify", self._app_version, version_aggregator.VersionTypes.INFO)
426
427 def _initialize_config_file_info(self) -> None:
428 """
429 Initialize Uncrustify configuration file info.
430
431 The config file path is relative to the package root.
432 """
433 self._app_config_file = UncrustifyCheck.DEFAULT_CONFIG_FILE_PATH
434 if "ConfigFilePath" in self._package_config:
435 self._app_config_file = self._package_config["ConfigFilePath"].strip()
436
437 self._app_config_file = os.path.normpath(
438 os.path.join(self._abs_package_path, self._app_config_file))
439
440 if not os.path.isfile(self._app_config_file):
441 raise FileNotFoundError(
442 errno.ENOENT, os.strerror(errno.ENOENT), self._app_config_file)
443
444 def _initialize_environment_info(self, package_rel_path: str, edk2_path: Edk2Path, package_config: Dict[str, List[str]], tc: JunitReportTestCase) -> None:
445 """
446 Initializes plugin environment information.
447 """
448 self._abs_package_path = edk2_path.GetAbsolutePathOnThisSystemFromEdk2RelativePath(
449 package_rel_path)
450 self._abs_workspace_path = edk2_path.WorkspacePath
451 self._package_config = package_config
452 self._package_name = os.path.basename(
453 os.path.normpath(package_rel_path))
454 self._plugin_name = self.__class__.__name__
455 self._plugin_path = os.path.dirname(os.path.realpath(__file__))
456 self._rel_package_path = package_rel_path
457 self._tc = tc
458
459 def _initialize_file_to_format_info(self) -> None:
460 """
461 Forms the list of source files for Uncrustify to process.
462 """
463 # Create a list of all the package relative file paths in the package to run against Uncrustify.
464 rel_file_paths_to_format = list(
465 UncrustifyCheck.STANDARD_PLUGIN_DEFINED_PATHS)
466
467 # Allow the ci.yaml to remove any of the pre-defined standard paths
468 if "IgnoreStandardPaths" in self._package_config:
469 for a in self._package_config["IgnoreStandardPaths"]:
470 if a.strip() in rel_file_paths_to_format:
471 self._tc.LogStdOut(
472 f"Ignoring standard path due to ci.yaml ignore: {a}")
473 rel_file_paths_to_format.remove(a.strip())
474 else:
475 raise UncrustifyInvalidIgnoreStandardPathsException(f"Invalid IgnoreStandardPaths value: {a}")
476
477 # Allow the ci.yaml to specify additional include paths for this package
478 if "AdditionalIncludePaths" in self._package_config:
479 rel_file_paths_to_format.extend(
480 self._package_config["AdditionalIncludePaths"])
481
482 self._abs_file_paths_to_format = []
483 for path in rel_file_paths_to_format:
484 self._abs_file_paths_to_format.extend(
485 [str(path.resolve()) for path in pathlib.Path(self._abs_package_path).rglob(path)])
486
487 # Remove files ignore in the plugin configuration file
488 plugin_ignored_files = list(filter(self._get_files_ignored_in_config(), self._abs_file_paths_to_format))
489
490 if plugin_ignored_files:
491 logging.info(
492 f"{self._package_name} file count before plugin ignore file exclusion: {len(self._abs_file_paths_to_format)}")
493 for path in plugin_ignored_files:
494 if path in self._abs_file_paths_to_format:
495 logging.info(f" File ignored in plugin config file: {path}")
496 self._abs_file_paths_to_format.remove(path)
497 logging.info(
498 f"{self._package_name} file count after plugin ignore file exclusion: {len(self._abs_file_paths_to_format)}")
499
500 if not "SkipGitExclusions" in self._package_config or not self._package_config["SkipGitExclusions"]:
501 # Remove files ignored by git
502 logging.info(
503 f"{self._package_name} file count before git ignore file exclusion: {len(self._abs_file_paths_to_format)}")
504
505 ignored_paths = self._get_git_ignored_paths()
506 self._abs_file_paths_to_format = list(
507 set(self._abs_file_paths_to_format).difference(ignored_paths))
508
509 logging.info(
510 f"{self._package_name} file count after git ignore file exclusion: {len(self._abs_file_paths_to_format)}")
511
512 # Remove files in submodules
513 logging.info(
514 f"{self._package_name} file count before submodule exclusion: {len(self._abs_file_paths_to_format)}")
515
516 submodule_paths = tuple(self._get_git_submodule_paths())
517 for path in submodule_paths:
518 logging.info(f" submodule path: {path}")
519
520 self._abs_file_paths_to_format = [
521 f for f in self._abs_file_paths_to_format if not f.startswith(submodule_paths)]
522
523 logging.info(
524 f"{self._package_name} file count after submodule exclusion: {len(self._abs_file_paths_to_format)}")
525
526 # Sort the files for more consistent results
527 self._abs_file_paths_to_format.sort()
528
529 def _initialize_test_case_output_options(self) -> None:
530 """
531 Initializes options that influence test case output.
532 """
533 self._audit_only_mode = False
534 self._output_file_diffs = True
535
536 if "AuditOnly" in self._package_config and self._package_config["AuditOnly"]:
537 self._audit_only_mode = True
538
539 if "OutputFileDiffs" in self._package_config and not self._package_config["OutputFileDiffs"]:
540 self._output_file_diffs = False
541
542 def _log_uncrustify_app_info(self) -> None:
543 """
544 Logs Uncrustify application information.
545 """
546 self._tc.LogStdOut(f"Found Uncrustify at {self._app_path}")
547 self._tc.LogStdOut(f"Uncrustify version: {self._app_version}")
548 self._tc.LogStdOut('\n')
549 logging.info(f"Found Uncrustify at {self._app_path}")
550 logging.info(f"Uncrustify version: {self._app_version}")
551 logging.info('\n')
552
553 def _process_uncrustify_results(self) -> None:
554 """
555 Process the results from Uncrustify.
556
557 Determines whether formatting errors are present and logs failures.
558 """
559 formatted_files = [str(path.resolve()) for path in pathlib.Path(
560 self._abs_package_path).rglob(f'*{UncrustifyCheck.FORMATTED_FILE_EXTENSION}')]
561
562 self._formatted_file_error_count = len(formatted_files)
563
564 if self._formatted_file_error_count > 0:
565 logging.error(
566 "Visit the following instructions to learn "
567 "how to find the detailed formatting errors in Azure "
568 "DevOps CI: "
569 "https://github.com/tianocore/tianocore.github.io/wiki/EDK-II-Code-Formatting#how-to-find-uncrustify-formatting-errors-in-continuous-integration-ci")
570 self._tc.LogStdError("Files with formatting errors:\n")
571
572 if self._output_file_diffs:
573 logging.info("Calculating file diffs. This might take a while...")
574
575 for formatted_file in formatted_files:
576 pre_formatted_file = formatted_file[:-
577 len(UncrustifyCheck.FORMATTED_FILE_EXTENSION)]
578 logging.error(pre_formatted_file)
579
580 if (self._output_file_diffs or
581 self._file_template_contents is not None or
582 self._func_template_contents is not None):
583 self._tc.LogStdError(
584 f"Formatting errors in {os.path.relpath(pre_formatted_file, self._abs_package_path)}\n")
585
586 with open(formatted_file) as ff:
587 formatted_file_text = ff.read()
588
589 if (self._file_template_contents is not None and
590 self._file_template_contents in formatted_file_text):
591 self._tc.LogStdError(f"File header is missing in {os.path.relpath(pre_formatted_file, self._abs_package_path)}\n")
592
593 if (self._func_template_contents is not None and
594 self._func_template_contents in formatted_file_text):
595 self._tc.LogStdError(f"A function header is missing in {os.path.relpath(pre_formatted_file, self._abs_package_path)}\n")
596
597 if self._output_file_diffs:
598 with open(pre_formatted_file) as pf:
599 pre_formatted_file_text = pf.read()
600
601 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):
602 self._tc.LogStdError(line)
603
604 self._tc.LogStdError('\n')
605 else:
606 self._tc.LogStdError(pre_formatted_file)
607
608 def _remove_tree(self, dir_path: str, ignore_errors: bool = False) -> None:
609 """
610 Helper for removing a directory. Over time there have been
611 many private implementations of this due to reliability issues in the
612 shutil implementations. To consolidate on a single function this helper is added.
613
614 On error try to change file attributes. Also add retry logic.
615
616 This function is temporarily borrowed from edk2toollib.utility_functions
617 since the version used in edk2 is not recent enough to include the
618 function.
619
620 This function should be replaced by "RemoveTree" when it is available.
621
622 Args:
623 - dir_path: Path to directory to remove.
624 - ignore_errors: Whether to ignore errors during removal
625 """
626
627 def _remove_readonly(func, path, _):
628 """
629 Private function to attempt to change permissions on file/folder being deleted.
630 """
631 os.chmod(path, os.stat.S_IWRITE)
632 func(path)
633
634 for _ in range(3): # retry up to 3 times
635 try:
636 shutil.rmtree(dir_path, ignore_errors=ignore_errors, onerror=_remove_readonly)
637 except OSError as err:
638 logging.warning(f"Failed to fully remove {dir_path}: {err}")
639 else:
640 break
641 else:
642 raise RuntimeError(f"Failed to remove {dir_path}")
643
644 def _run_uncrustify(self) -> None:
645 """
646 Runs Uncrustify for this instance of plugin execution.
647 """
648 logging.info("Executing Uncrustify. This might take a while...")
649 start_time = timeit.default_timer()
650 self._execute_uncrustify()
651 end_time = timeit.default_timer() - start_time
652
653 execution_summary = f"Uncrustify executed against {len(self._abs_file_paths_to_format)} files in {self._package_name} in {end_time:.2f} seconds.\n"
654
655 self._tc.LogStdOut(execution_summary)
656 logging.info(execution_summary)
657
658 if self._app_exit_code != 0 and self._app_exit_code != 1:
659 raise UncrustifyAppExecutionException(
660 f"Error {str(self._app_exit_code)} returned from Uncrustify:\n\n{str(self._app_output)}")