bisect_driver.py revision 28683f7dacd94801e9a0d4b28e5aa8a700fc3d6f
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
6"""Utilities for bisection of Android object files.
7
8This module contains a set of utilities to allow bisection between
9two sets (good and bad) of object files. Mostly used to find compiler
10bugs.
11
12Design doc:
13https://docs.google.com/document/d/1yDgaUIa2O5w6dc3sSTe1ry-1ehKajTGJGQCbyn0fcEM
14"""
15
16from __future__ import print_function
17
18import os
19import shutil
20import subprocess
21import sys
22
23VALID_MODES = ['POPULATE_GOOD', 'POPULATE_BAD', 'TRIAGE']
24DEP_CACHE = 'dep'
25GOOD_CACHE = 'good'
26BAD_CACHE = 'bad'
27LIST_FILE = os.path.join(GOOD_CACHE, '_LIST')
28
29CONTINUE_ON_MISSING = os.environ.get('BISECT_CONTINUE_ON_MISSING', None) == '1'
30
31
32class Error(Exception):
33  """The general compiler wrapper error class."""
34  pass
35
36
37def log_to_file(path, execargs, link_from=None, link_to=None):
38  """Common logging function.
39
40  Log current working directory, current execargs, and a from-to relationship
41  between files.
42  """
43  with open(path, 'a') as log:
44    log.write('cd: %s; %s\n' % (os.getcwd(), ' '.join(execargs)))
45    if link_from and link_to:
46      log.write('%s -> %s\n' % (link_from, link_to))
47
48
49def exec_and_return(execargs):
50  """Execute process and return.
51
52  Execute according to execargs and return immediately. Don't inspect
53  stderr or stdout.
54  """
55  return subprocess.call(execargs)
56
57
58def in_bad_set(obj_file):
59  """Check if object file is in bad set.
60
61  The binary search tool creates two files for each search iteration listing
62  the full set of bad objects and full set of good objects. We use this to
63  determine where an object file should be linked from (good or bad).
64  """
65  bad_set_file = os.environ.get('BISECT_BAD_SET')
66  ret = subprocess.call(['grep', '-x', '-q', obj_file, bad_set_file])
67  return ret == 0
68
69
70def makedirs(path):
71  """Try to create directories in path."""
72  try:
73    os.makedirs(path)
74  except os.error:
75    if not os.path.isdir(path):
76      raise
77
78
79def get_obj_path(execargs):
80  """Get the object path for the object file in the list of arguments.
81
82  Returns:
83    Tuple of object path from execution args (-o argument) and full object
84    path. If no object being outputted or output doesn't end in ".o" then return
85    empty strings.
86  """
87  try:
88    i = execargs.index('-o')
89  except ValueError:
90    return "", ""
91
92  obj_path = execargs[i+1]
93  if not obj_path.endswith(('.o',)):
94    # TODO: what suffixes do we need to contemplate
95    # TODO: add this as a warning
96    # TODO: need to handle -r compilations
97    return "", ""
98
99  return obj_path, os.path.join(os.getcwd(), obj_path)
100
101
102def get_dep_path(execargs):
103  """Get the dep file path for the dep file in the list of arguments.
104
105  Returns:
106    Tuple of dependency file path from execution args (-o argument) and full
107    dependency file path. If no dependency being outputted then return empty
108    strings.
109  """
110  try:
111    i = execargs.index('-MF')
112  except ValueError:
113    return "", ""
114
115  dep_path = execargs[i+1]
116  return dep_path, os.path.join(os.getcwd(), dep_path)
117
118
119def in_object_list(obj_name, list_filename):
120  """Check if object file name exist in file with object list."""
121  if not obj_name:
122    return False
123
124  with open(list_filename, 'r') as list_file:
125    for line in list_file:
126      if line.strip() == obj_name:
127        return True
128
129    return False
130
131
132def generate_side_effects(execargs, bisect_dir):
133  """Generate compiler side effects.
134
135  Generate and cache side effects so that we can trick make into thinking
136  the compiler is actually called during triaging.
137  """
138  # TODO(cburden): Cache .dwo files
139
140  # Cache dependency files
141  dep_path, _ = get_dep_path(execargs)
142  if not dep_path:
143    return
144
145  bisect_path = os.path.join(bisect_dir, DEP_CACHE, dep_path)
146  bisect_path_dir = os.path.dirname(bisect_path)
147  makedirs(bisect_path_dir)
148  pop_log = os.path.join(bisect_dir, DEP_CACHE, '_POPULATE_LOG')
149  log_to_file(pop_log, execargs, link_from=dep_path, link_to=bisect_path)
150
151  try:
152    if os.path.exists(dep_path):
153      shutil.copy2(dep_path, bisect_path)
154  except Exception:
155    print('Could not get dep file', file=sys.stderr)
156    raise
157
158
159def bisect_populate(execargs, bisect_dir, population_name):
160  """Add necessary information to the bisect cache for the given execution.
161
162  Extract the necessary information for bisection from the compiler
163  execution arguments and put it into the bisection cache. This
164  includes copying the created object file, adding the object
165  file path to the cache list and keeping a log of the execution.
166
167  Args:
168    execargs: compiler execution arguments.
169    bisect_dir: bisection directory.
170    population_name: name of the cache being populated (good/bad).
171  """
172  retval = exec_and_return(execargs)
173  if retval:
174    return retval
175
176  population_dir = os.path.join(bisect_dir, population_name)
177  makedirs(population_dir)
178  pop_log = os.path.join(population_dir, '_POPULATE_LOG')
179  log_to_file(pop_log, execargs)
180
181  obj_path, _ = get_obj_path(execargs)
182  if not obj_path:
183    return
184
185  bisect_path = os.path.join(population_dir, obj_path)
186  bisect_path_dir = os.path.dirname(bisect_path)
187  makedirs(bisect_path_dir)
188
189  try:
190    if os.path.exists(obj_path):
191      shutil.copy2(obj_path, bisect_path)
192      # Set cache object to be read-only so later compilations can't
193      # accidentally overwrite it.
194      os.chmod(bisect_path, 0444)
195  except Exception:
196    print('Could not populate bisect cache', file=sys.stderr)
197    raise
198
199  with open(os.path.join(population_dir, '_LIST'), 'a') as object_list:
200    object_list.write('%s\n' % obj_path)
201
202  # Cache the side effects generated by good compiler
203  if population_name == GOOD_CACHE:
204    generate_side_effects(execargs, bisect_dir)
205
206
207def bisect_triage(execargs, bisect_dir):
208  obj_path, _ = get_obj_path(execargs)
209  obj_list = os.path.join(bisect_dir, LIST_FILE)
210
211  # If the output isn't an object file just call compiler
212  if not obj_path:
213    return exec_and_return(execargs)
214
215  # If this isn't a bisected object just call compiler
216  # This shouldn't happen!
217  if not in_object_list(obj_path, obj_list):
218    if CONTINUE_ON_MISSING:
219      log_file = os.path.join(bisect_dir, '_MISSING_CACHED_OBJ_LOG')
220      log_to_file(log_file, execargs, link_from='? compiler', link_to=obj_path)
221      return exec_and_return(execargs)
222    else:
223      raise Error(('%s is missing from cache! To ignore export '
224                   'BISECT_CONTINUE_ON_MISSING=1. See documentation for more '
225                   'details on this option.' % obj_path))
226
227  # Generate compiler side effects. Trick Make into thinking compiler was
228  # actually executed.
229
230  # If dependency is generated from this call, link it from dependency cache
231  dep_path, full_dep_path = get_dep_path(execargs)
232  if dep_path:
233    cached_dep_path = os.path.join(bisect_dir, DEP_CACHE, dep_path)
234    if os.path.exists(cached_dep_path):
235      if os.path.exists(full_dep_path):
236        os.remove(full_dep_path)
237      os.link(cached_dep_path, full_dep_path)
238    else:
239      raise Error(('%s is missing from dependency cache! Unsure how to '
240                   'proceed. Make will now crash.' % cached_dep_path))
241
242  # If generated object file happened to be pruned/cleaned by Make then link it
243  # over from cache again.
244  if not os.path.exists(obj_path):
245    cache = BAD_CACHE if in_bad_set(obj_path) else GOOD_CACHE
246    cached_obj_path = os.path.join(bisect_dir, cache, obj_path)
247    if os.path.exists(cached_obj_path):
248      os.link(cached_obj_path, obj_path)
249    else:
250      raise Error('%s does not exist in %s cache' % (obj_path, cache))
251
252    # This is just used for debugging and stats gathering
253    log_file = os.path.join(bisect_dir, '_MISSING_OBJ_LOG')
254    log_to_file(log_file, execargs, link_from=cached_obj_path, link_to=obj_path)
255
256
257def bisect_driver(bisect_stage, bisect_dir, execargs):
258  """Call appropriate bisection stage according to value in bisect_stage."""
259  if bisect_stage == 'POPULATE_GOOD':
260    bisect_populate(execargs, bisect_dir, GOOD_CACHE)
261  elif bisect_stage == 'POPULATE_BAD':
262    bisect_populate(execargs, bisect_dir, BAD_CACHE)
263  elif bisect_stage == 'TRIAGE':
264    bisect_triage(execargs, bisect_dir)
265  else:
266    raise ValueError('wrong value for BISECT_STAGE: %s' % bisect_stage)
267