1#!/usr/bin/env python
2# Copyright 2014 the V8 project authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""
7This script runs every build as the first hook (See DEPS). If it detects that
8the build should be clobbered, it will delete the contents of the build
9directory.
10
11A landmine is tripped when a builder checks out a different revision, and the
12diff between the new landmines and the old ones is non-null. At this point, the
13build is clobbered.
14"""
15
16import difflib
17import errno
18import gyp_environment
19import logging
20import optparse
21import os
22import re
23import shutil
24import sys
25import subprocess
26import time
27
28import landmine_utils
29
30
31SRC_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
32
33
34def get_build_dir(build_tool, is_iphone=False):
35  """
36  Returns output directory absolute path dependent on build and targets.
37  Examples:
38    r'c:\b\build\slave\win\build\src\out'
39    '/mnt/data/b/build/slave/linux/build/src/out'
40    '/b/build/slave/ios_rel_device/build/src/xcodebuild'
41
42  Keep this function in sync with tools/build/scripts/slave/compile.py
43  """
44  ret = None
45  if build_tool == 'xcode':
46    ret = os.path.join(SRC_DIR, 'xcodebuild')
47  elif build_tool in ['make', 'ninja', 'ninja-ios']:  # TODO: Remove ninja-ios.
48    if 'CHROMIUM_OUT_DIR' in os.environ:
49      output_dir = os.environ.get('CHROMIUM_OUT_DIR').strip()
50      if not output_dir:
51        raise Error('CHROMIUM_OUT_DIR environment variable is set but blank!')
52    else:
53      output_dir = landmine_utils.gyp_generator_flags().get('output_dir', 'out')
54    ret = os.path.join(SRC_DIR, output_dir)
55  elif build_tool in ['msvs', 'vs', 'ib']:
56    ret = os.path.join(SRC_DIR, 'build')
57  else:
58    raise NotImplementedError('Unexpected GYP_GENERATORS (%s)' % build_tool)
59  return os.path.abspath(ret)
60
61
62def extract_gn_build_commands(build_ninja_file):
63  """Extracts from a build.ninja the commands to run GN.
64
65  The commands to run GN are the gn rule and build.ninja build step at the
66  top of the build.ninja file. We want to keep these when deleting GN builds
67  since we want to preserve the command-line flags to GN.
68
69  On error, returns the empty string."""
70  result = ""
71  with open(build_ninja_file, 'r') as f:
72    # Read until the second blank line. The first thing GN writes to the file
73    # is the "rule gn" and the second is the section for "build build.ninja",
74    # separated by blank lines.
75    num_blank_lines = 0
76    while num_blank_lines < 2:
77      line = f.readline()
78      if len(line) == 0:
79        return ''  # Unexpected EOF.
80      result += line
81      if line[0] == '\n':
82        num_blank_lines = num_blank_lines + 1
83  return result
84
85def delete_build_dir(build_dir):
86  # GN writes a build.ninja.d file. Note that not all GN builds have args.gn.
87  build_ninja_d_file = os.path.join(build_dir, 'build.ninja.d')
88  if not os.path.exists(build_ninja_d_file):
89    shutil.rmtree(build_dir)
90    return
91
92  # GN builds aren't automatically regenerated when you sync. To avoid
93  # messing with the GN workflow, erase everything but the args file, and
94  # write a dummy build.ninja file that will automatically rerun GN the next
95  # time Ninja is run.
96  build_ninja_file = os.path.join(build_dir, 'build.ninja')
97  build_commands = extract_gn_build_commands(build_ninja_file)
98
99  try:
100    gn_args_file = os.path.join(build_dir, 'args.gn')
101    with open(gn_args_file, 'r') as f:
102      args_contents = f.read()
103  except IOError:
104    args_contents = ''
105
106  shutil.rmtree(build_dir)
107
108  # Put back the args file (if any).
109  os.mkdir(build_dir)
110  if args_contents != '':
111    with open(gn_args_file, 'w') as f:
112      f.write(args_contents)
113
114  # Write the build.ninja file sufficiently to regenerate itself.
115  with open(os.path.join(build_dir, 'build.ninja'), 'w') as f:
116    if build_commands != '':
117      f.write(build_commands)
118    else:
119      # Couldn't parse the build.ninja file, write a default thing.
120      f.write('''rule gn
121command = gn -q gen //out/%s/
122description = Regenerating ninja files
123
124build build.ninja: gn
125generator = 1
126depfile = build.ninja.d
127''' % (os.path.split(build_dir)[1]))
128
129  # Write a .d file for the build which references a nonexistant file. This
130  # will make Ninja always mark the build as dirty.
131  with open(build_ninja_d_file, 'w') as f:
132    f.write('build.ninja: nonexistant_file.gn\n')
133
134
135def needs_clobber(landmines_path, new_landmines):
136  if os.path.exists(landmines_path):
137    with open(landmines_path, 'r') as f:
138      old_landmines = f.readlines()
139    if old_landmines != new_landmines:
140      old_date = time.ctime(os.stat(landmines_path).st_ctime)
141      diff = difflib.unified_diff(old_landmines, new_landmines,
142          fromfile='old_landmines', tofile='new_landmines',
143          fromfiledate=old_date, tofiledate=time.ctime(), n=0)
144      sys.stdout.write('Clobbering due to:\n')
145      sys.stdout.writelines(diff)
146      return True
147  else:
148    sys.stdout.write('Clobbering due to missing landmines file.\n')
149    return True
150  return False
151
152
153def clobber_if_necessary(new_landmines):
154  """Does the work of setting, planting, and triggering landmines."""
155  out_dir = get_build_dir(landmine_utils.builder())
156  landmines_path = os.path.normpath(os.path.join(out_dir, '..', '.landmines'))
157  try:
158    os.makedirs(out_dir)
159  except OSError as e:
160    if e.errno == errno.EEXIST:
161      pass
162
163  if needs_clobber(landmines_path, new_landmines):
164    # Clobber contents of build directory but not directory itself: some
165    # checkouts have the build directory mounted.
166    for f in os.listdir(out_dir):
167      path = os.path.join(out_dir, f)
168      if os.path.basename(out_dir) == 'build':
169        # Only delete build directories and files for MSVS builds as the folder
170        # shares some checked out files and directories.
171        if (os.path.isdir(path) and
172            re.search(r'(?:[Rr]elease)|(?:[Dd]ebug)', f)):
173          delete_build_dir(path)
174        elif (os.path.isfile(path) and
175              (path.endswith('.sln') or
176               path.endswith('.vcxproj') or
177               path.endswith('.vcxproj.user'))):
178          os.unlink(path)
179      else:
180        if os.path.isfile(path):
181          os.unlink(path)
182        elif os.path.isdir(path):
183          delete_build_dir(path)
184    if os.path.basename(out_dir) == 'xcodebuild':
185      # Xcodebuild puts an additional project file structure into build,
186      # while the output folder is xcodebuild.
187      project_dir = os.path.join(SRC_DIR, 'build', 'all.xcodeproj')
188      if os.path.exists(project_dir) and os.path.isdir(project_dir):
189        delete_build_dir(project_dir)
190
191  # Save current set of landmines for next time.
192  with open(landmines_path, 'w') as f:
193    f.writelines(new_landmines)
194
195
196def process_options():
197  """Returns a list of landmine emitting scripts."""
198  parser = optparse.OptionParser()
199  parser.add_option(
200      '-s', '--landmine-scripts', action='append',
201      default=[os.path.join(SRC_DIR, 'build', 'get_landmines.py')],
202      help='Path to the script which emits landmines to stdout. The target '
203           'is passed to this script via option -t. Note that an extra '
204           'script can be specified via an env var EXTRA_LANDMINES_SCRIPT.')
205  parser.add_option('-v', '--verbose', action='store_true',
206      default=('LANDMINES_VERBOSE' in os.environ),
207      help=('Emit some extra debugging information (default off). This option '
208          'is also enabled by the presence of a LANDMINES_VERBOSE environment '
209          'variable.'))
210
211  options, args = parser.parse_args()
212
213  if args:
214    parser.error('Unknown arguments %s' % args)
215
216  logging.basicConfig(
217      level=logging.DEBUG if options.verbose else logging.ERROR)
218
219  extra_script = os.environ.get('EXTRA_LANDMINES_SCRIPT')
220  if extra_script:
221    return options.landmine_scripts + [extra_script]
222  else:
223    return options.landmine_scripts
224
225
226def main():
227  landmine_scripts = process_options()
228
229  if landmine_utils.builder() in ('dump_dependency_json', 'eclipse'):
230    return 0
231
232  gyp_environment.set_environment()
233
234  landmines = []
235  for s in landmine_scripts:
236    proc = subprocess.Popen([sys.executable, s], stdout=subprocess.PIPE)
237    output, _ = proc.communicate()
238    landmines.extend([('%s\n' % l.strip()) for l in output.splitlines()])
239  clobber_if_necessary(landmines)
240
241  return 0
242
243
244if __name__ == '__main__':
245  sys.exit(main())
246