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 | |
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 in Linux. (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 FILES_TO_ARCHIVE = [ | |
17 'chrome', | |
18 'chrome_100_percent.pak', | |
19 'chrome_200_percent.pak', | |
20 'default_apps', | |
21 'icudtl.dat', | |
22 'libwidevinecdm.so', | |
23 'locales', | |
24 'nacl_helper', | |
25 'nacl_helper_bootstrap', | |
26 'nacl_irt_x86_64.nexe', | |
27 'natives_blob.bin', | |
28 'PepperFlash', | |
29 'product_logo_48.png', | |
30 'resources.pak', | |
31 'snapshot_blob.bin', | |
32 'xdg-mime', | |
33 'xdg-settings', | |
34 ] | |
35 | |
36 # Declares what files should be stripped of symbols to reduce the archive size. | |
37 STRIP_LIST = [ | |
38 'chrome', | |
39 'nacl_helper' | |
40 ] | |
41 | |
42 # API to convert Githash to Commit position number. | |
43 CHROMIUM_GITHASH_TO_SVN_URL = ( | |
44 'https://cr-rev.appspot.com/_ah/api/crrev/v1/commit/%s') | |
45 | |
46 BUILDER_NAME = 'Linux Builder' | |
47 ARCHIVE_PREFIX = 'full-build-linux' | |
48 REVISION_MAP_FILE = 'revision_map.json' | |
49 | |
50 ################################################################################ | |
51 import os | |
52 import bisect_repackage_utils | |
53 import zipfile | |
54 import zlib | |
55 import json | |
56 import sys | |
57 import optparse | |
58 import re | |
59 import urllib | |
60 import threading | |
61 from multiprocessing import Pool | |
62 from functools import partial | |
63 | |
64 # Declares where files should be stored temporarily for staging. | |
65 # TODO (miimnk): use tempfile.mkdtemp to make temporary folders | |
66 STAGING_DIR= os.path.join('.', 'a', 'tmp_forward') | |
dimu
2016/08/19 04:14:05
is there any cleanup for the staging dir?
miimnk
2016/08/19 20:29:43
Erased the two global variables and used 'tempfile
| |
67 STAGING_DIR_BACKWARD = os.path.join('.', 'a', 'tmp_backward') | |
68 | |
69 | |
70 class PathContext(object): | |
71 """A PathContext is used to carry the information used to construct URLs and | |
72 paths when dealing with the storage server and archives.""" | |
73 def __init__(self, original_gs_url, repackage_gs_url, | |
74 builder_name=BUILDER_NAME, file_prefix=ARCHIVE_PREFIX, | |
75 revision_map=REVISION_MAP_FILE): | |
76 super(PathContext, self).__init__() | |
77 self.original_gs_url = original_gs_url | |
78 self.repackage_gs_url = repackage_gs_url | |
79 self.builder_name = builder_name | |
80 self.file_prefix = file_prefix | |
81 self.revision_map = revision_map | |
82 | |
83 | |
84 def get_cp_from_hash(git_hash): | |
85 """Converts a GitHash to Commit position number""" | |
86 json_url = CHROMIUM_GITHASH_TO_SVN_URL % git_hash | |
87 response = urllib.urlopen(json_url) | |
88 if response.getcode() == 200: | |
89 try: | |
90 data = json.loads(response.read()) | |
91 except ValueError: | |
92 print 'ValueError for JSON URL: %s' % json_url | |
dimu
2016/08/19 04:14:05
It would be better to use logging instead of print
miimnk
2016/08/19 20:29:43
Done.
| |
93 return None | |
dimu
2016/08/19 04:14:05
throw exception for failure, and use try/catch in
miimnk
2016/08/19 20:29:43
Done.
| |
94 else: | |
95 print 'ValueError for JSON URL: %s' % json_url | |
96 return None | |
97 if 'number' in data: | |
98 return data['number'] | |
99 print 'Failed to get svn revision number for %s' % git_hash | |
100 return None | |
101 | |
102 def create_cp_from_hash_map(hash_list): | |
103 """Creates a dictionary that maps from Commit positio number | |
104 to corresponding GitHash""" | |
105 hash_map = {} | |
106 count = 0 | |
107 for git_hash in hash_list: | |
108 cp_num = get_cp_from_hash(git_hash) | |
109 hash_map[cp_num] = git_hash | |
110 print "Downloaded %s / %s git hash" %(count, len(hash_list)) | |
111 count += 1 | |
112 return hash_map | |
113 | |
114 def isProperHash(regex_match): | |
dimu
2016/08/19 04:14:05
move it to utils
dimu
2016/08/19 04:14:05
isGitCommitHash
miimnk
2016/08/19 20:29:42
Done.
miimnk
2016/08/19 20:29:43
Done.
| |
115 """Checks if match is correct SHA1 hash""" | |
116 if len(regex_match) == 40: return True | |
dimu
2016/08/19 04:14:05
use re.match(r"^[0-9,A-F]{40}$", regex_match.upper
miimnk
2016/08/19 20:29:43
Done.
| |
117 return False | |
118 | |
119 def isProperRevision(regex_match): | |
dimu
2016/08/19 04:14:05
move this to utils
miimnk
2016/08/19 20:29:43
Done.
| |
120 """Checks if match is correct revision(Cp number) format""" | |
121 if len(regex_match) == 6: return True | |
dimu
2016/08/19 04:14:05
also use regex
miimnk
2016/08/19 20:29:43
Done.
| |
122 return False | |
123 | |
124 def get_list_of_suffix(bucket_address, prefix, filter_function): | |
125 """Gets the list of suffixes in files in a google storage bucket. | |
126 For example, a google storage bucket containing one file | |
127 'full-build-linux_20983' will return ['20983'] if prefix is | |
128 provided as 'full-build-linux'. Google Storage bucket | |
129 containing multiple files will return multiple suffixes. """ | |
130 file_list = bisect_repackage_utils.GSutilList(bucket_address) | |
131 suffix_list = [] | |
132 extract_suffix = '.*?%s_(.*?)\.zip' %(prefix) | |
133 for file in file_list: | |
134 match = re.match(extract_suffix, file) | |
135 if match and filter_function(match.groups()[0]): | |
136 suffix_list.append(match.groups()[0]) | |
137 return suffix_list | |
138 | |
139 def download_build(cp_num, revision_map, zip_file_name, context): | |
140 """ Download a single build corresponding to the cp_num and context.""" | |
141 file_url = '%s/%s/%s_%s.zip' %(context.original_gs_url, context.builder_name, | |
142 context.file_prefix, revision_map[cp_num]) | |
143 bisect_repackage_utils.GSUtilDownloadFile(file_url, zip_file_name) | |
144 | |
145 def upload_build(zip_file, context): | |
146 """Uploads a single build in zip_file to the repackage_gs_url in context.""" | |
147 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) | |
148 upload_url = gs_base_url + '/' | |
149 bisect_repackage_utils.GSUtilCopy(zip_file, upload_url) | |
150 | |
151 def download_revision_map(context): | |
152 """Downloads the revision map in original_gs_url in context.""" | |
153 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) | |
154 download_url = gs_base_url + '/' + context.revision_map | |
155 try: | |
156 bisect_repackage_utils.GSUtilDownloadFile(download_url, | |
157 context.revision_map) | |
158 except Exception, e: | |
159 raise ValueError('Revision map does not exist. Re run the program with' | |
160 '-c option to upload a revision map to google storage') | |
161 | |
162 def get_revision_map(context): | |
163 """Downloads and returns the revision map in repackage_gs_url in context.""" | |
164 bisect_repackage_utils.RemoveFile(context.revision_map) | |
165 download_revision_map(context) | |
166 with open(context.revision_map, 'r') as f: | |
dimu
2016/08/19 04:14:05
nit: better naming: revision_file
miimnk
2016/08/19 20:29:43
Done.
| |
167 revision_map = json.load(f) | |
168 bisect_repackage_utils.RemoveFile(context.revision_map) | |
169 return revision_map | |
170 | |
171 def upload_revision_map(revision_map, context): | |
172 """Upload the given revision_map to the repackage_gs_url in context.""" | |
173 with open(context.revision_map, 'w') as fp: | |
174 json.dump(revision_map, fp) | |
175 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) | |
176 upload_url = gs_base_url + '/' | |
177 bisect_repackage_utils.GSUtilCopy(context.revision_map, upload_url) | |
178 bisect_repackage_utils.RemoveFile(context.revision_map) | |
179 | |
180 def create_upload_revision_map(context): | |
181 """ Creates and uploads a dictionary that maps from GitHash to CP number.""" | |
182 gs_base_url = '%s/%s' %(context.original_gs_url, context.builder_name) | |
183 hash_list = get_list_of_suffix(gs_base_url, context.file_prefix, isProperHash) | |
184 cp_num_to_hash_map = create_cp_from_hash_map(hash_list) | |
185 upload_revision_map(cp_num_to_hash_map, context) | |
186 | |
187 def update_upload_revision_map(context): | |
188 """ Updates and uploads a dictionary that maps from GitHash to CP number.""" | |
189 gs_base_url = '%s/%s' %(context.original_gs_url, context.builder_name) | |
190 revision_map = get_revision_map(context) | |
191 hash_list = get_list_of_suffix(gs_base_url, context.file_prefix, isProperHash) | |
192 hash_list = list(set(hash_list)-set(revision_map.values())) | |
193 cp_num_to_hash_map = create_cp_from_hash_map(hash_list) | |
194 upload_revision_map(cp_num_to_hash_map, context) | |
195 | |
196 def make_filtered_archive(file_archive, archive_name): | |
dimu
2016/08/19 04:14:05
better naming
miimnk
2016/08/19 20:29:43
Done. Changed to 'make_lightweight_archive'.
| |
197 """Repackages and strips the archive according to FILES_TO_ARCHIVE and | |
198 STRIP_LIST defined on the top.""" | |
199 return bisect_repackage_utils.MakeZip('.', | |
200 archive_name, | |
201 FILES_TO_ARCHIVE, | |
202 file_archive, | |
203 raise_error=False, | |
204 strip_files=STRIP_LIST) | |
205 | |
206 def remove_created_files_and_path(files, paths): | |
207 """ Removes all the files and paths passed in.""" | |
208 for file in files: | |
209 bisect_repackage_utils.RemoveFile(file) | |
210 for path in paths: | |
211 bisect_repackage_utils.RemovePath(path) | |
212 | |
213 def verify_chrome_run(zip_dir): | |
214 """This function executes chrome executable in zip_dir. Raises error if the | |
215 execution fails for any reason.""" | |
216 try: | |
217 command = [os.path.join(zip_dir, 'chrome')] | |
218 code = bisect_repackage_utils.RunCommand(command) | |
219 if code != 0: | |
220 raise ValueError("Executing Chrome Failed: Need to check ") | |
221 except Exception, e: | |
222 raise ValueError("Executing Chrome Failed: Need to check ") | |
223 | |
224 def repackage_single_revision(revision_map, isForwardArchive, verify_run, | |
225 staging_dir, context, cp_num): | |
226 """Repackages a single Chrome build for manual bisect.""" | |
227 archive_name = '%s_%s' %(context.file_prefix, cp_num) | |
228 file_archive = os.path.join(staging_dir, archive_name) | |
229 zip_file_name = '%s.zip' %file_archive | |
230 | |
231 bisect_repackage_utils.RemoveFile(zip_file_name) | |
232 download_build(cp_num, revision_map, zip_file_name, context) | |
233 extract_dir = os.path.join(staging_dir, archive_name) | |
234 bisect_repackage_utils.ExtractZip(zip_file_name, extract_dir) | |
235 extracted_folder = os.path.join(extract_dir, context.file_prefix) | |
236 | |
237 (zip_dir, zip_file) = make_filtered_archive(extracted_folder, archive_name) | |
238 | |
239 if verify_run: | |
240 verify_chrome_run(zip_dir) | |
241 | |
242 upload_build(zip_file, context) | |
243 # Removed temporary files created during repackaging process. | |
244 remove_created_files_and_path([zip_file, zip_file_name], | |
245 [zip_dir, extract_dir]) | |
246 | |
247 def repackage_revisions(revisions, revision_map, isForwardArchive, verify_run, | |
248 staging_dir, context, quit_event=None, | |
249 progress_event=None): | |
250 """ Repackages all Chrome builds listed in revisions. This function calls | |
251 'repackage_single_revision' with multithreading pool.'""" | |
252 p = Pool(3) | |
253 func = partial(repackage_single_revision, revision_map, isForwardArchive, | |
254 verify_run, staging_dir, context) | |
255 p.imap(func, revisions) | |
256 p.close() | |
257 p.join() | |
258 | |
259 def get_uploaded_builds(context): | |
260 """Returns already uploaded commit positions numbers in | |
261 context.repackage_gs_url""" | |
262 gs_base_url = '%s/%s' %(context.repackage_gs_url, context.builder_name) | |
263 return get_list_of_suffix(gs_base_url, context.file_prefix, isProperRevision) | |
264 | |
265 def get_revisions_to_package(revision_map, context): | |
266 """ Returns revisions that need to be repackaged. The first half of | |
267 the revisions will be sorted in ascending order and the second half of | |
268 the revisions will be sorted in desending order. | |
269 | |
270 The first half will be used for repackaging revisions in forward direction, | |
271 and the second half will be used for repackaging revisions in backward | |
272 direction.""" | |
273 already_packaged = get_uploaded_builds(context) | |
274 not_already_packaged = list(set(revision_map.keys())-set(already_packaged)) | |
275 revisions_to_package = sorted(not_already_packaged) | |
276 revisions_to_package = filter(lambda a: a != 'null', revisions_to_package) | |
277 | |
278 forward_rev = revisions_to_package[:len(revisions_to_package)/2] | |
279 backward_rev = sorted(revisions_to_package[len(revisions_to_package)/2:], | |
280 reverse=True) | |
281 return (forward_rev, backward_rev) | |
282 | |
283 class RepackageJob(object): | |
284 def __init__(self, name, revisions_to_package, revision_map, isForwardArchive, | |
285 verify_run, staging_dir, context): | |
286 super(RepackageJob, self).__init__() | |
287 self.name = name | |
288 self.revisions_to_package = revisions_to_package | |
289 self.revision_map = revision_map | |
290 self.isForwardArchive = isForwardArchive | |
291 self.verify_run = verify_run | |
292 self.staging_dir = staging_dir | |
293 self.context = context | |
294 self.quit_event = threading.Event() | |
295 self.progress_event = threading.Event() | |
296 self.thread = None | |
297 | |
298 def Start(self): | |
299 """Starts the download.""" | |
300 fetchargs = (self.revisions_to_package, | |
301 self.revision_map, | |
302 self.isForwardArchive, | |
303 self.verify_run, | |
304 self.staging_dir, | |
305 self.context, | |
306 self.quit_event, | |
307 self.progress_event) | |
308 self.thread = threading.Thread(target=repackage_revisions, | |
309 name=self.name, | |
310 args=fetchargs) | |
311 self.thread.start() | |
312 | |
313 def Stop(self): | |
314 """Stops the download which must have been started previously.""" | |
315 assert self.thread, 'DownloadJob must be started before Stop is called.' | |
316 self.quit_event.set() | |
317 self.thread.join() | |
318 | |
319 def WaitFor(self): | |
320 """Prints a message and waits for the download to complete. The download | |
321 must have been started previously.""" | |
322 assert self.thread, 'DownloadJob must be started before WaitFor is called.' | |
323 self.progress_event.set() # Display progress of download. def Stop(self): | |
324 assert self.thread, 'DownloadJob must be started before Stop is called.' | |
325 self.quit_event.set() | |
326 self.thread.join() | |
327 | |
328 def main(argv): | |
329 option_parser = optparse.OptionParser() | |
330 | |
331 # Verifies that the chrome executable runs | |
332 option_parser.add_option('-v', '--verify', | |
333 action='store_true', | |
334 help='Verifies that the Chrome executes normally' | |
335 'without errors') | |
336 | |
337 # This option will update the revision map. | |
338 option_parser.add_option('-u', '--update', | |
339 action='store_true', | |
340 help='Updates the list of revisions to repackage') | |
341 | |
342 # This option will creates the revision map. | |
343 option_parser.add_option('-c', '--create', | |
344 action='store_true', | |
345 help='Creates the list of revisions to repackage') | |
346 | |
347 # Original bucket that contains perf builds | |
348 option_parser.add_option('-o', '--original', | |
349 type='str', | |
350 help='Google storage url containing original Chrome builds') | |
351 | |
352 # Bucket that should archive lightweight perf builds | |
353 option_parser.add_option('-r', '--repackage', | |
354 type='str', | |
355 help='Google storage url to re-archive Chrome builds') | |
356 | |
357 verify_run = False | |
358 (opts, args) = option_parser.parse_args() | |
359 if not opts.original or not opts.repackage: | |
360 raise ValueError('Need to specify original gs bucket url and' | |
361 'repackage gs bucket url') | |
362 context = PathContext(opts.original, opts.repackage) | |
363 | |
364 if opts.create: | |
365 create_upload_revision_map(context) | |
366 | |
367 if opts.update: | |
368 update_upload_revision_map(context) | |
369 | |
370 if opts.verify: | |
371 verify_run = True | |
372 | |
373 revision_map = get_revision_map(context) | |
374 (forward_rev, backward_rev) = get_revisions_to_package(revision_map, context) | |
375 bisect_repackage_utils.MaybeMakeDirectory(STAGING_DIR) | |
376 bisect_repackage_utils.MaybeMakeDirectory(STAGING_DIR_BACKWARD) | |
377 | |
378 # Spwan two threads: One repackaging from oldest builds. The other repackaging | |
379 # from the newest builds. | |
380 forward_fetch = RepackageJob('forward_fetch', forward_rev, revision_map, True, | |
381 verify_run, os.path.abspath(STAGING_DIR), | |
382 context) | |
383 backward_fetch = RepackageJob('backward_fetch', backward_rev, revision_map, | |
384 False, verify_run, | |
385 os.path.abspath(STAGING_DIR_BACKWARD), | |
386 context) | |
387 forward_fetch.Start() | |
388 backward_fetch.Start() | |
389 forward_fetch.WaitFor() | |
390 backward_fetch.WaitFor() | |
391 | |
392 if '__main__' == __name__: | |
393 sys.exit(main(sys.argv)) | |
OLD | NEW |