OLD | NEW |
| (Empty) |
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 """ | |
7 This script runs every build as the first hook (See DEPS). If it detects that | |
8 the build should be clobbered, it will delete the contents of the build | |
9 directory. | |
10 | |
11 A landmine is tripped when a builder checks out a different revision, and the | |
12 diff between the new landmines and the old ones is non-null. At this point, the | |
13 build is clobbered. | |
14 """ | |
15 | |
16 import difflib | |
17 import errno | |
18 import gyp_environment | |
19 import logging | |
20 import optparse | |
21 import os | |
22 import re | |
23 import shutil | |
24 import sys | |
25 import subprocess | |
26 import time | |
27 | |
28 import landmine_utils | |
29 | |
30 | |
31 SRC_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) | |
32 | |
33 | |
34 def 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 | |
62 def 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 | |
85 def 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 | |
121 command = gn -q gen //out/%s/ | |
122 description = Regenerating ninja files | |
123 | |
124 build build.ninja: gn | |
125 generator = 1 | |
126 depfile = 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 | |
135 def 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 | |
153 def 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 | |
196 def 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 | |
226 def 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 | |
244 if __name__ == '__main__': | |
245 sys.exit(main()) | |
OLD | NEW |