Add UMA stats for ExtensionCache
[chromium-blink-merge.git] / tools / update_reference_build.py
blob112599b8c124aa2c3ee984879b148b447fa7fffb
1 #!/usr/bin/env python
3 # Copyright (c) 2013 The Chromium Authors. All rights reserved.
4 # Use of this source code is governed by a BSD-style license that can be
5 # found in the LICENSE file.
7 """Updates the Chrome reference builds.
9 Use -r option to update a Chromium reference build, or -b option for Chrome
10 official builds.
12 Usage:
13 $ cd /tmp
14 $ /path/to/update_reference_build.py -r <revision>
15 $ cd reference_builds/reference_builds
16 $ gcl change
17 $ gcl upload <change>
18 $ gcl commit <change>
19 """
21 import logging
22 import optparse
23 import os
24 import shutil
25 import subprocess
26 import sys
27 import time
28 import urllib
29 import urllib2
30 import zipfile
32 # Example chromium build location:
33 # gs://chromium-browser-snapshots/Linux/228977/chrome-linux.zip
34 CHROMIUM_URL_FMT = ('http://commondatastorage.googleapis.com/'
35 'chromium-browser-snapshots/%s/%s/%s')
37 # Chrome official build storage
38 # https://wiki.corp.google.com/twiki/bin/view/Main/ChromeOfficialBuilds
40 # Internal Google archive of official Chrome builds, example:
41 # https://goto.google.com/chrome_official_builds/
42 # 32.0.1677.0/precise32bit/chrome-precise32bit.zip
43 CHROME_INTERNAL_URL_FMT = ('http://master.chrome.corp.google.com/'
44 'official_builds/%s/%s/%s')
46 # Google storage location (no public web URL's), example:
47 # gs://chrome-archive/30/30.0.1595.0/precise32bit/chrome-precise32bit.zip
48 CHROME_GS_URL_FMT = ('gs://chrome-archive/%s/%s/%s/%s')
51 class BuildUpdater(object):
52 _PLATFORM_FILES_MAP = {
53 'Win': [
54 'chrome-win32.zip',
55 'chrome-win32-syms.zip',
57 'Mac': [
58 'chrome-mac.zip',
60 'Linux': [
61 'chrome-linux.zip',
63 'Linux_x64': [
64 'chrome-linux.zip',
68 _CHROME_PLATFORM_FILES_MAP = {
69 'Win': [
70 'chrome-win32.zip',
71 'chrome-win32-syms.zip',
73 'Mac': [
74 'chrome-mac.zip',
76 'Linux': [
77 'chrome-precise32bit.zip',
79 'Linux_x64': [
80 'chrome-precise64bit.zip',
84 # Map of platform names to gs:// Chrome build names.
85 _BUILD_PLATFORM_MAP = {
86 'Linux': 'precise32bit',
87 'Linux_x64': 'precise64bit',
88 'Win': 'win',
89 'Mac': 'mac',
92 _PLATFORM_DEST_MAP = {
93 'Linux': 'chrome_linux',
94 'Linux_x64': 'chrome_linux64',
95 'Win': 'chrome_win',
96 'Mac': 'chrome_mac',
99 def __init__(self, options):
100 self._platforms = options.platforms.split(',')
101 self._revision = options.build_number or int(options.revision)
102 self._use_build_number = bool(options.build_number)
103 self._use_gs = options.use_gs
105 @staticmethod
106 def _GetCmdStatusAndOutput(args, cwd=None, shell=False):
107 """Executes a subprocess and returns its exit code and output.
109 Args:
110 args: A string or a sequence of program arguments.
111 cwd: If not None, the subprocess's current directory will be changed to
112 |cwd| before it's executed.
113 shell: Whether to execute args as a shell command.
115 Returns:
116 The tuple (exit code, output).
118 logging.info(str(args) + ' ' + (cwd or ''))
119 p = subprocess.Popen(args=args, cwd=cwd, stdout=subprocess.PIPE,
120 stderr=subprocess.PIPE, shell=shell)
121 stdout, stderr = p.communicate()
122 exit_code = p.returncode
123 if stderr:
124 logging.critical(stderr)
125 logging.info(stdout)
126 return (exit_code, stdout)
128 def _GetBuildUrl(self, platform, revision, filename):
129 if self._use_build_number:
130 # Chrome Google storage bucket.
131 if self._use_gs:
132 release = revision[:revision.find('.')]
133 return (CHROME_GS_URL_FMT % (
134 release,
135 revision,
136 self._BUILD_PLATFORM_MAP[platform],
137 filename))
138 # Chrome internal archive.
139 return (CHROME_INTERNAL_URL_FMT % (
140 revision,
141 self._BUILD_PLATFORM_MAP[platform],
142 filename))
143 # Chromium archive.
144 return CHROMIUM_URL_FMT % (urllib.quote_plus(platform), revision, filename)
146 def _FindBuildRevision(self, platform, revision, filename):
147 # TODO(shadi): Iterate over build numbers to find a valid one.
148 if self._use_build_number:
149 return (revision
150 if self._DoesBuildExist(platform, revision, filename) else None)
152 MAX_REVISIONS_PER_BUILD = 100
153 for revision_guess in xrange(revision, revision + MAX_REVISIONS_PER_BUILD):
154 if self._DoesBuildExist(platform, revision_guess, filename):
155 return revision_guess
156 else:
157 time.sleep(.1)
158 return None
160 def _DoesBuildExist(self, platform, build_number, filename):
161 url = self._GetBuildUrl(platform, build_number, filename)
162 if self._use_gs:
163 return self._DoesGSFileExist(url)
165 r = urllib2.Request(url)
166 r.get_method = lambda: 'HEAD'
167 try:
168 urllib2.urlopen(r)
169 return True
170 except urllib2.HTTPError, err:
171 if err.code == 404:
172 return False
174 def _DoesGSFileExist(self, gs_file_name):
175 exit_code = BuildUpdater._GetCmdStatusAndOutput(
176 ['gsutil', 'ls', gs_file_name])[0]
177 return not exit_code
179 def _GetPlatformFiles(self, platform):
180 if self._use_build_number:
181 return BuildUpdater._CHROME_PLATFORM_FILES_MAP[platform]
182 return BuildUpdater._PLATFORM_FILES_MAP[platform]
184 def _DownloadBuilds(self):
185 for platform in self._platforms:
186 for f in self._GetPlatformFiles(platform):
187 output = os.path.join('dl', platform,
188 '%s_%s_%s' % (platform, self._revision, f))
189 if os.path.exists(output):
190 logging.info('%s alread exists, skipping download', output)
191 continue
192 build_revision = self._FindBuildRevision(platform, self._revision, f)
193 if not build_revision:
194 logging.critical('Failed to find %s build for r%s\n', platform,
195 self._revision)
196 sys.exit(1)
197 dirname = os.path.dirname(output)
198 if dirname and not os.path.exists(dirname):
199 os.makedirs(dirname)
200 url = self._GetBuildUrl(platform, build_revision, f)
201 self._DownloadFile(url, output)
203 def _DownloadFile(self, url, output):
204 logging.info('Downloading %s, saving to %s', url, output)
205 if self._use_build_number and self._use_gs:
206 BuildUpdater._GetCmdStatusAndOutput(['gsutil', 'cp', url, output])
207 else:
208 r = urllib2.urlopen(url)
209 with file(output, 'wb') as f:
210 f.write(r.read())
212 def _FetchSvnRepos(self):
213 if not os.path.exists('reference_builds'):
214 os.makedirs('reference_builds')
215 BuildUpdater._GetCmdStatusAndOutput(
216 ['gclient', 'config',
217 'svn://svn.chromium.org/chrome/trunk/deps/reference_builds'],
218 'reference_builds')
219 BuildUpdater._GetCmdStatusAndOutput(
220 ['gclient', 'sync'], 'reference_builds')
222 def _UnzipFile(self, dl_file, dest_dir):
223 if not zipfile.is_zipfile(dl_file):
224 return False
225 logging.info('Opening %s', dl_file)
226 with zipfile.ZipFile(dl_file, 'r') as z:
227 for content in z.namelist():
228 dest = os.path.join(dest_dir, content[content.find('/')+1:])
229 # Create dest parent dir if it does not exist.
230 if not os.path.isdir(os.path.dirname(dest)):
231 os.makedirs(os.path.dirname(dest))
232 # If dest is just a dir listing, do nothing.
233 if not os.path.basename(dest):
234 continue
235 if not os.path.isdir(os.path.dirname(dest)):
236 os.makedirs(os.path.dirname(dest))
237 with z.open(content) as unzipped_content:
238 logging.info('Extracting %s to %s (%s)', content, dest, dl_file)
239 with file(dest, 'wb') as dest_file:
240 dest_file.write(unzipped_content.read())
241 permissions = z.getinfo(content).external_attr >> 16
242 if permissions:
243 os.chmod(dest, permissions)
244 return True
246 def _ClearDir(self, dir):
247 """Clears all files in |dir| except for hidden files and folders."""
248 for root, dirs, files in os.walk(dir):
249 # Skip hidden files and folders (like .svn and .git).
250 files = [f for f in files if f[0] != '.']
251 dirs[:] = [d for d in dirs if d[0] != '.']
253 for f in files:
254 os.remove(os.path.join(root, f))
256 def _ExtractBuilds(self):
257 for platform in self._platforms:
258 if os.path.exists('tmp_unzip'):
259 os.path.unlink('tmp_unzip')
260 dest_dir = os.path.join('reference_builds', 'reference_builds',
261 BuildUpdater._PLATFORM_DEST_MAP[platform])
262 self._ClearDir(dest_dir)
263 for root, _, dl_files in os.walk(os.path.join('dl', platform)):
264 for dl_file in dl_files:
265 dl_file = os.path.join(root, dl_file)
266 if not self._UnzipFile(dl_file, dest_dir):
267 logging.info('Copying %s to %s', dl_file, dest_dir)
268 shutil.copy(dl_file, dest_dir)
270 def _SvnAddAndRemove(self):
271 svn_dir = os.path.join('reference_builds', 'reference_builds')
272 # List all changes without ignoring any files.
273 stat = BuildUpdater._GetCmdStatusAndOutput(['svn', 'stat', '--no-ignore'],
274 svn_dir)[1]
275 for line in stat.splitlines():
276 action, filename = line.split(None, 1)
277 # Add new and ignored files.
278 if action == '?' or action == 'I':
279 BuildUpdater._GetCmdStatusAndOutput(
280 ['svn', 'add', filename], svn_dir)
281 elif action == '!':
282 BuildUpdater._GetCmdStatusAndOutput(
283 ['svn', 'delete', filename], svn_dir)
284 filepath = os.path.join(svn_dir, filename)
285 if not os.path.isdir(filepath) and os.access(filepath, os.X_OK):
286 BuildUpdater._GetCmdStatusAndOutput(
287 ['svn', 'propset', 'svn:executable', 'true', filename], svn_dir)
289 def DownloadAndUpdateBuilds(self):
290 self._DownloadBuilds()
291 self._FetchSvnRepos()
292 self._ExtractBuilds()
293 self._SvnAddAndRemove()
296 def ParseOptions(argv):
297 parser = optparse.OptionParser()
298 usage = 'usage: %prog <options>'
299 parser.set_usage(usage)
300 parser.add_option('-b', dest='build_number',
301 help='Chrome official build number to pick up.')
302 parser.add_option('--gs', dest='use_gs', action='store_true', default=False,
303 help='Use Google storage for official builds. Used with -b '
304 'option. Default is false (i.e. use internal storage.')
305 parser.add_option('-p', dest='platforms',
306 default='Win,Mac,Linux,Linux_x64',
307 help='Comma separated list of platforms to download '
308 '(as defined by the chromium builders).')
309 parser.add_option('-r', dest='revision',
310 help='Revision to pick up.')
312 (options, _) = parser.parse_args(argv)
313 if not options.revision and not options.build_number:
314 logging.critical('Must specify either -r or -b.\n')
315 sys.exit(1)
316 if options.revision and options.build_number:
317 logging.critical('Must specify either -r or -b but not both.\n')
318 sys.exit(1)
319 if options.use_gs and not options.build_number:
320 logging.critical('Can only use --gs with -b option.\n')
321 sys.exit(1)
323 return options
326 def main(argv):
327 logging.getLogger().setLevel(logging.DEBUG)
328 options = ParseOptions(argv)
329 b = BuildUpdater(options)
330 b.DownloadAndUpdateBuilds()
331 logging.info('Successfully updated reference builds. Move to '
332 'reference_builds/reference_builds and make a change with gcl.')
334 if __name__ == '__main__':
335 sys.exit(main(sys.argv))