This python script is more configurable than the shell script that it replaces, and makes it clearer when looking at the makefile what it's acting on. Signed-off-by: Michael Richters <gedankenexperimenter@gmail.com>pull/1157/head
parent
3c3ff1efc8
commit
da03cbeccb
@ -0,0 +1,271 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright (c) 2022 Michael Richters <gedankenexperimenter@gmail.com>
|
||||||
|
|
||||||
|
# This is free and unencumbered software released into the public domain.
|
||||||
|
|
||||||
|
# Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||||
|
# distribute this software, either in source code form or as a compiled
|
||||||
|
# binary, for any purpose, commercial or non-commercial, and by any
|
||||||
|
# means.
|
||||||
|
|
||||||
|
# In jurisdictions that recognize copyright laws, the author or authors
|
||||||
|
# of this software dedicate any and all copyright interest in the
|
||||||
|
# software to the public domain. We make this dedication for the benefit
|
||||||
|
# of the public at large and to the detriment of our heirs and
|
||||||
|
# successors. We intend this dedication to be an overt act of
|
||||||
|
# relinquishment in perpetuity of all present and future rights to this
|
||||||
|
# software under copyright law.
|
||||||
|
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
# OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
# For more information, please refer to <http://unlicense.org/>
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
"""This script runs clang-format on Kaleidoscope's codebase."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import glob
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
def parse_args(args):
|
||||||
|
"""Parse command line parameters
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args (List[str]): command line parameters as list of strings
|
||||||
|
(for example ``["--help"]``).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:obj:`argparse.Namespace`: command line parameters namespace
|
||||||
|
"""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="""
|
||||||
|
Recursively search specified directories and format source files with
|
||||||
|
clang-format. By default, it operates on Arduino C++ source files with
|
||||||
|
extensions: *.{cpp,h,hpp,inc,ino}.""")
|
||||||
|
parser.add_argument(
|
||||||
|
'-v',
|
||||||
|
'--verbose',
|
||||||
|
dest='loglevel',
|
||||||
|
help="Verbose output",
|
||||||
|
action='store_const',
|
||||||
|
const=logging.INFO,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-q',
|
||||||
|
'--quiet',
|
||||||
|
dest='loglevel',
|
||||||
|
help="Suppress all non-error output",
|
||||||
|
action='store_const',
|
||||||
|
const=logging.ERROR,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-X',
|
||||||
|
'--exclude-dir',
|
||||||
|
metavar="<path>",
|
||||||
|
dest='exclude_dirs',
|
||||||
|
help="Exclude dir from search (path relative to the pwd)",
|
||||||
|
action='append',
|
||||||
|
default=[],
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-x',
|
||||||
|
'--exclude-file',
|
||||||
|
metavar="<file>",
|
||||||
|
dest='exclude_files',
|
||||||
|
help="Exclude <file> (base name only, not a full path) from formatting",
|
||||||
|
action='append',
|
||||||
|
default=[],
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-e',
|
||||||
|
'--regex',
|
||||||
|
metavar="<regex>",
|
||||||
|
dest='src_re_str',
|
||||||
|
help="Regular expression for matching source file names",
|
||||||
|
default=r'\.(cpp|h|hpp|inc|ino)$',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-f',
|
||||||
|
'--force',
|
||||||
|
action='store_true',
|
||||||
|
help="Format code even if there are unstaged changes",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--check',
|
||||||
|
action='store_true',
|
||||||
|
help="Check for changes after formatting",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'targets',
|
||||||
|
metavar="<search_dir>",
|
||||||
|
nargs='+',
|
||||||
|
help="""A list of files and/or directories to search for source files to format""",
|
||||||
|
)
|
||||||
|
return parser.parse_args(args)
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
def setup_logging(loglevel):
|
||||||
|
"""Setup basic logging
|
||||||
|
|
||||||
|
Args:
|
||||||
|
loglevel (int): minimum loglevel for emitting messages
|
||||||
|
"""
|
||||||
|
logformat = "%(message)s"
|
||||||
|
logging.basicConfig(
|
||||||
|
level=loglevel,
|
||||||
|
stream=sys.stdout,
|
||||||
|
format=logformat,
|
||||||
|
datefmt="",
|
||||||
|
)
|
||||||
|
return logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
def format_code(path, opts, clang_format_cmd):
|
||||||
|
"""Run clang-format on a directory."""
|
||||||
|
logging.info("Formatting code in %s...", path)
|
||||||
|
|
||||||
|
src_regex = re.compile(opts.src_re_str)
|
||||||
|
|
||||||
|
src_files = []
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(path):
|
||||||
|
for exclude_path in opts.exclude_dirs:
|
||||||
|
exclude_path = exclude_path.rstrip(os.path.sep)
|
||||||
|
if os.path.dirname(exclude_path) == root:
|
||||||
|
exclude_dir = os.path.basename(exclude_path)
|
||||||
|
if exclude_dir in dirs:
|
||||||
|
dirs.remove(exclude_dir)
|
||||||
|
|
||||||
|
for name in files:
|
||||||
|
if name in opts.exclude_files:
|
||||||
|
continue
|
||||||
|
if src_regex.search(name):
|
||||||
|
src_files.append(os.path.join(root, name))
|
||||||
|
|
||||||
|
proc = subprocess.run(clang_format_cmd + src_files)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
logging.error("Error: clang-format returned non-zero status: %s", proc.returncode)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
def build_file_list(path, src_regex):
|
||||||
|
"""Docstring"""
|
||||||
|
|
||||||
|
# If the specified path is a filename, return it (as a list), regardless of
|
||||||
|
# whether or not it matches the regex.
|
||||||
|
if os.path.isfile(path):
|
||||||
|
return [path]
|
||||||
|
|
||||||
|
# If the specified path is not valid, just return an empty list.
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
return []
|
||||||
|
|
||||||
|
# The specified path is a directory, so we search recursively for files
|
||||||
|
# contained therein that match the specified regular expression.
|
||||||
|
source_files = []
|
||||||
|
for root, dirs, files in os.walk(path):
|
||||||
|
# First, ignore all dotfiles (and directories).
|
||||||
|
dotfiles = set(glob.glob('.*'))
|
||||||
|
dirs = set(dirs) - dotfiles
|
||||||
|
files = set(dirs) - dotfiles
|
||||||
|
|
||||||
|
# Check for a list of file glob patterns that should be excluded.
|
||||||
|
if IWYU_IGNORE_FILE in files:
|
||||||
|
with open(os.path.join(root, IWYU_IGNORE_FILE)) as f:
|
||||||
|
for pattern in f.read().splitlines():
|
||||||
|
matches = set(glob.glob(os.path.join(root, pattern)))
|
||||||
|
dirs = set(dirs) - matches
|
||||||
|
files = set(files) - matches
|
||||||
|
|
||||||
|
# Add all matching files to the list of source files to be formatted.
|
||||||
|
for f in filter(src_regex.search, files):
|
||||||
|
source_files.append(os.path.join(root, f))
|
||||||
|
|
||||||
|
return source_files
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
def warn_if_output(byte_str, msg):
|
||||||
|
"""Convert a string of bytes to a UTF-8 string, break it on newlines. If
|
||||||
|
there is any output, print a warning message, followed by each line,
|
||||||
|
indented by four spaces."""
|
||||||
|
lines = byte_str.decode('utf-8').splitlines()
|
||||||
|
if '' in lines:
|
||||||
|
lines.remove('')
|
||||||
|
if len(lines) > 0:
|
||||||
|
logging.warning('%s', msg)
|
||||||
|
for line in lines:
|
||||||
|
logging.warning(' %s', line)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
def main(cli_args):
|
||||||
|
"""Parse command-line arguments and format source files."""
|
||||||
|
args = parse_args(cli_args)
|
||||||
|
if args.loglevel is None:
|
||||||
|
args.loglevel = logging.WARNING
|
||||||
|
setup_logging(args.loglevel)
|
||||||
|
|
||||||
|
clang_format = os.getenv('CLANG_FORMAT_CMD')
|
||||||
|
if clang_format is None:
|
||||||
|
clang_format = 'clang-format'
|
||||||
|
|
||||||
|
clang_format_cmd = [clang_format, '-i']
|
||||||
|
|
||||||
|
git_diff_cmd = ['git', 'diff', '--exit-code']
|
||||||
|
|
||||||
|
proc = subprocess.run(git_diff_cmd + ['--name-only'], capture_output=True)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
warn_if_output(proc.stdout, 'Warning: you have unstaged changes to these files:')
|
||||||
|
if not args.force:
|
||||||
|
logging.warning(
|
||||||
|
'Formatting aborted. Stage your changes or use --force to override.')
|
||||||
|
sys.exit(proc.returncode)
|
||||||
|
|
||||||
|
if args.loglevel >= logging.WARNING:
|
||||||
|
git_diff_cmd.append('--quiet')
|
||||||
|
elif args.loglevel <= logging.INFO:
|
||||||
|
git_diff_cmd.append('--name-only')
|
||||||
|
|
||||||
|
for path in args.targets:
|
||||||
|
format_code(path, args, clang_format_cmd)
|
||||||
|
|
||||||
|
if args.check:
|
||||||
|
logging.warning('Checking for changes made by the formatter...')
|
||||||
|
|
||||||
|
proc = subprocess.run(git_diff_cmd + ['--cached'], capture_output=True)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
logging.warning(
|
||||||
|
'Warning: Your working tree has staged changes. ' +
|
||||||
|
'Committed changes might not pass this check.')
|
||||||
|
warn_if_output(proc.stdout, 'The following files have unstaged changes:')
|
||||||
|
|
||||||
|
proc = subprocess.run(git_diff_cmd, capture_output=True)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
warn_if_output(proc.stdout, 'The following files have changes:')
|
||||||
|
logging.error(
|
||||||
|
'Check failed: Please commit formatting changes before submitting.')
|
||||||
|
sys.exit(proc.returncode)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main(sys.argv[1:])
|
@ -1,25 +1,12 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Allow the caller to specify a particular version of clang-format to use:
|
: "${KALEIDOSCOPE_DIR:=$(pwd)}"
|
||||||
: "${CLANG_FORMAT_CMD:=clang-format}"
|
cd "${KALEIDOSCOPE_DIR}" || exit 1
|
||||||
|
|
||||||
# Find all *.cpp and *.h files, except those in `testing/googletest/` and files
|
: "${VERBOSE:=}"
|
||||||
# generated by testcase scripts, and run `clang-format` on them:
|
|
||||||
find ./* -type f \( -name '*.h' -o -name '*.cpp' \) \
|
|
||||||
-not \( -path './testing/googletest/*' -o -name 'generated-testcase.cpp' \) \
|
|
||||||
-print0 | \
|
|
||||||
xargs -0 "${CLANG_FORMAT_CMD}" -i
|
|
||||||
|
|
||||||
# If we get the `--check` option, return an error if there are any changes to
|
"${KALEIDOSCOPE_DIR}/bin/format-code.py" \
|
||||||
# the git working tree after running `clang-format`:
|
--exclude-dir 'testing/googletest' \
|
||||||
if [[ $1 == '--check' ]]; then
|
--exclude-file 'generated-testcase.cpp' \
|
||||||
|
--verbose \
|
||||||
if ! git diff --quiet; then
|
src plugins testing
|
||||||
cat >&2 <<EOF
|
|
||||||
Differences found between git head and working tree. Either 'clang-format' made
|
|
||||||
formatting changes to your code, or you had uncommitted changes in your working
|
|
||||||
tree. Please remember to run 'bin/format-code.sh' before committing changes.
|
|
||||||
EOF
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
Loading…
Reference in new issue