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

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: Remove environ 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
« no previous file with comments | « scripts/master/factory/commands.py ('k') | scripts/slave/chromium_commands.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 for attempt in xrange(RETRIES):
pgervais 2014/02/12 17:46:26 Passing RETRIES as a global variable could be avoi
Ryan Tseng 2014/02/12 18:56:53 We just overloaded **kwargs in this function to pa
pgervais 2014/02/12 19:17:22 There are at least two solutions to that problem:
agable 2014/02/12 19:59:11 FWIW, the construct being used here (pass all **kw
101 attempt_msg = '(retry #%d)' % attempt if attempt else ''
102 print '===Running %s%s===' % (' '.join(args), attempt_msg)
103 start_time = time.time()
104 proc = subprocess.Popen(args, stdout=subprocess.PIPE,
105 stderr=subprocess.STDOUT)
106 # This is here because passing 'sys.stdout' into stdout for proc will
107 # produce out of order output.
108 while True:
109 buf = proc.stdout.read(1)
110 if not buf:
111 break
112 sys.stdout.write(buf)
pgervais 2014/02/12 17:46:26 Isn't that horribly inefficient? I trust that you
Ryan Tseng 2014/02/12 18:56:53 You'd think so, but its a pretty simple loop and o
113 code = proc.wait()
114 elapsed_time = ((time.time() - start_time) / 60.0)
115 if not code:
116 print '===Succeeded in %.1f mins===' % elapsed_time
pgervais 2014/02/12 17:46:26 nit: units have no plural. Use 'min' instead of 'm
Ryan Tseng 2014/02/12 18:56:53 I intentionally said "mins" because everything exc
pgervais 2014/02/12 19:17:22 My point was that you should write '2 min' not '2
agable 2014/02/12 19:59:11 +1. Any of 'min' or 'minutes' or 'minute(s)' are c
117 print
118 return 0
119 print '===Failed in %.1f mins===' % elapsed_time
120 print
pgervais 2014/02/12 17:46:26 You could have added \n in the previous line.
Ryan Tseng 2014/02/12 18:56:53 Noted, will fix in another CL
121
122 raise SubprocessFailed('%s failed with code %d in %s after %d attempts.' %
123 (' '.join(args), code, os.getcwd(), RETRIES))
124
125
126 def get_gclient_spec(solutions):
127 return GCLIENT_TEMPLATE % {
128 'solutions': pprint.pformat(solutions, indent=4),
129 'cache_dir': '"%s"' % CACHE_DIR
130 }
131
66 132
67 def check_enabled(master, builder, slave): 133 def check_enabled(master, builder, slave):
68 if master in ENABLED_MASTERS: 134 if master in ENABLED_MASTERS:
69 return True 135 return True
70 builder_list = ENABLED_BUILDERS.get(master) 136 builder_list = ENABLED_BUILDERS.get(master)
71 if builder_list and builder in builder_list: 137 if builder_list and builder in builder_list:
72 return True 138 return True
73 slave_list = ENABLED_SLAVES.get(master) 139 slave_list = ENABLED_SLAVES.get(master)
74 if slave_list and slave in slave_list: 140 if slave_list and slave in slave_list:
75 return True 141 return True
76 return False 142 return False
77 143
78 144
79 def check_disabled(master, builder, slave): 145 def check_disabled(master, builder, slave):
80 """Returns True if disabled, False if not disabled.""" 146 """Returns True if disabled, False if not disabled."""
81 builder_list = DISABLED_BUILDERS.get(master) 147 builder_list = DISABLED_BUILDERS.get(master)
82 if builder_list and builder in builder_list: 148 if builder_list and builder in builder_list:
83 return True 149 return True
84 slave_list = DISABLED_SLAVES.get(master) 150 slave_list = DISABLED_SLAVES.get(master)
85 if slave_list and slave in slave_list: 151 if slave_list and slave in slave_list:
86 return True 152 return True
87 return False 153 return False
88 154
89 155
90 def check_valid_host(master, builder, slave): 156 def check_valid_host(master, builder, slave):
91 return False 157 return (check_enabled(master, builder, slave)
158 and not check_disabled(master, builder, slave))
92 159
93 160
94 def solutions_printer(solutions): 161 def solutions_printer(solutions):
95 """Prints gclient solution to stdout.""" 162 """Prints gclient solution to stdout."""
96 print 'Gclient Solutions' 163 print 'Gclient Solutions'
97 print '=================' 164 print '================='
98 for solution in solutions: 165 for solution in solutions:
99 name = solution.get('name') 166 name = solution.get('name')
100 url = solution.get('url') 167 url = solution.get('url')
101 print '%s (%s)' % (name, url) 168 print '%s (%s)' % (name, url)
169 if solution.get('deps_file'):
170 print ' Dependencies file is %s' % solution['deps_file']
171 if 'managed' in solution:
172 print ' Managed mode is %s' % ('ON' if solution['managed'] else 'OFF')
102 custom_vars = solution.get('custom_vars') 173 custom_vars = solution.get('custom_vars')
103 if custom_vars: 174 if custom_vars:
104 print ' Custom Variables:' 175 print ' Custom Variables:'
105 for var_name, var_value in sorted(custom_vars.iteritems()): 176 for var_name, var_value in sorted(custom_vars.iteritems()):
106 print ' %s = %s' % (var_name, var_value) 177 print ' %s = %s' % (var_name, var_value)
107 custom_deps = solution.get('custom_deps') 178 custom_deps = solution.get('custom_deps')
108 if 'custom_deps' in solution: 179 if 'custom_deps' in solution:
109 print ' Custom Dependencies:' 180 print ' Custom Dependencies:'
110 for deps_name, deps_value in sorted(custom_deps.iteritems()): 181 for deps_name, deps_value in sorted(custom_deps.iteritems()):
111 if deps_value: 182 if deps_value:
112 print ' %s -> %s' % (deps_name, deps_value) 183 print ' %s -> %s' % (deps_name, deps_value)
113 else: 184 else:
114 print ' %s: Ignore' % deps_name 185 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 186 print
120 187
121 188
122 def solutions_to_git(input_solutions): 189 def solutions_to_git(input_solutions):
123 """Modifies urls in solutions to point at Git repos.""" 190 """Modifies urls in solutions to point at Git repos."""
124 solutions = copy.deepcopy(input_solutions) 191 solutions = copy.deepcopy(input_solutions)
125 for solution in solutions: 192 for solution in solutions:
126 original_url = solution['url'] 193 original_url = solution['url']
127 parsed_url = urlparse.urlparse(original_url) 194 parsed_url = urlparse.urlparse(original_url)
128 path = parsed_url.path 195 parsed_path = parsed_url.path
129 if path in RECOGNIZED_PATHS: 196 if parsed_path in RECOGNIZED_PATHS:
130 solution['url'] = RECOGNIZED_PATHS[path] 197 solution['url'] = RECOGNIZED_PATHS[parsed_path]
131 else: 198 else:
132 print 'Warning: path %s not recognized' % path 199 print 'Warning: path %s not recognized' % parsed_path
133 if solution.get('deps_file', 'DEPS') == 'DEPS': 200 if solution.get('deps_file', 'DEPS') == 'DEPS':
134 solution['deps_file'] = '.DEPS.git' 201 solution['deps_file'] = '.DEPS.git'
135 solution['managed'] = False 202 solution['managed'] = False
136 return solutions 203 return solutions
137 204
138 205
139 def ensure_no_git_checkout(): 206 def ensure_no_checkout(dir_names, scm_dirname):
140 """Ensure that there is no git checkout under build/. 207 """Ensure that there is no git checkout under build/.
141 208
142 If there is a git checkout under build/, then move build/ to build.dead/ 209 If there is an incorrect checkout under build/, then
210 move build/ to build.dead/
211 This function will check each directory in dir_names.
212
213 scm_dirname is expected to be either ['.svn', '.git']
143 """ 214 """
144 pass 215 assert scm_dirname in ['.svn', '.git']
216 has_checkout = any(map(lambda dir_name: path.exists(
217 path.join(os.getcwd(), dir_name, scm_dirname)), dir_names))
145 218
219 if has_checkout:
220 # cd .. && rm -rf ./build && mkdir ./build && cd build
221 build_dir = os.getcwd()
146 222
147 def ensure_no_svn_checkout(): 223 os.chdir(path.dirname(os.getcwd()))
148 """Ensure that there is no svn checkout under build/. 224 print '%s detected in checkout, deleting %s...' % (scm_dirname, build_dir),
225 shutil.rmtree(build_dir)
226 print 'done'
227 os.mkdir(build_dir)
228 os.chdir(build_dir)
149 229
150 If there is a svn checkout under build/, then move build/ to build.dead/
151 """
152 pass
153 230
154 231
155 def gclient_configure(solutions): 232 def gclient_configure(solutions):
156 pass 233 """Should do the same thing as gclient --spec='...'."""
234 with codecs.open('.gclient', mode='w', encoding='utf-8') as f:
235 f.write(get_gclient_spec(solutions))
157 236
158 237
159 def gclient_shallow_sync(): 238 def gclient_sync():
160 pass 239 call('gclient', 'sync', '--verbose', '--reset', '--force',
240 '--nohooks', '--noprehooks')
161 241
162 242
163 def git_pull_and_clean(): 243 def get_git_hash(revision, dir_name):
164 pass 244 match = "^git-svn-id: [^ ]*@%d" % revision
245 cmd = ['git', 'log', '--grep', match, '--format=%H', dir_name]
246 return subprocess.check_output(cmd).strip() or None
247
248
249 def deps2git(sln_dirs):
250 for sln_dir in sln_dirs:
251 deps_file = path.join(os.getcwd(), sln_dir, 'DEPS')
252 deps_git_file = path.join(os.getcwd(), sln_dir, '.DEPS.git')
253 if not path.isfile(deps_file):
254 return
255 # Do we have a better way of doing this....?
256 repo_type = 'internal' if 'internal' in sln_dir else 'public'
257 # TODO(hinoka): This will be what populates the git caches on the first
258 # run for all of the bots. Since deps2git is single threaded,
259 # all of this will run in a single threaded context and be
260 # super slow. Should make deps2git multithreaded.
261 call(sys.executable, DEPS2GIT_PATH, '-t', repo_type,
262 '--cache_dir=%s' % CACHE_DIR,
263 '--deps=%s' % deps_file, '--out=%s' % deps_git_file)
264
265
266 def git_checkout(solutions, revision):
267 build_dir = os.getcwd()
268 # Revision only applies to the first solution.
269 first_solution = True
270 for sln in solutions:
271 name = sln['name']
272 url = sln['url']
273 sln_dir = path.join(build_dir, name)
274 if not path.isdir(sln_dir):
275 call('git', 'clone', url, sln_dir)
276
277 # Clean out .DEPS.git changes first.
278 call('git', '-C', sln_dir, 'reset', '--hard')
279 call('git', '-C', sln_dir, 'clean', '-df')
280 call('git', '-C', sln_dir, 'pull', 'origin', 'master')
281 # TODO(hinoka): We probably have to make use of revision mapping.
282 if first_solution and revision and revision.lower() != 'head':
283 if revision and revision.isdigit() and len(revision) < 40:
284 # rev_num is really a svn revision number, convert it into a git hash.
285 git_ref = get_git_hash(revision, name)
286 else:
287 # rev_num is actually a git hash or ref, we can just use it.
288 git_ref = revision
289 call('git', '-C', sln_dir, 'checkout', git_ref)
290 else:
291 call('git', '-C', sln_dir, 'checkout', 'origin/master')
292
293 first_solution = False
165 294
166 295
167 def apply_issue(issue, patchset, root, server): 296 def apply_issue(issue, patchset, root, server):
168 pass 297 pass
169 298
170 299
171 def deps2git(): 300 def delete_flag(flag_file):
172 pass 301 """Remove bot update flag."""
302 if os.path.exists(flag_file):
303 os.remove(flag_file)
173 304
174 305
175 def gclient_sync(): 306 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.""" 307 """Deposit a bot update flag on the system to tell gclient not to run."""
181 pass 308 print 'Emitting flag file at %s' % flag_file
309 with open(flag_file, 'wb') as f:
310 f.write('Success!')
182 311
183 312
184 def parse_args(): 313 def parse_args():
185 parse = optparse.OptionParser() 314 parse = optparse.OptionParser()
186 315
187 parse.add_option('-i', '--issue', help='Issue number to patch from.') 316 parse.add_option('--issue', help='Issue number to patch from.')
188 parse.add_option('-p', '--patchset', 317 parse.add_option('--patchset',
189 help='Patchset from issue to patch from, if applicable.') 318 help='Patchset from issue to patch from, if applicable.')
190 parse.add_option('-r', '--root', help='Repository root.') 319 parse.add_option('--patch_url', help='Optional URL to SVN patch.')
191 parse.add_option('-c', '--server', help='Rietveld server.') 320 parse.add_option('--root', help='Repository root.')
192 parse.add_option('-s', '--specs', help='Gcilent spec.') 321 parse.add_option('--rietveld_server', help='Rietveld server.')
193 parse.add_option('-m', '--master', help='Master name.') 322 parse.add_option('--specs', help='Gcilent spec.')
323 parse.add_option('--master', help='Master name.')
194 parse.add_option('-f', '--force', action='store_true', 324 parse.add_option('-f', '--force', action='store_true',
195 help='Bypass check to see if we want to be run. ' 325 help='Bypass check to see if we want to be run. '
196 'Should ONLY be used locally.') 326 'Should ONLY be used locally.')
197 parse.add_option('-e', '--revision-mapping') 327 # TODO(hinoka): We don't actually use this yet, we should factor this in.
328 parse.add_option('--revision-mapping')
329 parse.add_option('--revision')
330 parse.add_option('--slave_name', default=socket.getfqdn().split('.')[0],
331 help='Hostname of the current machine, '
332 'used for determining whether or not to activate.')
333 parse.add_option('--builder_name', help='Name of the builder, '
334 'used for determining whether or not to activate.')
335 parse.add_option('--build_dir', default=os.getcwd())
336 parse.add_option('--flag_file', default=path.join(os.getcwd(),
337 'update.flag'))
198 338
199 return parse.parse_args() 339 return parse.parse_args()
200 340
201 341
202 def main(): 342 def main():
203 # Get inputs. 343 # Get inputs.
204 options, _ = parse_args() 344 options, _ = parse_args()
205 builder = os.environ.get('BUILDBOT_BUILDERNAME', None) 345 builder = options.builder_name
206 slave = os.environ.get('BUILDBOT_SLAVENAME', None) 346 slave = options.slave_name
207 master = options.master 347 master = options.master
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
« no previous file with comments | « scripts/master/factory/commands.py ('k') | scripts/slave/chromium_commands.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698