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