[ci] Add a script for checking whitespace endings of files

Signed-off-by: Miguel Young de la Sota <mcyoung@google.com>
diff --git a/util/fix_trailing_whitespace.py b/util/fix_trailing_whitespace.py
new file mode 100755
index 0000000..34f0112
--- /dev/null
+++ b/util/fix_trailing_whitespace.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+# fix_trailing_whitespace.py script ensures that all files passed into it satisfy
+# various requirements in terms of whitespace:
+# - There is no leading whitespace in the file.
+# - Lines do not have trailing non-newline whitespace and have UNIX-style line endings.
+# - The file ends in a single newline.
+
+import argparse
+import sys
+import re
+import subprocess
+
+from pathlib import Path
+
+# This file is $REPO_TOP/util/fix_trailing_newlines.py, so it takes two parent()
+# calls to get back to the top.
+REPO_TOP = Path(__file__).resolve().parent.parent
+
+def is_ignored(path):
+    return subprocess.run(['git', 'check-ignore', path]).returncode == 0
+
+def walk_tree(paths=[REPO_TOP]):
+    for path in paths:
+        if isinstance(path, str):
+            path = Path(path)
+
+        if path.is_symlink() or is_ignored(path) or 'LICENSE' in path.parts:
+            continue
+
+        if path.is_dir() and 'vendor' not in path.parts:
+            yield from walk_tree(path.iterdir())
+        else:
+            yield path
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        '--dry-run',
+        action='store_true',
+        help='report writes which would have happened')
+    parser.add_argument(
+        '--recursive', '-r',
+        action='store_true',
+        default=False,
+        help='traverse the entire tree modolo .gitignore'
+    )
+    parser.add_argument(
+        '--verbose', '-v',
+        action='store_true',
+        help='verbose output')
+    parser.add_argument(
+        'files',
+        type=str,
+        nargs='*',
+        help='files to fix whitespace for')
+    args = parser.parse_args()
+
+    files = args.files
+    if args.recursive:
+        files = walk_tree(args.files)
+
+    total_fixable = 0
+    for path in files:
+        path = Path(path).resolve().relative_to(REPO_TOP)
+        if not path.is_file() or path.is_symlink():
+            continue
+        if 'vendor' in path.parts or path.suffix in ['.patch', '.svg']:
+            continue
+        if args.verbose:
+            print(f'Checking: "{path}"')
+
+        try:
+            old_text = path.read_text()
+        except UnicodeDecodeError:
+            print(f'Binary file: "{path}"')
+            continue
+        new_text = "\n".join([line.rstrip() for line in old_text.strip().split("\n")]) + "\n"
+
+        if old_text != new_text:
+            print(f'Fixing file: "{path}"', file=sys.stdout)
+            total_fixable += 1
+            if not args.dry_run:
+                path.write_text(new_text)
+
+    if total_fixable:
+        verb = 'Would have fixed' if args.dry_run else 'Fixed'
+        print(f'{verb} {total_fixable} files.', file=sys.stderr)
+
+    # Pass if we fixed everything or there was nothing to fix.
+    return 1 if total_fixable > 0 else 0
+
+if __name__ == '__main__':
+    sys.exit(main())