Skip to content

Commit

Permalink
.pytool/LineEndingCheck.py: Add initial plugin
Browse files Browse the repository at this point in the history
Adds a simple CI plugin to check file line endings. This is meant to:

1. Supplement the Uncrustify Check plugin which fails if line endings
   are CRLF but does not obviously indicate that error since it is
   non-visible whitespace.

2. Discover line endings issues in files not checked by Uncrustify
   (which only operates against .c and .h files right now).

Signed-off-by: Michael Kubacki <michael.kubacki@microsoft.com>
  • Loading branch information
makubacki authored and kenlautner committed May 9, 2023
1 parent 811a5b2 commit 2fe0f02
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 0 deletions.
190 changes: 190 additions & 0 deletions .pytool/Plugin/LineEndingCheck/LineEndingCheck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# @file LineEndingCheck.py
#
# An edk2-pytool based plugin that checks line endings.
#
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: BSD-2-Clause-Patent
##

import glob
import logging
import os
from pathlib import Path
from typing import Any, Callable, Dict, List, Tuple

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

PLUGIN_NAME = "LineEndingCheck"

LINE_ENDINGS = [
b'\r\n',
b'\n\r',
b'\n',
b'\r'
]

ALLOWED_LINE_ENDING = b'\r\n'


class LineEndingCheckBadLineEnding(Exception):
pass


class LineEndingCheck(ICiBuildPlugin):
"""
A CiBuildPlugin that checks whether line endings are a certain format.
By default, the plugin runs against all files in a package unless a
specific file or file extension is excluded.
Configuration options:
"LineEndingCheck": {
"IgnoreFiles": [], # File patterns to ignore.
}
"""

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 line endings in " + packagename, packagename +
"." + PLUGIN_NAME)

def _get_files_ignored_in_config(self,
pkg_config: Dict[str, List[str]],
base_dir: str) -> Callable[[str], bool]:
""""
Returns a function that returns true if a given file string path is
ignored in the plugin configuration file and false otherwise.
Args:
pkg_config: Dictionary with the package configuration
base_dir: Base directory of the package
Returns:
Callable[[None], None]: A test case function.
"""
ignored_files = []
if "IgnoreFiles" in pkg_config:
ignored_files = pkg_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",
base_dir)

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 : Ran successfully
-1 : Skipped due to a missing pre-requisite
"""

abs_pkg_path = \
edk2_path.GetAbsolutePathOnThisSystemFromEdk2RelativePath(
package_rel_path)

if abs_pkg_path is None:
tc.SetSkipped()
tc.LogStdError(f"Package folder not found {abs_pkg_path}")
return 0

all_files = [n for n in glob.glob(os.path.join(abs_pkg_path, '*.*'),
recursive=True)]

ignored_files = list(filter(
self._get_files_ignored_in_config(
package_config, abs_pkg_path), all_files))

for file in ignored_files:
if file in all_files:
logging.info(f" File ignored in plugin config file: "
f"{Path(file).name}")
all_files.remove(file)

file_count = 0
line_ending_count = dict.fromkeys(LINE_ENDINGS, 0)

for file in all_files:
with open(file, 'rb') as fb:
for lineno, line in enumerate(fb):
try:
for e in LINE_ENDINGS:
if line.endswith(e):
line_ending_count[e] += 1

if e is not ALLOWED_LINE_ENDING:
file_name = Path(file).name
file_count += 1

tc.LogStdError(
f"Line ending on Line {lineno} in "
f"{file_name} is not allowed.\nLine "
f"ending is {e} and should be "
f"{ALLOWED_LINE_ENDING}.")
logging.error(
f"Line ending on Line {lineno} in "
f"{file_name} is not allowed.\nLine "
f"ending is {e} and should be "
f"{ALLOWED_LINE_ENDING}.")
raise LineEndingCheckBadLineEnding
break
except LineEndingCheckBadLineEnding:
break

del line_ending_count[ALLOWED_LINE_ENDING]

if any(line_ending_count.values()):
tc.SetFailed(
f"{PLUGIN_NAME} failed due to {file_count} files with "
f"incorrect line endings.",
"CHECK_FAILED")
else:
tc.SetSuccess()

return sum(line_ending_count.values())
27 changes: 27 additions & 0 deletions .pytool/Plugin/LineEndingCheck/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Line Ending Check Plugin

This CiBuildPlugin scans all the files in a package to verify that the line endings are CRLF.

> _Note:_ If you encounter a line ending issue found by this plugin, update your development environment to avoid
> issues again in the future.
>
> Most problems are caused by `autocrlf=true` in git settings, which will automatically adjust line endings upon
> checkout and commit which distorts the actual line endings from being consistent locally and remotely. In
> other cases, developing within a Linux workspace will natively use LF by default.
>
> It is simplest to set `autocrlf=false` to prevent manipulation of line endings outside of the actual values and set
> up your editor to use CRLF line endings within the project.
## Configuration

The plugin can be configured to ignore certain files.

``` yaml
"LineEndingCheck": {
"IgnoreFiles": []
}
```

### IgnoreFiles

An **optional** list of git ignore patterns relative to the package root used to exclude files from being checked.
11 changes: 11 additions & 0 deletions .pytool/Plugin/LineEndingCheck/line_ending_check_plug_in.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## @file
# CiBuildPlugin used to check line ending format.
#
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: BSD-2-Clause-Patent
##
{
"scope": "cibuild",
"name": "Line Ending Check Test",
"module": "LineEndingCheck"
}

0 comments on commit 2fe0f02

Please sign in to comment.