1 # Copyright 2014 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """An auto-roller for GN binaries into Chromium.
7 This script is used to update the GN binaries that a Chromium
8 checkout uses. In order to update the binaries, one must follow
11 1. Trigger try jobs to build a new GN binary at tip-of-tree and upload
12 the newly-built binaries into the right Google CloudStorage bucket.
13 2. Wait for the try jobs to complete.
14 3. Update the buildtools repo with the .sha1 hashes of the newly built
16 4. Update Chromium's DEPS file to the new version of the buildtools repo.
18 The script has four commands that correspond to the four steps above:
19 'build', 'wait', 'roll_buildtools', and 'roll_deps'.
21 The script has a fifth command, 'roll', that runs the four in order.
23 If given no arguments, the script will run the 'roll' command.
25 It can only be run on linux in a clean Chromium checkout; it should
26 error out in most cases if something bad happens, but the error checking
31 from __future__
import print_function
43 depot_tools_path
= None
44 for p
in os
.environ
['PATH'].split(os
.pathsep
):
45 if (p
.rstrip(os
.sep
).endswith('depot_tools') and
46 os
.path
.isfile(os
.path
.join(p
, 'gclient.py'))):
49 assert depot_tools_path
50 if not depot_tools_path
in sys
.path
:
51 sys
.path
.insert(0, depot_tools_path
)
53 third_party_path
= os
.path
.join(depot_tools_path
, 'third_party')
54 if not third_party_path
in sys
.path
:
55 sys
.path
.insert(0, third_party_path
)
60 CHROMIUM_REPO
= 'https://chromium.googlesource.com/chromium/src.git'
62 CODE_REVIEW_SERVER
= 'https://codereview.chromium.org'
65 class GNRoller(object):
67 self
.chromium_src_dir
= None
68 self
.buildtools_dir
= None
69 self
.old_gn_commitish
= None
70 self
.new_gn_commitish
= None
71 self
.old_gn_version
= None
72 self
.new_gn_version
= None
73 self
.reviewer
= 'dpranke@chromium.org'
74 if os
.getenv('USER') == 'dpranke':
75 self
.reviewer
= 'brettw@chromium.org'
78 parser
= argparse
.ArgumentParser()
79 parser
.usage
= __doc__
80 parser
.add_argument('command', nargs
='?', default
='roll',
81 help='build|roll|roll_buildtools|roll_deps|wait'
82 ' (%(default)s is the default)')
84 args
= parser
.parse_args()
85 command
= args
.command
87 if not ret
and command
in ('roll', 'build'):
88 ret
= self
.TriggerBuild()
89 if not ret
and command
in ('roll', 'wait'):
90 ret
= self
.WaitForBuildToFinish()
91 if not ret
and command
in ('roll', 'roll_buildtools'):
92 ret
= self
.RollBuildtools()
93 if not ret
and command
in ('roll', 'roll_deps'):
99 if sys
.platform
!= 'linux2':
100 print('roll_gn is only tested and working on Linux for now.')
103 ret
, out
, _
= self
.Call('git config --get remote.origin.url')
105 if ret
or origin
!= CHROMIUM_REPO
:
106 print('Not in a Chromium repo? git config --get remote.origin.url '
107 'returned %d: %s' % (ret
, origin
))
110 ret
, _
, _
= self
.Call('git diff -q')
112 print("Checkout is dirty, exiting")
115 _
, out
, _
= self
.Call('git rev-parse --show-toplevel', cwd
=os
.getcwd())
116 self
.chromium_src_dir
= out
.strip()
117 self
.buildtools_dir
= os
.path
.join(self
.chromium_src_dir
, 'buildtools')
119 self
.new_gn_commitish
, self
.new_gn_version
= self
.GetNewVersions()
121 _
, out
, _
= self
.Call('gn --version')
122 self
.old_gn_version
= out
.strip()
124 _
, out
, _
= self
.Call('git crrev-parse %s' % self
.old_gn_version
)
125 self
.old_gn_commitish
= out
.strip()
128 def GetNewVersions(self
):
129 _
, out
, _
= self
.Call('git log -1 --grep Cr-Commit-Position')
130 commit_msg
= out
.splitlines()
131 first_line
= commit_msg
[0]
132 new_gn_commitish
= first_line
.split()[1]
134 last_line
= commit_msg
[-1]
135 new_gn_version
= re
.sub('.*master@{#(\d+)}', '\\1', last_line
)
137 return new_gn_commitish
, new_gn_version
139 def TriggerBuild(self
):
140 ret
, _
, _
= self
.Call('git new-branch build_gn_%s' % self
.new_gn_version
)
142 print('Failed to create a new branch for build_gn_%s' %
146 self
.MakeDummyDepsChange()
148 ret
, out
, err
= self
.Call('git commit -a -m "Build gn at %s"' %
151 print('git commit failed: %s' % out
+ err
)
154 print('Uploading CL to build GN at {#%s} - %s' %
155 (self
.new_gn_version
, self
.new_gn_commitish
))
156 ret
, out
, err
= self
.Call('git cl upload -f')
158 print('git-cl upload failed: %s' % out
+ err
)
161 print('Starting try jobs')
162 self
.Call('git-cl try -b linux_chromium_gn_upload '
163 '-b mac_chromium_gn_upload '
164 '-b win8_chromium_gn_upload -r %s' % self
.new_gn_commitish
)
168 def MakeDummyDepsChange(self
):
169 with
open('DEPS') as fp
:
170 deps_content
= fp
.read()
171 new_deps
= deps_content
.replace("'buildtools_revision':",
172 "'buildtools_revision': ")
174 with
open('DEPS', 'w') as fp
:
177 def WaitForBuildToFinish(self
):
178 print('Checking build')
179 results
= self
.CheckBuild()
180 while any(r
['state'] == 'pending' for r
in results
.values()):
182 print('Sleeping for 30 seconds')
184 print('Checking build')
185 results
= self
.CheckBuild()
186 return 0 if all(r
['state'] == 'success' for r
in results
.values()) else 1
188 def CheckBuild(self
):
189 _
, out
, _
= self
.Call('git-cl issue')
191 issue
= int(out
.split()[2])
193 _
, out
, _
= self
.Call('git config user.email')
195 rpc_server
= upload
.GetRpcServer(CODE_REVIEW_SERVER
, email
)
197 props
= json
.loads(rpc_server
.Send('/api/%d' % issue
))
198 except Exception as _e
:
201 patchset
= int(props
['patchsets'][-1])
204 patchset_data
= json
.loads(rpc_server
.Send('/api/%d/%d' %
206 except Exception as _e
:
209 TRY_JOB_RESULT_STATES
= ('success', 'warnings', 'failure', 'skipped',
210 'exception', 'retry', 'pending')
211 try_job_results
= patchset_data
['try_job_results']
212 if not try_job_results
:
213 print('No try jobs found on most recent patchset')
217 for job
in try_job_results
:
218 builder
= job
['builder']
219 if builder
== 'linux_chromium_gn_upload':
221 elif builder
== 'mac_chromium_gn_upload':
223 elif builder
== 'win8_chromium_gn_upload':
226 print('Unexpected builder: %s')
229 state
= TRY_JOB_RESULT_STATES
[int(job
['result'])]
230 url_str
= ' %s' % job
['url']
231 build
= url_str
.split('/')[-1]
234 results
.setdefault(platform
, {'build': -1, 'sha1': '', 'url': url_str
})
236 if state
== 'success':
237 jsurl
= url_str
.replace('/builders/', '/json/builders/')
238 fp
= urllib2
.urlopen(jsurl
)
239 js
= json
.loads(fp
.read())
241 for step
in js
['steps']:
242 if step
['name'] == 'gn sha1':
243 sha1
= step
['text'][1]
245 if results
[platform
]['build'] < build
:
246 results
[platform
]['build'] = build
247 results
[platform
]['sha1'] = sha1
248 results
[platform
]['state'] = state
249 results
[platform
]['url'] = url_str
251 for platform
, r
in results
.items():
253 print(' sha1: %s' % r
['sha1'])
254 print(' state: %s' % r
['state'])
255 print(' build: %s' % r
['build'])
256 print(' url: %s' % r
['url'])
261 def RollBuildtools(self
):
262 results
= self
.CheckBuild()
263 if not all(r
['state'] == 'success' for r
in results
.values()):
264 print("Roll isn't done or didn't succeed, exiting:")
267 desc
= self
.GetBuildtoolsDesc()
269 self
.Call('git new-branch roll_buildtools_gn_%s' % self
.new_gn_version
,
270 cwd
=self
.buildtools_dir
)
272 for platform
in results
:
273 fname
= 'gn.exe.sha1' if platform
== 'win' else 'gn.sha1'
274 path
= os
.path
.join(self
.buildtools_dir
, platform
, fname
)
275 with
open(path
, 'w') as fp
:
276 fp
.write('%s\n' % results
[platform
]['sha1'])
278 desc_file
= tempfile
.NamedTemporaryFile(delete
=False)
280 desc_file
.write(desc
)
282 self
.Call('git commit -a -F %s' % desc_file
.name
,
283 cwd
=self
.buildtools_dir
)
284 self
.Call('git-cl upload -f --send-mail',
285 cwd
=self
.buildtools_dir
)
287 os
.remove(desc_file
.name
)
289 self
.Call('git cl push', cwd
=self
.buildtools_dir
)
291 # Fetch the revision we just committed so that RollDEPS will find it.
292 self
.Call('git cl fetch', cwd
=self
.buildtools_dir
)
297 _
, out
, _
= self
.Call('git rev-parse origin/master',
298 cwd
=self
.buildtools_dir
)
299 new_buildtools_commitish
= out
.strip()
302 old_buildtools_commitish
= ''
303 with
open(os
.path
.join(self
.chromium_src_dir
, 'DEPS')) as fp
:
304 for l
in fp
.readlines():
305 m
= re
.match(".*'buildtools_revision':.*'(.+)',", l
)
307 old_buildtools_commitish
= m
.group(1)
308 new_deps_lines
.append(" 'buildtools_revision': '%s'," %
309 new_buildtools_commitish
)
311 new_deps_lines
.append(l
)
313 if not old_buildtools_commitish
:
314 print('Could not update DEPS properly, exiting')
317 with
open('DEPS', 'w') as fp
:
318 fp
.write(''.join(new_deps_lines
) + '\n')
320 desc
= self
.GetDEPSRollDesc(old_buildtools_commitish
,
321 new_buildtools_commitish
)
322 desc_file
= tempfile
.NamedTemporaryFile(delete
=False)
324 desc_file
.write(desc
)
326 self
.Call('git commit -a -F %s' % desc_file
.name
)
327 self
.Call('git-cl upload -f --send-mail --commit-queue')
329 os
.remove(desc_file
.name
)
332 def GetBuildtoolsDesc(self
):
333 gn_changes
= self
.GetGNChanges()
335 'Roll gn %s..%s (r%s:%s)\n'
340 self
.old_gn_commitish
,
341 self
.new_gn_commitish
,
348 def GetDEPSRollDesc(self
, old_buildtools_commitish
, new_buildtools_commitish
):
349 gn_changes
= self
.GetGNChanges()
354 ' in order to roll GN %s..%s (r%s:%s)\n'
359 old_buildtools_commitish
,
360 new_buildtools_commitish
,
361 self
.old_gn_commitish
,
362 self
.new_gn_commitish
,
369 def GetGNChanges(self
):
370 _
, out
, _
= self
.Call(
371 "git log --pretty=' %h %s' " +
372 "%s..%s tools/gn" % (self
.old_gn_commitish
, self
.new_gn_commitish
))
375 def Call(self
, cmd
, cwd
=None):
376 proc
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
, shell
=True,
377 cwd
=(cwd
or self
.chromium_src_dir
))
378 out
, err
= proc
.communicate()
379 return proc
.returncode
, out
, err
382 if __name__
== '__main__':
384 sys
.exit(roller
.Roll())