update_platform_patches.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  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. """
  7. Utility to ease the updating of platform patches against ungoogled-chromium's patches
  8. """
  9. import argparse
  10. import os
  11. import shutil
  12. import sys
  13. from pathlib import Path
  14. sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'utils'))
  15. from _common import ENCODING, get_logger
  16. from patches import merge_patches
  17. sys.path.pop(0)
  18. _SERIES = 'series'
  19. _SERIES_ORIG = 'series.orig'
  20. _SERIES_PREPEND = 'series.prepend'
  21. _SERIES_MERGED = 'series.merged'
  22. def merge_platform_patches(platform_patches_dir, prepend_patches_dir):
  23. '''
  24. Prepends prepend_patches_dir into platform_patches_dir
  25. Returns True if successful, False otherwise
  26. '''
  27. if not (platform_patches_dir / _SERIES).exists():
  28. get_logger().error('Unable to find platform series file: %s',
  29. platform_patches_dir / _SERIES)
  30. return False
  31. # Make series.orig file
  32. shutil.copyfile(str(platform_patches_dir / _SERIES), str(platform_patches_dir / _SERIES_ORIG))
  33. # Make series.prepend
  34. shutil.copyfile(str(prepend_patches_dir / _SERIES), str(platform_patches_dir / _SERIES_PREPEND))
  35. # Merge patches
  36. merge_patches([prepend_patches_dir], platform_patches_dir, prepend=True)
  37. (platform_patches_dir / _SERIES).replace(platform_patches_dir / _SERIES_MERGED)
  38. return True
  39. def _dir_empty(path):
  40. '''
  41. Returns True if the directory exists and is empty; False otherwise
  42. '''
  43. try:
  44. next(os.scandir(str(path)))
  45. except StopIteration:
  46. return True
  47. except FileNotFoundError:
  48. pass
  49. return False
  50. def _remove_files_with_dirs(root_dir, sorted_file_iter):
  51. '''
  52. Deletes a list of sorted files relative to root_dir, removing empty directories along the way
  53. '''
  54. past_parent = None
  55. for partial_path in sorted_file_iter:
  56. complete_path = Path(root_dir, partial_path)
  57. try:
  58. complete_path.unlink()
  59. except FileNotFoundError:
  60. get_logger().warning('Could not remove prepended patch: %s', complete_path)
  61. if past_parent != complete_path.parent:
  62. while past_parent and _dir_empty(past_parent):
  63. past_parent.rmdir()
  64. past_parent = past_parent.parent
  65. past_parent = complete_path.parent
  66. # Handle last path's directory
  67. while _dir_empty(complete_path.parent):
  68. complete_path.parent.rmdir()
  69. complete_path = complete_path.parent
  70. def unmerge_platform_patches(platform_patches_dir):
  71. '''
  72. Undo merge_platform_patches(), adding any new patches from series.merged as necessary
  73. Returns True if successful, False otherwise
  74. '''
  75. if not (platform_patches_dir / _SERIES_PREPEND).exists():
  76. get_logger().error('Unable to find series.prepend at: %s',
  77. platform_patches_dir / _SERIES_PREPEND)
  78. return False
  79. prepend_series = set(
  80. filter(len,
  81. (platform_patches_dir / _SERIES_PREPEND).read_text(encoding=ENCODING).splitlines()))
  82. # Remove prepended files with directories
  83. _remove_files_with_dirs(platform_patches_dir, sorted(prepend_series))
  84. # Determine positions of blank spaces in series.orig
  85. if not (platform_patches_dir / _SERIES_ORIG).exists():
  86. get_logger().error('Unable to find series.orig at: %s', platform_patches_dir / _SERIES_ORIG)
  87. return False
  88. orig_series = (platform_patches_dir / _SERIES_ORIG).read_text(encoding=ENCODING).splitlines()
  89. # patch path -> list of lines after patch path and before next patch path
  90. path_comments = dict()
  91. # patch path -> inline comment for patch
  92. path_inline_comments = dict()
  93. previous_path = None
  94. for partial_path in orig_series:
  95. if not partial_path or partial_path.startswith('#'):
  96. if partial_path not in path_comments:
  97. path_comments[previous_path] = list()
  98. path_comments[previous_path].append(partial_path)
  99. else:
  100. path_parts = partial_path.split(' #', maxsplit=1)
  101. previous_path = path_parts[0]
  102. if len(path_parts) == 2:
  103. path_inline_comments[path_parts[0]] = path_parts[1]
  104. # Apply changes on series.merged into a modified version of series.orig
  105. if not (platform_patches_dir / _SERIES_MERGED).exists():
  106. get_logger().error('Unable to find series.merged at: %s',
  107. platform_patches_dir / _SERIES_MERGED)
  108. return False
  109. new_series = filter(
  110. len, (platform_patches_dir / _SERIES_MERGED).read_text(encoding=ENCODING).splitlines())
  111. new_series = filter((lambda x: x not in prepend_series), new_series)
  112. new_series = list(new_series)
  113. series_index = 0
  114. while series_index < len(new_series):
  115. current_path = new_series[series_index]
  116. if current_path in path_inline_comments:
  117. new_series[series_index] = current_path + ' #' + path_inline_comments[current_path]
  118. if current_path in path_comments:
  119. new_series.insert(series_index + 1, '\n'.join(path_comments[current_path]))
  120. series_index += 1
  121. series_index += 1
  122. # Write series file
  123. with (platform_patches_dir / _SERIES).open('w', encoding=ENCODING) as series_file:
  124. series_file.write('\n'.join(new_series))
  125. series_file.write('\n')
  126. # All other operations are successful; remove merging intermediates
  127. (platform_patches_dir / _SERIES_MERGED).unlink()
  128. (platform_patches_dir / _SERIES_ORIG).unlink()
  129. (platform_patches_dir / _SERIES_PREPEND).unlink()
  130. return True
  131. def main():
  132. """CLI Entrypoint"""
  133. parser = argparse.ArgumentParser(description=__doc__)
  134. parser.add_argument(
  135. 'command',
  136. choices=('merge', 'unmerge'),
  137. help='Merge or unmerge ungoogled-chromium patches with platform patches')
  138. parser.add_argument(
  139. 'platform_patches',
  140. type=Path,
  141. help='The path to the platform patches in GNU Quilt format to merge into')
  142. args = parser.parse_args()
  143. repo_dir = Path(__file__).resolve().parent.parent
  144. success = False
  145. if args.command == 'merge':
  146. success = merge_platform_patches(args.platform_patches, repo_dir / 'patches')
  147. elif args.command == 'unmerge':
  148. success = unmerge_platform_patches(args.platform_patches)
  149. else:
  150. raise NotImplementedError(args.command)
  151. if success:
  152. return 0
  153. return 1
  154. if __name__ == '__main__':
  155. sys.exit(main())