c.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. """Format C code according to QMK's style.
  2. """
  3. from shutil import which
  4. from subprocess import CalledProcessError, DEVNULL, Popen, PIPE
  5. from argcomplete.completers import FilesCompleter
  6. from milc import cli
  7. from qmk.path import normpath, is_relative_to
  8. from qmk.c_parse import c_source_files
  9. c_file_suffixes = ('c', 'h', 'cpp', 'hpp')
  10. core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms', 'modules')
  11. ignored = ('tmk_core/protocol/usb_hid', 'platforms/chibios/boards')
  12. def find_clang_format():
  13. """Returns the path to clang-format.
  14. """
  15. for clang_version in range(20, 6, -1):
  16. binary = f'clang-format-{clang_version}'
  17. if which(binary):
  18. return binary
  19. return 'clang-format'
  20. def find_diffs(files):
  21. """Run clang-format and diff it against a file.
  22. """
  23. found_diffs = False
  24. for file in files:
  25. cli.log.debug('Checking for changes in %s', file)
  26. clang_format = Popen([find_clang_format(), file], stdout=PIPE, stderr=PIPE, universal_newlines=True)
  27. diff = cli.run(['diff', '-u', f'--label=a/{file}', f'--label=b/{file}', str(file), '-'], stdin=clang_format.stdout, capture_output=True)
  28. if diff.returncode != 0:
  29. print(diff.stdout)
  30. found_diffs = True
  31. return found_diffs
  32. def cformat_run(files):
  33. """Spawn clang-format subprocess with proper arguments
  34. """
  35. # Determine which version of clang-format to use
  36. clang_format = [find_clang_format(), '-i']
  37. try:
  38. cli.run([*clang_format, *map(str, files)], check=True, capture_output=False, stdin=DEVNULL)
  39. cli.log.info('Successfully formatted the C code.')
  40. return True
  41. except CalledProcessError as e:
  42. cli.log.error('Error formatting C code!')
  43. cli.log.debug('%s exited with returncode %s', e.cmd, e.returncode)
  44. cli.log.debug('STDOUT:')
  45. cli.log.debug(e.stdout)
  46. cli.log.debug('STDERR:')
  47. cli.log.debug(e.stderr)
  48. return False
  49. def filter_files(files, core_only=False):
  50. """Yield only files to be formatted and skip the rest
  51. """
  52. files = list(map(normpath, filter(None, files)))
  53. for file in files:
  54. if core_only:
  55. # The following statement checks each file to see if the file path is
  56. # - in the core directories
  57. # - not in the ignored directories
  58. if not any(is_relative_to(file, i) for i in core_dirs) or any(is_relative_to(file, i) for i in ignored):
  59. cli.log.debug("Skipping non-core file %s, as '--core-only' is used.", file)
  60. continue
  61. if file.suffix[1:] in c_file_suffixes:
  62. yield file
  63. else:
  64. cli.log.debug('Skipping file %s', file)
  65. @cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.")
  66. @cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.')
  67. @cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all core files.')
  68. @cli.argument('--core-only', arg_only=True, action='store_true', help='Format core files only.')
  69. @cli.argument('files', nargs='*', arg_only=True, type=normpath, completer=FilesCompleter('.c'), help='Filename(s) to format.')
  70. @cli.subcommand("Format C code according to QMK's style.", hidden=False if cli.config.user.developer else True)
  71. def format_c(cli):
  72. """Format C code according to QMK's style.
  73. """
  74. # Find the list of files to format
  75. if cli.args.files:
  76. files = list(filter_files(cli.args.files, cli.args.core_only))
  77. if not files:
  78. cli.log.error('No C files in filelist: %s', ', '.join(map(str, cli.args.files)))
  79. return False
  80. if cli.args.all_files:
  81. cli.log.warning('Filenames passed with -a, only formatting: %s', ','.join(map(str, files)))
  82. elif cli.args.all_files:
  83. all_files = c_source_files(core_dirs)
  84. files = list(filter_files(all_files, True))
  85. else:
  86. git_diff_cmd = ['git', 'diff', '--name-only', cli.args.base_branch, *core_dirs]
  87. git_diff = cli.run(git_diff_cmd, stdin=DEVNULL)
  88. if git_diff.returncode != 0:
  89. cli.log.error("Error running %s", git_diff_cmd)
  90. print(git_diff.stderr)
  91. return git_diff.returncode
  92. changed_files = git_diff.stdout.strip().split('\n')
  93. files = list(filter_files(changed_files, True))
  94. # Sanity check
  95. if not files:
  96. cli.log.error('No changed files detected. Use "qmk format-c -a" to format all core files')
  97. return False
  98. # Run clang-format on the files we've found
  99. if cli.args.dry_run:
  100. return not find_diffs(files)
  101. else:
  102. return cformat_run(files)