patches-mtime-cache.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. #!/usr/bin/env python3
  2. import argparse
  3. import hashlib
  4. import json
  5. import os
  6. import posixpath
  7. import sys
  8. import traceback
  9. from lib.patches import patch_from_dir
  10. def patched_file_paths(patches_config):
  11. for target in patches_config:
  12. patch_dir = target.get('patch_dir')
  13. repo = target.get('repo')
  14. for line in patch_from_dir(patch_dir).split("\n"):
  15. if line.startswith("+++"):
  16. yield posixpath.join(repo, line[6:])
  17. def generate_cache(patches_config):
  18. mtime_cache = {}
  19. for file_path in patched_file_paths(patches_config):
  20. if file_path in mtime_cache:
  21. # File may be patched multiple times, we don't need to
  22. # rehash it since we are looking at the final result
  23. continue
  24. if not os.path.exists(file_path):
  25. print("Skipping non-existent file:", file_path)
  26. continue
  27. with open(file_path, "rb") as f:
  28. mtime_cache[file_path] = {
  29. "sha256": hashlib.sha256(f.read()).hexdigest(),
  30. "atime": os.path.getatime(file_path),
  31. "mtime": os.path.getmtime(file_path),
  32. }
  33. return mtime_cache
  34. def apply_mtimes(mtime_cache):
  35. updates = []
  36. for file_path, metadata in mtime_cache.items():
  37. if not os.path.exists(file_path):
  38. print("Skipping non-existent file:", file_path)
  39. continue
  40. with open(file_path, "rb") as f:
  41. if hashlib.sha256(f.read()).hexdigest() == metadata["sha256"]:
  42. updates.append(
  43. [file_path, metadata["atime"], metadata["mtime"]]
  44. )
  45. # We can't atomically set the times for all files at once, but by waiting
  46. # to update until we've checked all the files we at least have less chance
  47. # of only updating some files due to an error on one of the files
  48. for [file_path, atime, mtime] in updates:
  49. os.utime(file_path, (atime, mtime))
  50. def set_mtimes(patches_config, mtime):
  51. mtime_cache = {}
  52. for file_path in patched_file_paths(patches_config):
  53. if file_path in mtime_cache:
  54. continue
  55. if not os.path.exists(file_path):
  56. print("Skipping non-existent file:", file_path)
  57. continue
  58. mtime_cache[file_path] = mtime
  59. for file_path, file_mtime in mtime_cache.items():
  60. os.utime(file_path, (file_mtime, file_mtime))
  61. def main():
  62. parser = argparse.ArgumentParser(
  63. description="Make mtime cache for patched files"
  64. )
  65. subparsers = parser.add_subparsers(
  66. dest="operation", help="sub-command help"
  67. )
  68. apply_subparser = subparsers.add_parser(
  69. "apply", help="apply the mtimes from the cache"
  70. )
  71. apply_subparser.add_argument(
  72. "--cache-file", required=True, help="mtime cache file"
  73. )
  74. apply_subparser.add_argument(
  75. "--preserve-cache",
  76. action="store_true",
  77. help="don't delete cache after applying",
  78. )
  79. generate_subparser = subparsers.add_parser(
  80. "generate", help="generate the mtime cache"
  81. )
  82. generate_subparser.add_argument(
  83. "--cache-file", required=True, help="mtime cache file"
  84. )
  85. set_subparser = subparsers.add_parser(
  86. "set", help="set all mtimes to a specific date"
  87. )
  88. set_subparser.add_argument(
  89. "--mtime",
  90. type=int,
  91. required=True,
  92. help="mtime to use for all patched files",
  93. )
  94. for subparser in [generate_subparser, set_subparser]:
  95. subparser.add_argument(
  96. "--patches-config",
  97. type=argparse.FileType("r"),
  98. required=True,
  99. help="patches' config in the JSON format",
  100. )
  101. args = parser.parse_args()
  102. if args.operation == "generate":
  103. try:
  104. # Cache file may exist from a previously aborted sync. Reuse it.
  105. with open(args.cache_file, mode='r', encoding='utf-8') as fin:
  106. json.load(fin) # Make sure it's not an empty file
  107. print("Using existing mtime cache for patches")
  108. return 0
  109. except Exception:
  110. pass
  111. try:
  112. with open(args.cache_file, mode="w", encoding='utf-8') as fin:
  113. mtime_cache = generate_cache(json.load(args.patches_config))
  114. json.dump(mtime_cache, fin, indent=2)
  115. except Exception:
  116. print(
  117. "ERROR: failed to generate mtime cache for patches",
  118. file=sys.stderr,
  119. )
  120. traceback.print_exc(file=sys.stderr)
  121. return 0
  122. elif args.operation == "apply":
  123. if not os.path.exists(args.cache_file):
  124. print("ERROR: --cache-file does not exist", file=sys.stderr)
  125. return 0 # Cache file may not exist, fail more gracefully
  126. try:
  127. with open(args.cache_file, mode='r', encoding='utf-8') as file_in:
  128. apply_mtimes(json.load(file_in))
  129. if not args.preserve_cache:
  130. os.remove(args.cache_file)
  131. except Exception:
  132. print(
  133. "ERROR: failed to apply mtime cache for patches",
  134. file=sys.stderr,
  135. )
  136. traceback.print_exc(file=sys.stderr)
  137. return 0
  138. elif args.operation == "set":
  139. answer = input(
  140. "WARNING: Manually setting mtimes could mess up your build. "
  141. "If you're sure, type yes: "
  142. )
  143. if answer.lower() != "yes":
  144. print("Aborting")
  145. return 0
  146. set_mtimes(json.load(args.patches_config), args.mtime)
  147. return 0
  148. if __name__ == "__main__":
  149. sys.exit(main())