bisect_driver.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. # Copyright 2016 Google Inc. All Rights Reserved.
  2. #
  3. # This script is used to help the compiler wrapper in the Android build system
  4. # bisect for bad object files.
  5. """Utilities for bisection of Android object files.
  6. This module contains a set of utilities to allow bisection between
  7. two sets (good and bad) of object files. Mostly used to find compiler
  8. bugs.
  9. Reference page:
  10. https://sites.google.com/a/google.com/chromeos-toolchain-team-home2/home/team-tools-and-scripts/bisecting-chromeos-compiler-problems/bisection-compiler-wrapper
  11. Design doc:
  12. https://docs.google.com/document/d/1yDgaUIa2O5w6dc3sSTe1ry-1ehKajTGJGQCbyn0fcEM
  13. """
  14. from __future__ import print_function
  15. import contextlib
  16. import fcntl
  17. import os
  18. import shutil
  19. import subprocess
  20. import sys
  21. VALID_MODES = ['POPULATE_GOOD', 'POPULATE_BAD', 'TRIAGE']
  22. GOOD_CACHE = 'good'
  23. BAD_CACHE = 'bad'
  24. LIST_FILE = os.path.join(GOOD_CACHE, '_LIST')
  25. CONTINUE_ON_MISSING = os.environ.get('BISECT_CONTINUE_ON_MISSING', None) == '1'
  26. WRAPPER_SAFE_MODE = os.environ.get('BISECT_WRAPPER_SAFE_MODE', None) == '1'
  27. class Error(Exception):
  28. """The general compiler wrapper error class."""
  29. pass
  30. @contextlib.contextmanager
  31. def lock_file(path, mode):
  32. """Lock file and block if other process has lock on file.
  33. Acquire exclusive lock for file. Only blocks other processes if they attempt
  34. to also acquire lock through this method. If only reading (modes 'r' and 'rb')
  35. then the lock is shared (i.e. many reads can happen concurrently, but only one
  36. process may write at a time).
  37. This function is a contextmanager, meaning it's meant to be used with the
  38. "with" statement in Python. This is so cleanup and setup happens automatically
  39. and cleanly. Execution of the outer "with" statement happens at the "yield"
  40. statement. Execution resumes after the yield when the outer "with" statement
  41. ends.
  42. Args:
  43. path: path to file being locked
  44. mode: mode to open file with ('w', 'r', etc.)
  45. """
  46. with open(path, mode) as f:
  47. # Share the lock if just reading, make lock exclusive if writing
  48. if f.mode == 'r' or f.mode == 'rb':
  49. lock_type = fcntl.LOCK_SH
  50. else:
  51. lock_type = fcntl.LOCK_EX
  52. try:
  53. fcntl.lockf(f, lock_type)
  54. yield f
  55. f.flush()
  56. except:
  57. raise
  58. finally:
  59. fcntl.lockf(f, fcntl.LOCK_UN)
  60. def log_to_file(path, execargs, link_from=None, link_to=None):
  61. """Common logging function.
  62. Log current working directory, current execargs, and a from-to relationship
  63. between files.
  64. """
  65. with lock_file(path, 'a') as log:
  66. log.write('cd: %s; %s\n' % (os.getcwd(), ' '.join(execargs)))
  67. if link_from and link_to:
  68. log.write('%s -> %s\n' % (link_from, link_to))
  69. def exec_and_return(execargs):
  70. """Execute process and return.
  71. Execute according to execargs and return immediately. Don't inspect
  72. stderr or stdout.
  73. """
  74. return subprocess.call(execargs)
  75. def which_cache(obj_file):
  76. """Determine which cache an object belongs to.
  77. The binary search tool creates two files for each search iteration listing
  78. the full set of bad objects and full set of good objects. We use this to
  79. determine where an object file should be linked from (good or bad).
  80. """
  81. bad_set_file = os.environ.get('BISECT_BAD_SET')
  82. ret = subprocess.call(['grep', '-x', '-q', obj_file, bad_set_file])
  83. if ret == 0:
  84. return BAD_CACHE
  85. else:
  86. return GOOD_CACHE
  87. def makedirs(path):
  88. """Try to create directories in path."""
  89. try:
  90. os.makedirs(path)
  91. except os.error:
  92. if not os.path.isdir(path):
  93. raise
  94. def get_obj_path(execargs):
  95. """Get the object path for the object file in the list of arguments.
  96. Returns:
  97. Absolute object path from execution args (-o argument). If no object being
  98. outputted or output doesn't end in ".o" then return empty string.
  99. """
  100. try:
  101. i = execargs.index('-o')
  102. except ValueError:
  103. return ''
  104. obj_path = execargs[i + 1]
  105. if not obj_path.endswith(('.o',)):
  106. # TODO: what suffixes do we need to contemplate
  107. # TODO: add this as a warning
  108. # TODO: need to handle -r compilations
  109. return ''
  110. return os.path.abspath(obj_path)
  111. def get_dep_path(execargs):
  112. """Get the dep file path for the dep file in the list of arguments.
  113. Returns:
  114. Absolute path of dependency file path from execution args (-o argument). If
  115. no dependency being outputted then return empty string.
  116. """
  117. if '-MD' not in execargs and '-MMD' not in execargs:
  118. return ''
  119. # If -MF given this is the path of the dependency file. Otherwise the
  120. # dependency file is the value of -o but with a .d extension
  121. if '-MF' in execargs:
  122. i = execargs.index('-MF')
  123. dep_path = execargs[i + 1]
  124. return os.path.abspath(dep_path)
  125. full_obj_path = get_obj_path(execargs)
  126. if not full_obj_path:
  127. return ''
  128. return full_obj_path[:-2] + '.d'
  129. def get_dwo_path(execargs):
  130. """Get the dwo file path for the dwo file in the list of arguments.
  131. Returns:
  132. Absolute dwo file path from execution args (-gsplit-dwarf argument) If no
  133. dwo file being outputted then return empty string.
  134. """
  135. if '-gsplit-dwarf' not in execargs:
  136. return ''
  137. full_obj_path = get_obj_path(execargs)
  138. if not full_obj_path:
  139. return ''
  140. return full_obj_path[:-2] + '.dwo'
  141. def in_object_list(obj_name, list_filename):
  142. """Check if object file name exist in file with object list."""
  143. if not obj_name:
  144. return False
  145. with lock_file(list_filename, 'r') as list_file:
  146. for line in list_file:
  147. if line.strip() == obj_name:
  148. return True
  149. return False
  150. def get_side_effects(execargs):
  151. """Determine side effects generated by compiler
  152. Returns:
  153. List of paths of objects that the compiler generates as side effects.
  154. """
  155. side_effects = []
  156. # Cache dependency files
  157. full_dep_path = get_dep_path(execargs)
  158. if full_dep_path:
  159. side_effects.append(full_dep_path)
  160. # Cache dwo files
  161. full_dwo_path = get_dwo_path(execargs)
  162. if full_dwo_path:
  163. side_effects.append(full_dwo_path)
  164. return side_effects
  165. def cache_file(execargs, bisect_dir, cache, abs_file_path):
  166. """Cache compiler output file (.o/.d/.dwo)."""
  167. # os.path.join fails with absolute paths, use + instead
  168. bisect_path = os.path.join(bisect_dir, cache) + abs_file_path
  169. bisect_path_dir = os.path.dirname(bisect_path)
  170. makedirs(bisect_path_dir)
  171. pop_log = os.path.join(bisect_dir, cache, '_POPULATE_LOG')
  172. log_to_file(pop_log, execargs, abs_file_path, bisect_path)
  173. try:
  174. if os.path.exists(abs_file_path):
  175. shutil.copy2(abs_file_path, bisect_path)
  176. except Exception:
  177. print('Could not cache file %s' % abs_file_path, file=sys.stderr)
  178. raise
  179. def restore_file(bisect_dir, cache, abs_file_path):
  180. """Restore file from cache (.o/.d/.dwo)."""
  181. # os.path.join fails with absolute paths, use + instead
  182. cached_path = os.path.join(bisect_dir, cache) + abs_file_path
  183. if os.path.exists(cached_path):
  184. if os.path.exists(abs_file_path):
  185. os.remove(abs_file_path)
  186. try:
  187. os.link(cached_path, abs_file_path)
  188. except OSError:
  189. shutil.copyfile(cached_path, abs_file_path)
  190. else:
  191. raise Error(('%s is missing from %s cache! Unsure how to proceed. Make '
  192. 'will now crash.' % (cache, cached_path)))
  193. def bisect_populate(execargs, bisect_dir, population_name):
  194. """Add necessary information to the bisect cache for the given execution.
  195. Extract the necessary information for bisection from the compiler
  196. execution arguments and put it into the bisection cache. This
  197. includes copying the created object file, adding the object
  198. file path to the cache list and keeping a log of the execution.
  199. Args:
  200. execargs: compiler execution arguments.
  201. bisect_dir: bisection directory.
  202. population_name: name of the cache being populated (good/bad).
  203. """
  204. retval = exec_and_return(execargs)
  205. if retval:
  206. return retval
  207. full_obj_path = get_obj_path(execargs)
  208. # If not a normal compiler call then just exit
  209. if not full_obj_path:
  210. return
  211. cache_file(execargs, bisect_dir, population_name, full_obj_path)
  212. population_dir = os.path.join(bisect_dir, population_name)
  213. with lock_file(os.path.join(population_dir, '_LIST'), 'a') as object_list:
  214. object_list.write('%s\n' % full_obj_path)
  215. for side_effect in get_side_effects(execargs):
  216. cache_file(execargs, bisect_dir, population_name, side_effect)
  217. def bisect_triage(execargs, bisect_dir):
  218. full_obj_path = get_obj_path(execargs)
  219. obj_list = os.path.join(bisect_dir, LIST_FILE)
  220. # If the output isn't an object file just call compiler
  221. if not full_obj_path:
  222. return exec_and_return(execargs)
  223. # If this isn't a bisected object just call compiler
  224. # This shouldn't happen!
  225. if not in_object_list(full_obj_path, obj_list):
  226. if CONTINUE_ON_MISSING:
  227. log_file = os.path.join(bisect_dir, '_MISSING_CACHED_OBJ_LOG')
  228. log_to_file(log_file, execargs, '? compiler', full_obj_path)
  229. return exec_and_return(execargs)
  230. else:
  231. raise Error(('%s is missing from cache! To ignore export '
  232. 'BISECT_CONTINUE_ON_MISSING=1. See documentation for more '
  233. 'details on this option.' % full_obj_path))
  234. cache = which_cache(full_obj_path)
  235. # If using safe WRAPPER_SAFE_MODE option call compiler and overwrite the
  236. # result from the good/bad cache. This option is safe and covers all compiler
  237. # side effects, but is very slow!
  238. if WRAPPER_SAFE_MODE:
  239. retval = exec_and_return(execargs)
  240. if retval:
  241. return retval
  242. os.remove(full_obj_path)
  243. restore_file(bisect_dir, cache, full_obj_path)
  244. return
  245. # Generate compiler side effects. Trick Make into thinking compiler was
  246. # actually executed.
  247. for side_effect in get_side_effects(execargs):
  248. restore_file(bisect_dir, cache, side_effect)
  249. # If generated object file happened to be pruned/cleaned by Make then link it
  250. # over from cache again.
  251. if not os.path.exists(full_obj_path):
  252. restore_file(bisect_dir, cache, full_obj_path)
  253. def bisect_driver(bisect_stage, bisect_dir, execargs):
  254. """Call appropriate bisection stage according to value in bisect_stage."""
  255. if bisect_stage == 'POPULATE_GOOD':
  256. bisect_populate(execargs, bisect_dir, GOOD_CACHE)
  257. elif bisect_stage == 'POPULATE_BAD':
  258. bisect_populate(execargs, bisect_dir, BAD_CACHE)
  259. elif bisect_stage == 'TRIAGE':
  260. bisect_triage(execargs, bisect_dir)
  261. else:
  262. raise ValueError('wrong value for BISECT_STAGE: %s' % bisect_stage)