Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(159)

Side by Side Diff: scripts/slave/bot_update.py

Issue 157073002: Bot update! (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/build.git@master
Patch Set: No need to print message in chromium_util Created 6 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright 2014 The Chromium Authors. All rights reserved. 2 # Copyright 2014 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 6 import codecs
7 import copy 7 import copy
8 import optparse 8 import optparse
9 import os 9 import os
10 import pprint
11 import shutil
12 import socket
13 import subprocess
10 import sys 14 import sys
15 import time
11 import urlparse 16 import urlparse
12 17
18 import os.path as path
19
13 20
14 RECOGNIZED_PATHS = { 21 RECOGNIZED_PATHS = {
15 # If SVN path matches key, the entire URL is rewritten to the Git url. 22 # If SVN path matches key, the entire URL is rewritten to the Git url.
16 '/chrome/trunk/src': 23 '/chrome/trunk/src':
17 'https://chromium.googlesource.com/chromium/src.git', 24 'https://chromium.googlesource.com/chromium/src.git',
18 '/chrome-internal/trunk/src-internal': 25 '/chrome-internal/trunk/src-internal':
19 'https://chrome-internal.googlesource.com/chrome/src-internal.git' 26 'https://chrome-internal.googlesource.com/chrome/src-internal.git'
20 } 27 }
21 28
22 29
(...skipping 21 matching lines...) Expand all
44 The bot will perform a Git checkout in this step. 51 The bot will perform a Git checkout in this step.
45 The "gclient revert" and "update" steps are no-ops. 52 The "gclient revert" and "update" steps are no-ops.
46 53
47 """ 54 """
48 55
49 NOT_ACTIVATED_MESSAGE = """INACTIVE. 56 NOT_ACTIVATED_MESSAGE = """INACTIVE.
50 This step does nothing. You actually want to look at the "update" step. 57 This step does nothing. You actually want to look at the "update" step.
51 58
52 """ 59 """
53 60
61
62 GCLIENT_TEMPLATE = """solutions = %(solutions)s
63
64 cache_dir = %(cache_dir)s
65 """
66
54 ENABLED_MASTERS = ['chromium.git'] 67 ENABLED_MASTERS = ['chromium.git']
55 # Master: Builders dict.
56 ENABLED_BUILDERS = {} 68 ENABLED_BUILDERS = {}
57 # Master: Slaves dict.
58 ENABLED_SLAVES = {} 69 ENABLED_SLAVES = {}
59 70
60 # Disabled filters get run AFTER enabled filters, so for example if a builder 71 # Disabled filters get run AFTER enabled filters, so for example if a builder
61 # config is enabled, but a bot on that builder is disabled, that bot will 72 # config is enabled, but a bot on that builder is disabled, that bot will
62 # be disabled. 73 # be disabled.
63 DISABLED_BUILDERS = {} 74 DISABLED_BUILDERS = {}
64 DISABLED_SLAVES = {} 75 DISABLED_SLAVES = {}
65 76
77 # How many times to retry failed subprocess calls.
78 RETRIES = 3
79
80 SCRIPTS_PATH = path.dirname(path.dirname(path.abspath(__file__)))
81 DEPS2GIT_DIR_PATH = path.join(SCRIPTS_PATH, 'tools', 'deps2git')
82 DEPS2GIT_PATH = path.join(DEPS2GIT_DIR_PATH, 'deps2git.py')
83 S2G_INTERNAL_FROM_PATH = path.join(SCRIPTS_PATH, 'tools', 'deps2git_internal',
84 'svn_to_git_internal.py')
85 S2G_INTERNAL_DEST_PATH = path.join(DEPS2GIT_DIR_PATH, 'svn_to_git_internal.py')
86
87 # ../../cache_dir aka /b/build/slave/cache_dir
88 THIS_DIR = path.abspath(os.getcwd())
89 BUILDER_DIR = path.dirname(THIS_DIR)
90 SLAVE_DIR = path.dirname(BUILDER_DIR)
91 CACHE_DIR = path.join(SLAVE_DIR, 'cache_dir')
92
93
94 class SubprocessFailed(Exception):
95 pass
96
97
98 def call(*args):
99 """Interactive subprocess call."""
100 tries = 0
101 while tries < RETRIES:
iannucci 2014/02/08 01:31:31 could also do for try in xrange(RETRIES): ...
Ryan Tseng 2014/02/08 01:52:21 Done.
102 tries_msg = '(retry #%d)' % tries if tries else ''
103 print '===Running %s%s===' % (' '.join(args), tries_msg)
104 start_time = time.time()
105 proc = subprocess.Popen(args, stdout=subprocess.PIPE,
106 stderr=subprocess.STDOUT)
107 # This is here because passing 'sys.stdout' into stdout for proc will
108 # produce out of order output.
109 while True:
110 buf = proc.stdout.read(1)
iannucci 2014/02/08 01:31:31 can't we just do readline() calls?
Ryan Tseng 2014/02/08 01:52:21 And rely on the system native newline semantics pl
iannucci 2014/02/08 03:37:21 oh, I see you're just copying bytes... how does th
Ryan Tseng 2014/02/10 23:11:28 It doesn't but unless we do multiple calls at a ti
111 if not buf:
112 break
113 sys.stdout.write(buf)
114 code = proc.wait()
115 elapsed_time = ((time.time() - start_time) / 60.0)
116 if not code:
117 print '===Succeeded in %.1f mins===' % elapsed_time
118 print
119 break
iannucci 2014/02/08 01:31:31 just do a return here
Ryan Tseng 2014/02/08 01:52:21 Done.
120 print '===Failed in %.1f mins===' % elapsed_time
121 print
122 tries += 1
123
124 if code:
iannucci 2014/02/08 01:31:31 then you don't need this check here
Ryan Tseng 2014/02/08 01:52:21 Done.
125 raise SubprocessFailed('%s failed with code %d in %s' %
126 (' '.join(args), code, os.getcwd()))
iannucci 2014/02/08 01:31:31 ... after XX tries ?
Ryan Tseng 2014/02/08 01:52:21 Done.
127 return code
iannucci 2014/02/08 01:31:31 don't need this here either
Ryan Tseng 2014/02/08 01:52:21 Done.
128
129
130 def get_gclient_spec(solutions):
131 return GCLIENT_TEMPLATE % {
132 'solutions': pprint.pformat(solutions, indent=4),
133 'cache_dir': '"%s"' % CACHE_DIR
134 }
135
66 136
67 def check_enabled(master, builder, slave): 137 def check_enabled(master, builder, slave):
68 if master in ENABLED_MASTERS: 138 if master in ENABLED_MASTERS:
69 return True 139 return True
70 builder_list = ENABLED_BUILDERS.get(master) 140 builder_list = ENABLED_BUILDERS.get(master)
71 if builder_list and builder in builder_list: 141 if builder_list and builder in builder_list:
72 return True 142 return True
73 slave_list = ENABLED_SLAVES.get(master) 143 slave_list = ENABLED_SLAVES.get(master)
74 if slave_list and slave in slave_list: 144 if slave_list and slave in slave_list:
75 return True 145 return True
76 return False 146 return False
77 147
78 148
79 def check_disabled(master, builder, slave): 149 def check_disabled(master, builder, slave):
80 """Returns True if disabled, False if not disabled.""" 150 """Returns True if disabled, False if not disabled."""
81 builder_list = DISABLED_BUILDERS.get(master) 151 builder_list = DISABLED_BUILDERS.get(master)
82 if builder_list and builder in builder_list: 152 if builder_list and builder in builder_list:
83 return True 153 return True
84 slave_list = DISABLED_SLAVES.get(master) 154 slave_list = DISABLED_SLAVES.get(master)
85 if slave_list and slave in slave_list: 155 if slave_list and slave in slave_list:
86 return True 156 return True
87 return False 157 return False
88 158
89 159
90 def check_valid_host(master, builder, slave): 160 def check_valid_host(master, builder, slave):
91 return False 161 return (check_enabled(master, builder, slave)
162 and not check_disabled(master, builder, slave))
92 163
93 164
94 def solutions_printer(solutions): 165 def solutions_printer(solutions):
95 """Prints gclient solution to stdout.""" 166 """Prints gclient solution to stdout."""
96 print 'Gclient Solutions' 167 print 'Gclient Solutions'
97 print '=================' 168 print '================='
98 for solution in solutions: 169 for solution in solutions:
99 name = solution.get('name') 170 name = solution.get('name')
100 url = solution.get('url') 171 url = solution.get('url')
101 print '%s (%s)' % (name, url) 172 print '%s (%s)' % (name, url)
173 if solution.get('deps_file'):
174 print ' Dependencies file is %s' % solution['deps_file']
175 if 'managed' in solution:
176 print ' Managed mode is %s' % ('ON' if solution['managed'] else 'OFF')
102 custom_vars = solution.get('custom_vars') 177 custom_vars = solution.get('custom_vars')
103 if custom_vars: 178 if custom_vars:
104 print ' Custom Variables:' 179 print ' Custom Variables:'
105 for var_name, var_value in sorted(custom_vars.iteritems()): 180 for var_name, var_value in sorted(custom_vars.iteritems()):
106 print ' %s = %s' % (var_name, var_value) 181 print ' %s = %s' % (var_name, var_value)
107 custom_deps = solution.get('custom_deps') 182 custom_deps = solution.get('custom_deps')
108 if 'custom_deps' in solution: 183 if 'custom_deps' in solution:
109 print ' Custom Dependencies:' 184 print ' Custom Dependencies:'
110 for deps_name, deps_value in sorted(custom_deps.iteritems()): 185 for deps_name, deps_value in sorted(custom_deps.iteritems()):
111 if deps_value: 186 if deps_value:
112 print ' %s -> %s' % (deps_name, deps_value) 187 print ' %s -> %s' % (deps_name, deps_value)
113 else: 188 else:
114 print ' %s: Ignore' % deps_name 189 print ' %s: Ignore' % deps_name
115 if solution.get('deps_file'):
116 print ' Dependencies file is %s' % solution['deps_file']
117 if 'managed' in solution:
118 print ' Managed mode is %s' % ('ON' if solution['managed'] else 'OFF')
119 print 190 print
120 191
121 192
122 def solutions_to_git(input_solutions): 193 def solutions_to_git(input_solutions):
123 """Modifies urls in solutions to point at Git repos.""" 194 """Modifies urls in solutions to point at Git repos."""
124 solutions = copy.deepcopy(input_solutions) 195 solutions = copy.deepcopy(input_solutions)
125 for solution in solutions: 196 for solution in solutions:
126 original_url = solution['url'] 197 original_url = solution['url']
127 parsed_url = urlparse.urlparse(original_url) 198 parsed_url = urlparse.urlparse(original_url)
128 path = parsed_url.path 199 parsed_path = parsed_url.path
129 if path in RECOGNIZED_PATHS: 200 if parsed_path in RECOGNIZED_PATHS:
130 solution['url'] = RECOGNIZED_PATHS[path] 201 solution['url'] = RECOGNIZED_PATHS[parsed_path]
131 else: 202 else:
132 print 'Warning: path %s not recognized' % path 203 print 'Warning: path %s not recognized' % parsed_path
133 if solution.get('deps_file', 'DEPS') == 'DEPS': 204 if solution.get('deps_file', 'DEPS') == 'DEPS':
134 solution['deps_file'] = '.DEPS.git' 205 solution['deps_file'] = '.DEPS.git'
135 solution['managed'] = False 206 solution['managed'] = False
136 return solutions 207 return solutions
137 208
138 209
139 def ensure_no_git_checkout(): 210 def ensure_no_checkout(dir_names, scm_dirname):
140 """Ensure that there is no git checkout under build/. 211 """Ensure that there is no git checkout under build/.
141 212
142 If there is a git checkout under build/, then move build/ to build.dead/ 213 If there is an incorrect checkout under build/, then
214 move build/ to build.dead/
215 This function will check each directory in dir_names.
216
217 scm_dirname is expected to be either ['.svn', '.git']
143 """ 218 """
144 pass 219 assert scm_dirname in ['.svn', '.git']
220 has_checkout = any(map(lambda dir_name: path.exists(
221 path.join(os.getcwd(), dir_name, scm_dirname)), dir_names))
145 222
223 if has_checkout:
224 # cd .. && rm -rf ./build && mkdir ./build && cd build
225 build_dir = os.getcwd()
146 226
147 def ensure_no_svn_checkout(): 227 os.chdir(path.dirname(os.getcwd()))
148 """Ensure that there is no svn checkout under build/. 228 print '%s detected in checking, deleting %s...' % (scm_dirname, build_dir),
iannucci 2014/02/08 01:31:31 checking? checkout?
Ryan Tseng 2014/02/08 01:52:21 Done.
229 shutil.rmtree(build_dir)
230 print 'done'
231 os.mkdir(build_dir)
232 os.chdir(build_dir)
149 233
150 If there is a svn checkout under build/, then move build/ to build.dead/
151 """
152 pass
153 234
154 235
155 def gclient_configure(solutions): 236 def gclient_configure(solutions):
156 pass 237 """Should do the same thing as gclient --spec='...'."""
238 with codecs.open('.gclient', mode='w', encoding='utf-8') as f:
239 f.write(get_gclient_spec(solutions))
157 240
158 241
159 def gclient_shallow_sync(): 242 def gclient_sync():
160 pass 243 call('gclient', 'sync', '--verbose', '--reset', '--force',
244 '--nohooks', '--noprehooks')
161 245
162 246
163 def git_pull_and_clean(): 247 def get_git_hash(revision, dir_name):
164 pass 248 match = "^git-svn-id: [^ ]*@%d" % revision
249 cmd = ['git', 'log', '--grep', match, '--format=%H', dir_name]
250 return subprocess.check_output(cmd).strip() or None
251
252
253 def deps2git(sln_dirs):
254 for sln_dir in sln_dirs:
255 deps_file = path.join(os.getcwd(), sln_dir, 'DEPS')
256 deps_git_file = path.join(os.getcwd(), sln_dir, '.DEPS.git')
257 if not path.isfile(deps_file):
258 return
259 # Do we have a better way of doing this....?
260 repo_type = 'internal' if 'internal' in sln_dir else 'public'
261 # TODO(hinoka): This will be what populates the git caches on the first
262 # run for all of the bots. Since deps2git is single threaded,
263 # all of this will run in a single threaded context and be
264 # super slow. Should make deps2git multithreaded.
265 call(sys.executable, DEPS2GIT_PATH, '-t', repo_type,
266 '--cache_dir=%s' % CACHE_DIR,
267 '--deps=%s' % deps_file, '--out=%s' % deps_git_file)
268
269
270 def git_checkout(solutions, revision):
271 build_dir = os.getcwd()
272 # Revision only applies to the first solution.
273 first_solution = True
274 for sln in solutions:
275 name = sln['name']
276 url = sln['url']
277 sln_dir = path.join(build_dir, name)
278 if not path.isdir(sln_dir):
279 call('git', 'clone', url, sln_dir)
280
281 # Clean out .DEPS.git changes first.
282 call('git', '-C', sln_dir, 'reset', '--hard')
283 call('git', '-C', sln_dir, 'clean', '-df')
284 call('git', '-C', sln_dir, 'pull', 'origin', 'master')
285 # TODO(hinoka): We probably have to make use of revision mapping.
286 if first_solution and revision and revision.lower() != 'head':
287 if revision and revision.isdigit() and len(revision) < 40:
288 # rev_num is really a svn revision number, convert it into a git hash.
289 git_ref = get_git_hash(revision, name)
290 else:
291 # rev_num is actually a git hash or ref, we can just use it.
292 git_ref = revision
293 call('git', '-C', sln_dir, 'checkout', git_ref)
294 else:
295 call('git', '-C', sln_dir, 'checkout', 'origin/master')
296
297 first_solution = False
165 298
166 299
167 def apply_issue(issue, patchset, root, server): 300 def apply_issue(issue, patchset, root, server):
168 pass 301 pass
169 302
170 303
171 def deps2git(): 304 def delete_flag(flag_file):
172 pass 305 """Remove bot update flag."""
306 if os.path.exists(flag_file):
307 os.remove(flag_file)
173 308
174 309
175 def gclient_sync(): 310 def emit_flag(flag_file):
176 pass
177
178
179 def deposit_bot_update_flag():
180 """Deposit a bot update flag on the system to tell gclient not to run.""" 311 """Deposit a bot update flag on the system to tell gclient not to run."""
181 pass 312 print 'Emitting flag file at %s' % flag_file
313 with open(flag_file, 'wb') as f:
314 f.write('Success!')
182 315
183 316
184 def parse_args(): 317 def parse_args():
185 parse = optparse.OptionParser() 318 parse = optparse.OptionParser()
186 319
187 parse.add_option('-i', '--issue', help='Issue number to patch from.') 320 parse.add_option('-i', '--issue', help='Issue number to patch from.')
188 parse.add_option('-p', '--patchset', 321 parse.add_option('-p', '--patchset',
189 help='Patchset from issue to patch from, if applicable.') 322 help='Patchset from issue to patch from, if applicable.')
190 parse.add_option('-r', '--root', help='Repository root.') 323 parse.add_option('-r', '--root', help='Repository root.')
191 parse.add_option('-c', '--server', help='Rietveld server.') 324 parse.add_option('-c', '--server', help='Rietveld server.')
iannucci 2014/02/08 01:31:31 --server isn't really descriptive. Should be --rie
Ryan Tseng 2014/02/08 01:52:21 Done.
192 parse.add_option('-s', '--specs', help='Gcilent spec.') 325 parse.add_option('-s', '--specs', help='Gcilent spec.')
193 parse.add_option('-m', '--master', help='Master name.') 326 parse.add_option('-m', '--master', help='Master name.')
194 parse.add_option('-f', '--force', action='store_true', 327 parse.add_option('-f', '--force', action='store_true',
195 help='Bypass check to see if we want to be run. ' 328 help='Bypass check to see if we want to be run. '
196 'Should ONLY be used locally.') 329 'Should ONLY be used locally.')
330 # TODO(hinoka): We don't actually use this yet, we should factor this in.
197 parse.add_option('-e', '--revision-mapping') 331 parse.add_option('-e', '--revision-mapping')
332 parse.add_option('-v', '--revision')
333 parse.add_option('-b', '--build_dir', default=os.getcwd())
334 parse.add_option('-g', '--flag_file', default=path.join(os.getcwd(),
iannucci 2014/02/08 01:31:31 These short options aren't super-meaningful... wdy
Ryan Tseng 2014/02/08 01:52:21 But I like short option names :( I guess thats ok
335 'update.flag'))
198 336
199 return parse.parse_args() 337 return parse.parse_args()
200 338
201 339
202 def main(): 340 def main():
203 # Get inputs. 341 # Get inputs.
204 options, _ = parse_args() 342 options, _ = parse_args()
205 builder = os.environ.get('BUILDBOT_BUILDERNAME', None) 343 builder = os.environ.get('BUILDBOT_BUILDERNAME', None)
206 slave = os.environ.get('BUILDBOT_SLAVENAME', None) 344 slave = os.environ.get('BUILDBOT_SLAVENAME', socket.getfqdn().split('.')[0])
iannucci 2014/02/08 01:31:31 urk.... We need CLI options for these so we can us
Ryan Tseng 2014/02/08 01:52:21 We get them as free always in the environ as part
iannucci 2014/02/08 03:37:21 1) They assume buildbot 2) Passing data via env va
207 master = options.master 345 master = options.master
346 if not options.revision:
347 options.revision = os.environ.get('BUILDBOT_REVISION')
208 348
209 # Check if this script should activate or not. 349 # Check if this script should activate or not.
210 active = check_valid_host(master, builder, slave) or options.force 350 active = check_valid_host(master, builder, slave) or options.force
211 351
212 # Print helpful messages to tell devs whats going on. 352 # Print helpful messages to tell devs whats going on.
213 print BOT_UPDATE_MESSAGE % { 353 print BOT_UPDATE_MESSAGE % {
214 'master': master, 354 'master': master,
215 'builder': builder, 355 'builder': builder,
216 'slave': slave, 356 'slave': slave,
217 }, 357 },
218 # Print to stderr so that it shows up red on win/mac. 358 # Print to stderr so that it shows up red on win/mac.
219 print ACTIVATED_MESSAGE if active else NOT_ACTIVATED_MESSAGE 359 print ACTIVATED_MESSAGE if active else NOT_ACTIVATED_MESSAGE
220 360
221 # Parse, munipulate, and print the gclient solutions. 361 # Parse, munipulate, and print the gclient solutions.
222 specs = {} 362 specs = {}
223 exec(options.specs, specs) # TODO(hinoka): LOL this is terrible. 363 exec(options.specs, specs)
224 solutions = specs.get('solutions', []) 364 svn_solutions = specs.get('solutions', [])
225 git_solutions = solutions_to_git(solutions) 365 git_solutions = solutions_to_git(svn_solutions)
226 solutions_printer(git_solutions) 366 solutions_printer(git_solutions)
227 367
228 # Do the checkout. 368 # Cleanup svn checkout if active, otherwise remove git checkout and exit.
229 # TODO(hinoka): Uncomment these once they're implemented. 369 dir_names = [sln.get('name') for sln in svn_solutions if 'name' in sln]
230 # ensure_no_svn_checkout() 370 if active:
231 # gclient_configure(git_solutions) 371 ensure_no_checkout(dir_names, '.svn')
232 # gclient_shallow_sync() 372 emit_flag(options.flag_file)
233 # git_pull_and_clean() 373 else:
374 ensure_no_checkout(dir_names, '.git')
375 delete_flag(options.flag_file)
376 return
377
378 # Get a checkout of each solution, without DEPS or hooks.
379 # Calling git directory because there is no way to run Gclient without
380 # invoking DEPS.
381 print 'Fetching Git checkout'
382 git_checkout(git_solutions, options.revision)
383
384 # TODO(hinoka): This must be implemented before we can turn this on for TS.
234 # if options.issue: 385 # if options.issue:
235 # apply_issue(options.issue, options.patchset, options.root, options.server) 386 # apply_issue(options.issue, options.patchset, options.root, options.server)
236 # deps2git() 387
237 # gclient_sync() 388 # Magic to get deps2git to work with internal DEPS.
389 shutil.copyfile(S2G_INTERNAL_FROM_PATH, S2G_INTERNAL_DEST_PATH)
390 deps2git(dir_names)
391
392 gclient_configure(git_solutions)
393 gclient_sync()
238 394
239 395
240 if __name__ == '__main__': 396 if __name__ == '__main__':
241 sys.exit(main()) 397 sys.exit(main())
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698