From 3ea933e489d9214802fd4d9c01a3266c79a4152f Mon Sep 17 00:00:00 2001 From: Delsauce Date: Fri, 11 Nov 2022 09:29:46 -0800 Subject: [PATCH] Initial commit --- README.md | 25 ++++++++ hooks/__init__.py | 4 ++ hooks/format_c_source.py | 134 +++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + setup.py | 54 ++++++++++++++++ 5 files changed, 219 insertions(+) create mode 100644 README.md create mode 100644 hooks/__init__.py create mode 100644 hooks/format_c_source.py create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..29cbc4a --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# fw-precommit-hooks +Contains custom hooks for the [pre-commit](https://pre-commit.com/) python package for checking source code. + +## Support +This works with two different tools to format source code with the help of pre-commit: + * [astyle](https://astyle.sourceforge.net/) - an old package that does a pretty decent job, but no longer maintained + * [clang-format](https://clang.llvm.org/docs/ClangFormat.html) - a newer tools that is well supported with lots of formatting options + +## Usage +In order to use this, you need to add a section to your .pre-commit-config.yaml file to get this hook that specifies which of the formatters you want to use and give it a path to the options file(s). You can use either or both `astyle` or `clang-format` + +Here is an example of clang format usage: +``` +- repo: https://github.com/delsauce/pre-commit-hooks + rev: v1.0.0 + hooks: + - id: format-c-source + types: [file, c] + args: [--clangformat, 'path-to-clang-format-options-file'] +``` + +If you'd rather use `astyle` change the args: +``` + args: [--astlye, 'path-to-astyle-options-file'] +``` diff --git a/hooks/__init__.py b/hooks/__init__.py new file mode 100644 index 0000000..9b55e7d --- /dev/null +++ b/hooks/__init__.py @@ -0,0 +1,4 @@ +""" +Version info for this package +""" +__version__ = "1.0.0" diff --git a/hooks/format_c_source.py b/hooks/format_c_source.py new file mode 100644 index 0000000..12090b5 --- /dev/null +++ b/hooks/format_c_source.py @@ -0,0 +1,134 @@ +""" +This module runs various style tools to format source code files. It is meant to be used +in conjunction with the python module precommit (https://pre-commit.com/) to help enforce +guidelines before code is committed to a git repository. +""" +import os +import subprocess +import argparse +import shutil +import sys +from typing import ( + Optional, + Sequence +) + +def main(argv: Optional[Sequence[str]] = None) -> int: + """ + Format specified text source files + + :param argv: list of filenames to process + :return program exit code, 0 = success, > 0 fail (1 for generic error) + """ + + parser = argparse.ArgumentParser() + parser.add_argument( + 'filenames', nargs='*', + help='Filenames pre-commit believes are changed.', + ) + + # Eventually we can add other tool options here... + parser.add_argument( + '-a', '--astyle', type=str, + help='path to astyle config file', + ) + parser.add_argument( + '-c', '--clangformat', type=str, + help='path to clang-format options file', + ) + + args = parser.parse_args(argv) + + if not (args.astyle or args.clangformat): + parser.error('No action requested, add --astyle or --clangformat') + + # Bail early if there are no files to process + if len(args.filenames) == 0: + print('No files') + return 0 + + # Note: sourcetree doesn't use the same system path when running commit hooks (there + # is a bug in their tool). We can try to fix that here (not really sure what + # happens on windows -- so not handling that now) + if sys.platform != 'win32': + if '/usr/local/bin' not in os.environ['PATH']: + os.environ['PATH'] += os.pathsep + '/usr/local/bin' + + if args.astyle: + # check to make sure astyle is installed + locate = shutil.which('astyle') + + if locate is None: + print('astyle executable not found on in PATH, is it installed?') + print('Consult your favorite package manager for installation') + print('(e.g. \'brew install astyle\' on mac or \'apt-get install astyle\' on Ubuntu)') + return 1 + + # Check to make sure that options file is present + if not os.path.exists(args.astyle): + print(f'{args.astyle} not found. Please check that the file exists') + return 1 + + # Run astyle + for fname in args.filenames: + # Since check=True below, this will throw an exception if the call + # to astyle fails for some reason. Note, as long as the options and + # filename passed to astyle are OK, it should never fail (always returns + # a '0' whether it formatted a file or not. Need to check the output + # to know if it actually changed anything in the file or not. + astyle_result = subprocess.run(["astyle", + "--options=" + args.astyle, fname], + capture_output=True, text=True, + check=True) + + if 'Formatted' in astyle_result.stdout: + print('Formatted: ' + fname) + + + if args.clangformat: + # check to make sure astyle is installed + locate = shutil.which('clang-format') + + if locate is None: + print('clang-format executable not found on in PATH, is it installed?') + print('Consult your favorite package manager for installation') + print('(e.g. \'brew install clang-format\' on mac or \'apt-get install clang-format\' on Debian/Ubuntu)') + return 1 + + # Check to make sure that options file is present + if not os.path.exists(args.clangformat): + print(f'{args.clangformat} not found. Please check that the file exists') + return 1 + + # Run clang-format + for fname in args.filenames: + # Since check=True below, this will throw an exception if the call + # to clang-format fails for some reason. Note, as long as the options and + # filename passed to clang-format are OK, it should never fail. + + # First do a dry run to see if the file needs formatting or not + # (this is just to support better info being printed during execution) + clang_result = subprocess.run(["clang-format", + "--dry-run", + "--Werror", + "--style=file:" + args.clangformat, + "-i", fname], + capture_output=True, text=True) + + if clang_result.returncode != 0: + # Error code set means the file needs formatting + clang_result = subprocess.run(["clang-format", + "--style=file:" + args.clangformat, + "-i", fname]) + + if clang_result.returncode == 0: + print('Formatted: ' + fname) + else: + print('clang-format can not format file: ' + clang_result.stdout) + return 1 + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..93da9db --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# No requirements necessary for the module, add to this file if changes needed + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..a2410c0 --- /dev/null +++ b/setup.py @@ -0,0 +1,54 @@ +''' +pip package configuration for embedded c coding standard compliance +''' +import codecs +import os +from setuptools import setup + +# pylint: disable-next=consider-using-with +reqs: list = open("requirements.txt", "r",encoding='utf-8').read().splitlines() + +def read(rel_path): + ''' + Supporting function to read the __init__.py file from the package + to get attributes. See get_version() below for more details. + ''' + here = os.path.abspath(os.path.dirname(__file__)) + # intentionally *not* adding an encoding option to open, See: + # https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690 + with codecs.open(os.path.join(here, rel_path), 'r') as file_pointer: + return file_pointer.read() + +def get_version(rel_path): + ''' + Allows the version of the package to be fetched via the one + stored in the __init__.py file. This enables a single location + to store this information (rather than having to pass it in + with the metadata in the call to setup()) + + See here: + https://packaging.python.org/guides/single-sourcing-package-version/ + ''' + for line in read(rel_path).splitlines(): + if line.startswith('__version__'): + delim = '"' if '"' in line else "'" + return line.split(delim)[1] + + raise RuntimeError("Unable to find version string.") + +setup(name='pre_commit_hooks', + description='C/C++ File formatting and cleanup per coding standards', + url='https://github.com/delsauce/pre-commit-hooks', + license='MIT', + packages=['pre_commit_hooks'], + zip_safe=False, + version=get_version("hooks/__init__.py"), + entry_points={ + 'console_scripts': [ + 'format-c-source=hooks.format_c_source:main', + ], + }, + + python_requires='>=3.7', + install_requires=reqs +)