OLD | NEW |
---|---|
(Empty) | |
1 # Copyright (c) 2016 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. | |
4 | |
5 """ Bisect repackage tool for Linux | |
6 | |
7 This script repacakges chrome builds for manual bisect script. | |
8 """ | |
9 | |
10 #Declares required files to run manual bisect script on chrome Linux | |
prasadv
2016/08/30 22:12:20
nit: Need 1 space after #
miimnk
2016/09/01 00:49:11
Done.
| |
11 #builds in perf. Binary files that should be stripped to reduce zip file | |
12 #size are declared. The file list was gotten from the local chrome | |
13 #executable path. (This can be retrieved by typing 'chrome://version' | |
14 #in chrome and following the executable path. The list needs to be updated if | |
15 #future chrome versions require additional files. | |
16 CHROME_REQUIRED_FILES = { | |
17 'linux': [ | |
prasadv
2016/08/30 22:12:20
4 space on hanging indent.
repeat same wherever ap
miimnk
2016/09/01 00:49:11
Done.
| |
18 'chrome', | |
19 'chrome_100_percent.pak', | |
20 'chrome_200_percent.pak', | |
21 'default_apps', | |
22 'icudtl.dat', | |
23 'libwidevinecdm.so', | |
24 'locales', | |
25 'nacl_helper', | |
26 'nacl_helper_bootstrap', | |
27 'nacl_irt_x86_64.nexe', | |
28 'natives_blob.bin', | |
29 'PepperFlash', | |
30 'product_logo_48.png', | |
31 'resources.pak', | |
32 'snapshot_blob.bin', | |
33 'xdg-mime', | |
34 'xdg-settings' | |
35 ], | |
36 'win64': [ | |
37 'chrome.dll', | |
38 'chrome.exe', | |
39 'chrome_100_percent.pak', | |
40 'chrome_200_percent.pak', | |
41 'chrome_child.dll', | |
42 'chrome_elf.dll', | |
43 'chrome_watcher.dll', | |
44 'default_apps', | |
45 'd3dcompiler_47.dll', | |
46 'icudtl.dat', | |
47 'libEGL.dll', | |
48 'libGLESv2.dll', | |
49 'locales', | |
50 'nacl_irt_x86_64.nexe', | |
51 'natives_blob.bin', | |
52 'PepperFlash', | |
53 'resources.pak', | |
54 'SecondaryTile.png', | |
55 'snapshot_blob.bin' | |
56 ], | |
57 'mac': [ | |
58 'Google Chrome.app' | |
59 ] | |
60 } | |
61 | |
62 CHROME_WHITELIST_FILES = { | |
63 # ^$ meanse not to include any files from whitelist | |
64 'linux': '^$', | |
65 'win64': '^\d+\.\d+\.\d+\.\d+\.manifest$', | |
66 'mac': '^$' | |
67 } | |
68 | |
69 CHROME_STRIP_LIST = { | |
70 'linux': [ | |
71 'chrome', | |
72 'nacl_helper' | |
73 ], | |
74 'win64': [ | |
75 # No stripping symbols from win64 archives | |
76 ], | |
77 'mac': [ | |
78 # No stripping symbols from mac archives | |
79 ] | |
80 } | |
81 | |
82 # API to convert Githash to Commit position number. | |
83 CHROMIUM_GITHASH_TO_SVN_URL = ( | |
84 'https://cr-rev.appspot.com/_ah/api/crrev/v1/commit/%s') | |
85 | |
86 REVISION_MAP_FILE = 'revision_map.json' | |
87 | |
88 BUILDER_NAME = { | |
89 'linux': 'Linux Builder', | |
90 'mac': 'Mac Builder', | |
91 'win32': 'Win Builder', | |
92 'win64': 'Win x64 Builder' | |
93 } | |
94 | |
95 ARCHIVE_PREFIX = { | |
96 'linux': 'full-build-linux', | |
97 'mac': 'full-build-mac', | |
98 'win32': 'full-build-win32', | |
99 'win64': 'full-build-win32' | |
100 } | |
101 | |
102 | |
103 ################################################################################ | |
prasadv
2016/08/30 22:12:20
Remove this # line
miimnk
2016/09/01 00:49:11
Done.
| |
104 import os | |
prasadv
2016/08/30 22:12:20
Ordering of import is not correct.
refer Python st
miimnk
2016/09/01 00:49:11
Done.
| |
105 import bisect_repackage_utils | |
106 import zipfile | |
107 import zlib | |
108 import json | |
109 import sys | |
110 import optparse | |
111 import re | |
112 import urllib | |
113 import threading | |
114 from multiprocessing import Pool | |
115 from functools import partial | |
116 import tempfile | |
117 import logging | |
118 | |
119 class PathContext(object): | |
120 """A PathContext is used to carry the information used to construct URLs and | |
prasadv
2016/08/30 22:12:21
Single line doc string.
miimnk
2016/09/01 00:49:11
Done.
| |
121 paths when dealing with the storage server and archives.""" | |
122 def __init__(self, original_gs_url, repackage_gs_url, | |
123 archive, revision_file=REVISION_MAP_FILE): | |
124 super(PathContext, self).__init__() | |
125 self.original_gs_url = original_gs_url | |
126 self.repackage_gs_url = repackage_gs_url | |
127 self.archive = archive | |
128 self.builder_name = BUILDER_NAME[archive] | |
129 self.file_prefix = ARCHIVE_PREFIX[archive] | |
130 self.revision_file = revision_file | |
131 | |
132 | |
133 def get_cp_from_hash(git_hash): | |
134 """Converts a GitHash to Commit position number""" | |
135 json_url = CHROMIUM_GITHASH_TO_SVN_URL % git_hash | |
136 response = urllib.urlopen(json_url) | |
137 if response.getcode() == 200: | |
138 try: | |
139 data = json.loads(response.read()) | |
140 except ValueError: | |
141 logging.warning('ValueError for JSON URL: %s' % json_url) | |
142 raise ValueError('Failed to extract commit position') | |
143 else: | |
144 logging.warning('ValueError for JSON URL: %s' % json_url) | |
145 raise ValueError('Failed to extract commit position') | |
146 if 'number' in data: | |
147 return data['number'] | |
148 logging.warning('Failed to get svn revision number for %s' % git_hash) | |
149 raise ValueError('Failed to extract commit position') | |
150 | |
151 def create_cp_from_hash_map(hash_list): | |
152 """Creates a dictionary that maps from Commit positio number | |
153 to corresponding GitHash""" | |
154 hash_map = {} | |
155 count = 0 | |
156 for git_hash in hash_list: | |
157 try: | |
158 cp_num = get_cp_from_hash(git_hash) | |
159 hash_map[cp_num] = git_hash | |
160 print "Downloaded %s / %s git hash" %(count, len(hash_list)) | |
161 count += 1 | |
162 except ValueError: | |
163 pass | |
164 return hash_map | |
165 | |
166 def get_list_of_suffix(bucket_address, prefix, filter_function): | |
167 """Gets the list of suffixes in files in a google storage bucket. | |
168 For example, a google storage bucket containing one file | |
169 'full-build-linux_20983' will return ['20983'] if prefix is | |
170 provided as 'full-build-linux'. Google Storage bucket | |
171 containing multiple files will return multiple suffixes. """ | |
172 file_list = bisect_repackage_utils.GSutilList(bucket_address) | |
173 suffix_list = [] | |
174 extract_suffix = '.*?%s_(.*?)\.zip' %(prefix) | |
175 for file in file_list: | |
176 match = re.match(extract_suffix, file) | |
177 if match and filter_function(match.groups()[0]): | |
178 suffix_list.append(match.groups()[0]) | |
179 return suffix_list | |
180 | |
181 def download_build(cp_num, revision_map, zip_file_name, context): | |
182 """ Download a single build corresponding to the cp_num and context.""" | |
183 file_url = '%s/%s/%s_%s.zip' %(context.original_gs_url, context.builder_name, | |
184 context.file_prefix, revision_map[cp_num]) | |
185 bisect_repackage_utils.GSUtilDownloadFile(file_url, zip_file_name) | |
186 | |
187 def upload_build(zip_file, context): | |
188 """Uploads a single build in zip_file to the repackage_gs_url in context.""" | |
189 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) | |
190 upload_url = gs_base_url + '/' | |
191 bisect_repackage_utils.GSUtilCopy(zip_file, upload_url) | |
192 | |
193 def download_revision_map(context): | |
194 """Downloads the revision map in original_gs_url in context.""" | |
195 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) | |
196 download_url = gs_base_url + '/' + context.revision_file | |
197 bisect_repackage_utils.GSUtilDownloadFile(download_url, | |
198 context.revision_file) | |
199 | |
200 def get_revision_map(context): | |
201 """Downloads and returns the revision map in repackage_gs_url in context.""" | |
202 bisect_repackage_utils.RemoveFile(context.revision_file) | |
203 download_revision_map(context) | |
204 with open(context.revision_file, 'r') as revision_file: | |
205 revision_map = json.load(revision_file) | |
206 bisect_repackage_utils.RemoveFile(context.revision_file) | |
207 return revision_map | |
208 | |
209 def upload_revision_map(revision_map, context): | |
210 """Upload the given revision_map to the repackage_gs_url in context.""" | |
211 with open(context.revision_file, 'w') as revision_file: | |
212 json.dump(revision_map, revision_file) | |
213 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) | |
214 upload_url = gs_base_url + '/' | |
215 bisect_repackage_utils.GSUtilCopy(context.revision_file, upload_url) | |
216 bisect_repackage_utils.RemoveFile(context.revision_file) | |
217 | |
218 def create_upload_revision_map(context): | |
219 """ Creates and uploads a dictionary that maps from GitHash to CP number.""" | |
220 gs_base_url = '%s/%s' %(context.original_gs_url, context.builder_name) | |
221 hash_list = get_list_of_suffix(gs_base_url, context.file_prefix, | |
222 bisect_repackage_utils.isGitCommitHash) | |
223 cp_num_to_hash_map = create_cp_from_hash_map(hash_list) | |
224 upload_revision_map(cp_num_to_hash_map, context) | |
225 | |
226 def update_upload_revision_map(context): | |
227 """ Updates and uploads a dictionary that maps from GitHash to CP number.""" | |
228 gs_base_url = '%s/%s' %(context.original_gs_url, context.builder_name) | |
229 revision_map = get_revision_map(context) | |
230 hash_list = get_list_of_suffix(gs_base_url, context.file_prefix, | |
231 bisect_repackage_utils.isGitCommitHash) | |
232 hash_list = list(set(hash_list)-set(revision_map.values())) | |
233 cp_num_to_hash_map = create_cp_from_hash_map(hash_list) | |
234 merged_dict = dict(cp_num_to_hash_map.items() + revision_map.items()) | |
235 upload_revision_map(merged_dict, context) | |
236 | |
237 def make_lightweight_archive(file_archive, archive_name, files_to_archive, | |
238 context, staging_dir): | |
239 """Repackages and strips the archive according to FILES_TO_ARCHIVE and | |
240 STRIP_LIST defined on the top.""" | |
241 strip_list = CHROME_STRIP_LIST[context.archive] | |
242 tmp_archive = os.path.join(staging_dir, 'tmp_%s' % archive_name) | |
243 (zip_file, zip_dir) = bisect_repackage_utils.MakeZip(tmp_archive, | |
244 archive_name, | |
245 files_to_archive, | |
246 file_archive, | |
247 raise_error=False, | |
248 strip_files=strip_list) | |
249 return (zip_file, zip_dir, tmp_archive) | |
250 | |
251 def remove_created_files_and_path(files, paths): | |
252 """ Removes all the files and paths passed in.""" | |
253 for file in files: | |
254 bisect_repackage_utils.RemoveFile(file) | |
255 for path in paths: | |
256 bisect_repackage_utils.RemovePath(path) | |
257 | |
258 def verify_chrome_run(zip_dir): | |
259 """This function executes chrome executable in zip_dir. Raises error if the | |
260 execution fails for any reason.""" | |
261 try: | |
262 command = [os.path.join(zip_dir, 'chrome')] | |
263 code = bisect_repackage_utils.RunCommand(command) | |
264 if code != 0: | |
265 raise ValueError("Executing Chrome Failed: Need to check ") | |
266 except Exception, e: | |
267 raise ValueError("Executing Chrome Failed: Need to check ") | |
268 | |
269 | |
270 def get_whitelist_files(extracted_folder, archive): | |
271 """Gets all the files & directories matching whitelisted regex.""" | |
272 whitelist_files = [] | |
273 all_files = os.listdir(extracted_folder) | |
274 for file in all_files: | |
275 if re.match(CHROME_WHITELIST_FILES[archive], file): | |
276 whitelist_files.append(file) | |
277 return whitelist_files | |
278 | |
279 | |
280 def repackage_single_revision(revision_map, isForwardArchive, verify_run, | |
281 staging_dir, context, cp_num): | |
282 """Repackages a single Chrome build for manual bisect.""" | |
283 archive_name = '%s_%s' %(context.file_prefix, cp_num) | |
284 file_archive = os.path.join(staging_dir, archive_name) | |
285 zip_file_name = '%s.zip' % (file_archive) | |
286 download_build(cp_num, revision_map, zip_file_name, context) | |
287 extract_dir = os.path.join(staging_dir, archive_name) | |
288 bisect_repackage_utils.ExtractZip(zip_file_name, extract_dir) | |
289 extracted_folder = os.path.join(extract_dir, context.file_prefix) | |
290 if CHROME_WHITELIST_FILES[context.archive]: | |
291 whitelist_files = get_whitelist_files(extracted_folder, context.archive) | |
292 files_to_include = whitelist_files + CHROME_REQUIRED_FILES[context.archive] | |
293 else: | |
294 files_to_include = CHROME_REQUIRED_FILES[context.archive] | |
295 (zip_dir, zip_file, tmp_archive) = make_lightweight_archive(extracted_folder, | |
296 archive_name, | |
297 files_to_include, context, | |
298 staging_dir) | |
299 | |
300 if verify_run: | |
301 verify_chrome_run(zip_dir) | |
302 | |
303 upload_build(zip_file, context) | |
304 # Removed temporary files created during repackaging process. | |
305 remove_created_files_and_path([zip_file_name], | |
306 [zip_dir, extract_dir, tmp_archive]) | |
307 | |
308 def repackage_revisions(revisions, revision_map, isForwardArchive, verify_run, | |
309 staging_dir, context, quit_event=None, | |
310 progress_event=None): | |
311 """ Repackages all Chrome builds listed in revisions. This function calls | |
312 'repackage_single_revision' with multithreading pool.'""" | |
313 p = Pool(3) | |
314 func = partial(repackage_single_revision, revision_map, isForwardArchive, | |
315 verify_run, staging_dir, context) | |
316 p.imap(func, revisions) | |
317 p.close() | |
318 p.join() | |
319 | |
320 def get_uploaded_builds(context): | |
321 """Returns already uploaded commit positions numbers in | |
322 context.repackage_gs_url""" | |
323 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) | |
324 return get_list_of_suffix(gs_base_url, context.file_prefix, | |
325 bisect_repackage_utils.isCommitPosition) | |
326 | |
327 def get_revisions_to_package(revision_map, context): | |
328 """ Returns revisions that need to be repackaged. The first half of | |
329 the revisions will be sorted in ascending order and the second half of | |
330 the revisions will be sorted in desending order. | |
331 | |
332 The first half will be used for repackaging revisions in forward direction, | |
333 and the second half will be used for repackaging revisions in backward | |
334 direction.""" | |
335 already_packaged = get_uploaded_builds(context) | |
336 not_already_packaged = list(set(revision_map.keys())-set(already_packaged)) | |
337 revisions_to_package = sorted(not_already_packaged) | |
338 revisions_to_package = filter(lambda a: a != 'null', revisions_to_package) | |
339 | |
340 forward_rev = revisions_to_package[:len(revisions_to_package)/2] | |
341 backward_rev = sorted(revisions_to_package[len(revisions_to_package)/2:], | |
342 reverse=True) | |
343 return (forward_rev, backward_rev) | |
344 | |
345 class RepackageJob(object): | |
346 def __init__(self, name, revisions_to_package, revision_map, isForwardArchive, | |
347 verify_run, staging_dir, context): | |
348 super(RepackageJob, self).__init__() | |
349 self.name = name | |
350 self.revisions_to_package = revisions_to_package | |
351 self.revision_map = revision_map | |
352 self.isForwardArchive = isForwardArchive | |
353 self.verify_run = verify_run | |
354 self.staging_dir = staging_dir | |
355 self.context = context | |
356 self.quit_event = threading.Event() | |
357 self.progress_event = threading.Event() | |
358 self.thread = None | |
359 | |
360 def Start(self): | |
361 """Starts the download.""" | |
362 fetchargs = (self.revisions_to_package, | |
363 self.revision_map, | |
364 self.isForwardArchive, | |
365 self.verify_run, | |
366 self.staging_dir, | |
367 self.context, | |
368 self.quit_event, | |
369 self.progress_event) | |
370 self.thread = threading.Thread(target=repackage_revisions, | |
371 name=self.name, | |
372 args=fetchargs) | |
373 self.thread.start() | |
374 | |
375 def Stop(self): | |
376 """Stops the download which must have been started previously.""" | |
377 assert self.thread, 'DownloadJob must be started before Stop is called.' | |
378 self.quit_event.set() | |
379 self.thread.join() | |
380 | |
381 def WaitFor(self): | |
382 """Prints a message and waits for the download to complete. The download | |
383 must have been started previously.""" | |
384 assert self.thread, 'DownloadJob must be started before WaitFor is called.' | |
385 self.progress_event.set() # Display progress of download. def Stop(self): | |
386 assert self.thread, 'DownloadJob must be started before Stop is called.' | |
387 self.quit_event.set() | |
388 self.thread.join() | |
389 | |
390 def main(argv): | |
391 option_parser = optparse.OptionParser() | |
392 | |
393 choices = ['mac', 'win32', 'win64', 'linux'] | |
394 | |
395 option_parser.add_option('-a', '--archive', | |
396 choices=choices, | |
397 help='Builders to repacakge from [%s].' % | |
398 '|'.join(choices)) | |
399 | |
400 # Verifies that the chrome executable runs | |
401 option_parser.add_option('-v', '--verify', | |
402 action='store_true', | |
403 help='Verifies that the Chrome executes normally' | |
404 'without errors') | |
405 | |
406 # This option will update the revision map. | |
407 option_parser.add_option('-u', '--update', | |
408 action='store_true', | |
409 help='Updates the list of revisions to repackage') | |
410 | |
411 # This option will creates the revision map. | |
412 option_parser.add_option('-c', '--create', | |
413 action='store_true', | |
414 help='Creates the list of revisions to repackage') | |
415 | |
416 # Original bucket that contains perf builds | |
417 option_parser.add_option('-o', '--original', | |
418 type='str', | |
419 help='Google storage url containing original Chrome builds') | |
420 | |
421 # Bucket that should archive lightweight perf builds | |
422 option_parser.add_option('-r', '--repackage', | |
423 type='str', | |
424 help='Google storage url to re-archive Chrome builds') | |
425 | |
426 verify_run = False | |
427 (opts, args) = option_parser.parse_args() | |
428 if opts.archive is None: | |
429 print 'Error: missing required parameter: --archive' | |
430 parser.print_help() | |
431 return 1 | |
432 if not opts.original or not opts.repackage: | |
433 raise ValueError('Need to specify original gs bucket url and' | |
434 'repackage gs bucket url') | |
435 context = PathContext(opts.original, opts.repackage, opts.archive) | |
436 | |
437 if opts.create: | |
438 create_upload_revision_map(context) | |
439 | |
440 if opts.update: | |
441 update_upload_revision_map(context) | |
442 | |
443 if opts.verify: | |
444 verify_run = True | |
445 | |
446 revision_map = get_revision_map(context) | |
447 (forward_rev, backward_rev) = get_revisions_to_package(revision_map, context) | |
448 base_dir = os.path.join('.', context.archive) | |
449 # Clears any uncleared staging directories and create one | |
450 bisect_repackage_utils.RemovePath(base_dir) | |
451 bisect_repackage_utils.MaybeMakeDirectory(base_dir) | |
452 staging_dir = os.path.abspath(tempfile.mkdtemp(prefix='staging_fwd', | |
453 dir=base_dir)) | |
454 staging_dir_bwd = os.path.abspath(tempfile.mkdtemp(prefix='staging_bwd', | |
455 dir=base_dir)) | |
456 backward_fetch = RepackageJob('backward_fetch', backward_rev, revision_map, | |
457 False, verify_run, staging_dir_bwd, | |
458 context) | |
459 try: | |
460 # Only run backward fetch | |
461 backward_fetch.Start() | |
462 backward_fetch.WaitFor() | |
463 except (KeyboardInterrupt, SystemExit): | |
464 print 'Cleaning up...' | |
465 bisect_repackage_utils.RemovePath(staging_dir) | |
466 bisect_repackage_utils.RemovePath(staging_dir_bwd) | |
467 print 'Cleaning up...' | |
468 bisect_repackage_utils.RemovePath(staging_dir) | |
469 bisect_repackage_utils.RemovePath(staging_dir_bwd) | |
470 | |
471 if '__main__' == __name__: | |
472 sys.exit(main(sys.argv)) | |
OLD | NEW |