check_patch_files.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. #!/usr/bin/env python3
  2. # -*- coding: UTF-8 -*-
  3. # Copyright (c) 2019 The ungoogled-chromium Authors. All rights reserved.
  4. # Use of this source code is governed by a BSD-style license that can be
  5. # found in the LICENSE file.
  6. """Run sanity checking algorithms over ungoogled-chromium's patch files
  7. It checks the following:
  8. * All patches exist
  9. * All patches are referenced by the patch order
  10. Exit codes:
  11. * 0 if no problems detected
  12. * 1 if warnings or errors occur
  13. """
  14. import argparse
  15. import sys
  16. from pathlib import Path
  17. from third_party import unidiff
  18. sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'utils'))
  19. from _common import ENCODING, get_logger, parse_series # pylint: disable=wrong-import-order
  20. sys.path.pop(0)
  21. # File suffixes to ignore for checking unused patches
  22. _PATCHES_IGNORE_SUFFIXES = {'.md'}
  23. def _read_series_file(patches_dir, series_file, join_dir=False):
  24. """
  25. Returns a generator over the entries in the series file
  26. patches_dir is a pathlib.Path to the directory of patches
  27. series_file is a pathlib.Path relative to patches_dir
  28. join_dir indicates if the patches_dir should be joined with the series entries
  29. """
  30. for entry in parse_series(patches_dir / series_file):
  31. if join_dir:
  32. yield patches_dir / entry
  33. else:
  34. yield entry
  35. def check_patch_readability(patches_dir, series_path=Path('series')):
  36. """
  37. Check if the patches from iterable patch_path_iter are readable.
  38. Patches that are not are logged to stdout.
  39. Returns True if warnings occured, False otherwise.
  40. """
  41. warnings = False
  42. for patch_path in _read_series_file(patches_dir, series_path, join_dir=True):
  43. if patch_path.exists():
  44. with patch_path.open(encoding=ENCODING) as file_obj:
  45. try:
  46. unidiff.PatchSet(file_obj.read())
  47. except unidiff.errors.UnidiffParseError:
  48. get_logger().exception('Could not parse patch: %s', patch_path)
  49. warnings = True
  50. continue
  51. else:
  52. get_logger().warning('Patch not found: %s', patch_path)
  53. warnings = True
  54. return warnings
  55. def check_unused_patches(patches_dir, series_path=Path('series')):
  56. """
  57. Checks if there are unused patches in patch_dir from series file series_path.
  58. Unused patches are logged to stdout.
  59. patches_dir is a pathlib.Path to the directory of patches
  60. series_path is a pathlib.Path to the series file relative to the patches_dir
  61. Returns True if there are unused patches; False otherwise.
  62. """
  63. unused_patches = set()
  64. for path in patches_dir.rglob('*'):
  65. if path.is_dir():
  66. continue
  67. if path.suffix in _PATCHES_IGNORE_SUFFIXES:
  68. continue
  69. unused_patches.add(str(path.relative_to(patches_dir)))
  70. unused_patches -= set(_read_series_file(patches_dir, series_path))
  71. unused_patches.remove(str(series_path))
  72. logger = get_logger()
  73. for entry in sorted(unused_patches):
  74. logger.warning('Unused patch: %s', entry)
  75. return bool(unused_patches)
  76. def check_series_duplicates(patches_dir, series_path=Path('series')):
  77. """
  78. Checks if there are duplicate entries in the series file
  79. series_path is a pathlib.Path to the series file relative to the patches_dir
  80. returns True if there are duplicate entries; False otherwise.
  81. """
  82. entries_seen = set()
  83. for entry in _read_series_file(patches_dir, series_path):
  84. if entry in entries_seen:
  85. get_logger().warning('Patch appears more than once in series: %s', entry)
  86. return True
  87. entries_seen.add(entry)
  88. return False
  89. def main():
  90. """CLI entrypoint"""
  91. root_dir = Path(__file__).resolve().parent.parent
  92. default_patches_dir = root_dir / 'patches'
  93. parser = argparse.ArgumentParser(description=__doc__)
  94. parser.add_argument('-p',
  95. '--patches',
  96. type=Path,
  97. default=default_patches_dir,
  98. help='Path to the patches directory to use. Default: %(default)s')
  99. args = parser.parse_args()
  100. warnings = False
  101. warnings |= check_patch_readability(args.patches)
  102. warnings |= check_series_duplicates(args.patches)
  103. warnings |= check_unused_patches(args.patches)
  104. if warnings:
  105. sys.exit(1)
  106. sys.exit(0)
  107. if __name__ == '__main__':
  108. main()