OLD | NEW |
| (Empty) |
1 #!/usr/bin/env python | |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
6 '''Utility to update the SDK manifest file in the build_tools directory''' | |
7 | |
8 | |
9 import optparse | |
10 import os | |
11 import re | |
12 import string | |
13 import subprocess | |
14 import sys | |
15 import urllib2 | |
16 | |
17 # Create the various paths of interest | |
18 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) | |
19 SDK_SRC_DIR = os.path.dirname(os.path.dirname(SCRIPT_DIR)) | |
20 SRC_DIR = os.path.dirname(os.path.dirname(SDK_SRC_DIR)) | |
21 NACL_DIR = os.path.join(SRC_DIR, 'native_client') | |
22 | |
23 sys.path.append(os.path.join(SDK_SRC_DIR, 'tools')) | |
24 sys.path.append(os.path.join(NACL_DIR, 'build')) | |
25 | |
26 import sdk_update | |
27 | |
28 HELP = '''"Usage: %prog [-b bundle] [options]" | |
29 | |
30 Actions for particular bundles: | |
31 sdk_tools: Upload the most recently built nacl_sdk.zip and sdk_tools.tgz | |
32 files to the server and update the manifest file | |
33 pepper_??: Download the latest pepper builds off the appropriate branch, | |
34 upload these files to the appropriate location on the server, and | |
35 update the manifest file. | |
36 <others>: Only update manifest file -- you'll need to upload the file yourself | |
37 ''' | |
38 | |
39 # Map option keys to manifest attribute key. Option keys are used to retrieve | |
40 # option values from cmd-line options. Manifest attribute keys label the | |
41 # corresponding value in the manifest object. | |
42 OPTION_KEY_MAP = { | |
43 # option key manifest attribute key | |
44 'bundle_desc_url': 'desc_url', | |
45 'bundle_revision': sdk_update.REVISION_KEY, | |
46 'bundle_version': sdk_update.VERSION_KEY, | |
47 'desc': 'description', | |
48 'recommended': 'recommended', | |
49 'stability': 'stability', | |
50 } | |
51 # Map options keys to platform key, as stored in the bundle. | |
52 OPTION_KEY_TO_PLATFORM_MAP = { | |
53 'mac_arch_url': 'mac', | |
54 'win_arch_url': 'win', | |
55 'linux_arch_url': 'linux', | |
56 'all_arch_url': 'all', | |
57 } | |
58 | |
59 NACL_SDK_ROOT = os.path.dirname(os.path.dirname(os.path.dirname( | |
60 os.path.abspath(__file__)))) | |
61 | |
62 BUILD_TOOLS_OUT = os.path.join(NACL_SDK_ROOT, 'scons-out', 'build', 'obj', | |
63 'build_tools') | |
64 | |
65 BUNDLE_SDK_TOOLS = 'sdk_tools' | |
66 BUNDLE_PEPPER_MATCHER = re.compile('^pepper_([0-9]+)$') | |
67 IGNORE_OPTIONS = set([ | |
68 'archive_id', 'gsutil', 'manifest_file', 'upload', 'root_url']) | |
69 | |
70 | |
71 class Error(Exception): | |
72 '''Generic error/exception for update_manifest module''' | |
73 pass | |
74 | |
75 | |
76 def UpdateBundle(bundle, options): | |
77 ''' Update the bundle per content of the options. | |
78 | |
79 Args: | |
80 options: options data. Attributes that are used are also deleted from | |
81 options.''' | |
82 # Check, set and consume individual bundle options. | |
83 for option_key, attribute_key in OPTION_KEY_MAP.iteritems(): | |
84 option_val = getattr(options, option_key, None) | |
85 if option_val is not None: | |
86 bundle[attribute_key] = option_val | |
87 delattr(options, option_key) | |
88 # Validate what we have so far; we may just avoid going through a lengthy | |
89 # download, just to realize that some other trivial stuff is missing. | |
90 bundle.Validate() | |
91 # Check and consume archive-url options. | |
92 for option_key, host_os in OPTION_KEY_TO_PLATFORM_MAP.iteritems(): | |
93 platform_url = getattr(options, option_key, None) | |
94 if platform_url is not None: | |
95 bundle.UpdateArchive(host_os, platform_url) | |
96 delattr(options, option_key) | |
97 | |
98 | |
99 class UpdateSDKManifest(sdk_update.SDKManifest): | |
100 '''Adds functions to SDKManifest that are only used in update_manifest''' | |
101 | |
102 def _ValidateBundleName(self, name): | |
103 ''' Verify that name is a valid bundle. | |
104 | |
105 Args: | |
106 name: the proposed name for the bundle. | |
107 | |
108 Return: | |
109 True if the name is valid for a bundle, False otherwise.''' | |
110 valid_char_set = '()-_.%s%s' % (string.ascii_letters, string.digits) | |
111 name_len = len(name) | |
112 return (name_len > 0 and all(c in valid_char_set for c in name)) | |
113 | |
114 def _UpdateManifestVersion(self, options): | |
115 ''' Update the manifest version number from the options | |
116 | |
117 Args: | |
118 options: options data containing an attribute self.manifest_version ''' | |
119 version_num = int(options.manifest_version) | |
120 self._manifest_data['manifest_version'] = version_num | |
121 del options.manifest_version | |
122 | |
123 def _UpdateBundle(self, options): | |
124 ''' Update or setup a bundle from the options. | |
125 | |
126 Args: | |
127 options: options data containing at least a valid bundle_name | |
128 attribute. Other relevant bundle attributes will also be | |
129 used (and consumed) by this function. ''' | |
130 # Get and validate the bundle name | |
131 if not self._ValidateBundleName(options.bundle_name): | |
132 raise Error('Invalid bundle name: "%s"' % options.bundle_name) | |
133 bundle_name = options.bundle_name | |
134 del options.bundle_name | |
135 # Get the corresponding bundle, or create it. | |
136 bundle = self.GetBundle(bundle_name) | |
137 if not bundle: | |
138 bundle = sdk_update.Bundle(bundle_name) | |
139 self.SetBundle(bundle) | |
140 UpdateBundle(bundle, options) | |
141 | |
142 def _VerifyAllOptionsConsumed(self, options, bundle_name): | |
143 ''' Verify that all the options have been used. Raise an exception if | |
144 any valid option has not been used. Returns True if all options have | |
145 been consumed. | |
146 | |
147 Args: | |
148 options: the object containing the remaining unused options attributes. | |
149 bundle_name: The name of the bundle, or None if it's missing.''' | |
150 # Any option left in the list should have value = None | |
151 for key, val in options.__dict__.items(): | |
152 if val != None and key not in IGNORE_OPTIONS: | |
153 if bundle_name: | |
154 raise Error('Unused option "%s" for bundle "%s"' % (key, bundle_name)) | |
155 else: | |
156 raise Error('No bundle name specified') | |
157 return True | |
158 | |
159 def UpdateManifest(self, options): | |
160 ''' Update the manifest object with values from the command-line options | |
161 | |
162 Args: | |
163 options: options object containing attribute for the command-line options. | |
164 Note that all the non-trivial options are consumed. | |
165 ''' | |
166 # Go over all the options and update the manifest data accordingly. | |
167 # Valid options are consumed as they are used. This gives us a way to | |
168 # verify that all the options are used. | |
169 if options.manifest_version is not None: | |
170 self._UpdateManifestVersion(options) | |
171 # Keep a copy of bundle_name, which will be consumed by UpdateBundle, for | |
172 # use in _VerifyAllOptionsConsumed below. | |
173 bundle_name = options.bundle_name | |
174 if bundle_name is not None: | |
175 self._UpdateBundle(options) | |
176 self._VerifyAllOptionsConsumed(options, bundle_name) | |
177 self._ValidateManifest() | |
178 | |
179 def ValidateManifestLinks(self): | |
180 '''Validates all the links in the manifest file and throws if one is bad''' | |
181 valid = True | |
182 for bundle in self._manifest_data[sdk_update.BUNDLES_KEY]: | |
183 for archive in bundle.GetArchives(): | |
184 stream = None | |
185 try: | |
186 print "Checking size of data at link: %s" % archive.GetUrl() | |
187 stream = urllib2.urlopen(archive.GetUrl()) | |
188 server_size = int(stream.info()[sdk_update.HTTP_CONTENT_LENGTH]) | |
189 if server_size != archive.GetSize(): | |
190 sys.stderr.write('Size mismatch for %s. Expected %s but got %s\n' % | |
191 (archive.GetUrl(), archive.GetSize(), server_size)) | |
192 sys.stderr.flush() | |
193 valid = False | |
194 finally: | |
195 if stream: | |
196 stream.close() | |
197 if not valid: | |
198 raise Error('Files on server do not match the manifest file') | |
199 | |
200 | |
201 class GsUtil(object): | |
202 def __init__(self, gsutil): | |
203 '''gsutil is the path to the gsutil executable''' | |
204 self.gsutil = gsutil | |
205 self.root = 'gs://nativeclient-mirror/nacl/nacl_sdk' | |
206 | |
207 def GetURI(self, path): | |
208 '''Return the full gs:// URI for a given relative path''' | |
209 return '/'.join([self.root, path]) | |
210 | |
211 def Run(self, command): | |
212 '''Runs gsutil with a given argument list and returns exit status''' | |
213 args = [self.gsutil] + command | |
214 print 'GSUtil.Run(%s)' % args | |
215 sys.stdout.flush() | |
216 return subprocess.call(args) | |
217 | |
218 def CheckIfExists(self, path): | |
219 '''Check whether a given path exists on commondatastorage | |
220 | |
221 Args: | |
222 path: path relative to SDK root directory on the server | |
223 | |
224 Returns: True if it exists, False if it does not''' | |
225 # todo(mball): Be a little more intelligent about this check and compare | |
226 # the output strings against expected values | |
227 return self.Run(['ls', self.GetURI(path)]) == 0 | |
228 | |
229 def Copy(self, source, destination): | |
230 '''Copies a given source file to a destination path and makes it readable | |
231 | |
232 Args: | |
233 source: path to source file on local filesystem | |
234 destination: path to destination, relative to root directory''' | |
235 args = ['cp', '-a', 'public-read', source, self.GetURI(destination)] | |
236 if self.Run(args) != 0: | |
237 raise Error('Unable to copy %s to %s' % (source, destination)) | |
238 | |
239 | |
240 class UpdateSDKManifestFile(sdk_update.SDKManifestFile): | |
241 '''Adds functions to SDKManifestFile that are only used in update_manifest''' | |
242 | |
243 def __init__(self, options): | |
244 '''Create a new SDKManifest object with default contents. | |
245 | |
246 If |json_filepath| is specified, and it exists, its contents are loaded and | |
247 used to initialize the internal manifest. | |
248 | |
249 Args: | |
250 json_filepath: path to json file to read/write, or None to write a new | |
251 manifest file to stdout. | |
252 ''' | |
253 # Strip-off all the I/O-based options that do not relate to bundles | |
254 self._json_filepath = options.manifest_file | |
255 self.gsutil = GsUtil(options.gsutil) | |
256 self.options = options | |
257 self._manifest = UpdateSDKManifest() | |
258 if self._json_filepath: | |
259 self._LoadFile() | |
260 | |
261 def _HandleSDKTools(self): | |
262 '''Handles the sdk_tools bundle''' | |
263 # General sanity checking of parameters | |
264 SDK_TOOLS_FILES = ['sdk_tools.tgz', 'nacl_sdk.zip'] | |
265 options = self.options | |
266 if options.bundle_version is None: | |
267 options.bundle_version = sdk_update.MAJOR_REV | |
268 if options.bundle_version != sdk_update.MAJOR_REV: | |
269 raise Error('Specified version (%s) does not match MAJOR_REV (%s)' % | |
270 (options.bundle_version, sdk_update.MAJOR_REV)) | |
271 if options.bundle_revision is None: | |
272 options.bundle_revision = sdk_update.MINOR_REV | |
273 if options.bundle_revision != sdk_update.MINOR_REV: | |
274 raise Error('Specified revision (%s) does not match MINOR_REV (%s)' % | |
275 (options.bundle_revision, sdk_update.MINOR_REV)) | |
276 version = '%s.%s' % (options.bundle_version, options.bundle_revision) | |
277 # Update the remaining options | |
278 if options.desc is None: | |
279 options.desc = ('Native Client SDK Tools, revision %s.%s' % | |
280 (options.bundle_version, options.bundle_revision)) | |
281 options.recommended = options.recommended or 'yes' | |
282 options.stability = options.stability or 'stable' | |
283 if options.upload: | |
284 # Check whether the tools already exist | |
285 for name in SDK_TOOLS_FILES: | |
286 path = '/'.join([version, name]) | |
287 if self.gsutil.CheckIfExists(path): | |
288 raise Error('File already exists at %s' % path) | |
289 # Upload the tools files to the server | |
290 for name in SDK_TOOLS_FILES: | |
291 source = os.path.join(BUILD_TOOLS_OUT, name) | |
292 destination = '/'.join([version, name]) | |
293 self.gsutil.Copy(source, destination) | |
294 url = '/'.join([options.root_url, version, 'sdk_tools.tgz']) | |
295 options.mac_arch_url = options.mac_arch_url or url | |
296 options.linux_arch_url = options.linux_arch_url or url | |
297 options.win_arch_url = options.win_arch_url or url | |
298 | |
299 def _HandlePepper(self): | |
300 '''Handles the pepper bundles''' | |
301 options = self.options | |
302 match = BUNDLE_PEPPER_MATCHER.match(options.bundle_name) | |
303 if match is not None: | |
304 options.bundle_version = int(match.group(1)) | |
305 if options.bundle_version is None: | |
306 raise Error('Need to specify a bundle version') | |
307 if options.bundle_revision is None: | |
308 raise Error('Need to specify a bundle revision') | |
309 if options.bundle_name == 'pepper': | |
310 self.options.bundle_name = 'pepper_%s' % options.bundle_version | |
311 if options.desc is None: | |
312 options.desc = ('Chrome %s bundle, revision %s' % | |
313 (options.bundle_version, options.bundle_revision)) | |
314 root_url = options.root_url | |
315 if options.archive_id: | |
316 # Support archive names like trunk.113440 or 17.0.963.3, which is how | |
317 # the Chrome builders archive things. | |
318 root_url = '/'.join([root_url, options.archive_id]) | |
319 else: | |
320 # This is the old archive naming scheme | |
321 root_url = '%s/pepper_%s_%s' % (root_url, options.bundle_version, | |
322 options.bundle_revision) | |
323 options.mac_arch_url = '/'.join([root_url, 'naclsdk_mac.bz2']) | |
324 options.linux_arch_url = '/'.join([root_url, 'naclsdk_linux.bz2']) | |
325 options.win_arch_url = '/'.join([root_url, 'naclsdk_win.bz2']) | |
326 | |
327 def HandleBundles(self): | |
328 '''Handles known bundles by automatically uploading files''' | |
329 bundle_name = self.options.bundle_name | |
330 print 'bundle_name=' + bundle_name | |
331 if bundle_name == BUNDLE_SDK_TOOLS: | |
332 self._HandleSDKTools() | |
333 elif bundle_name.startswith('pepper'): | |
334 self._HandlePepper() | |
335 | |
336 def UpdateWithOptions(self): | |
337 ''' Update the manifest file with the given options. Create the manifest | |
338 if it doesn't already exists. Raises an Error if the manifest doesn't | |
339 validate after updating. | |
340 | |
341 Args: | |
342 options: option data''' | |
343 # UpdateManifest does not know how to deal with file-related options | |
344 self._manifest.UpdateManifest(self.options) | |
345 self.WriteFile() | |
346 | |
347 | |
348 def CommandPush(options, args, manifest_file): | |
349 '''Check the manifest file and push it to the server if it's okay''' | |
350 print 'Running Push with options=%s and args=%s' % (options, args) | |
351 manifest = manifest_file._manifest | |
352 manifest.UpdateManifest(options) | |
353 print 'Validating links within manifest file' | |
354 manifest.ValidateManifestLinks() | |
355 print 'Copying manifest file to server' | |
356 manifest_file.gsutil.Copy(options.manifest_file, 'naclsdk_manifest.json') | |
357 | |
358 | |
359 def main(argv): | |
360 '''Main entry for update_manifest.py''' | |
361 | |
362 buildtools_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
363 parser = optparse.OptionParser(usage=HELP) | |
364 | |
365 # Setup options | |
366 parser.add_option( | |
367 '-a', '--archive-id', dest='archive_id', | |
368 default=None, | |
369 help='Archive identifier, produced by the Chromium builders; string ' | |
370 'like "trunk.113440" or "17.0.963.3". Used with --root-url to ' | |
371 'build the full archive URL. If not set the archive id defaults to ' | |
372 '"pepper_<version>_<revision>"') | |
373 parser.add_option( | |
374 '-b', '--bundle-version', dest='bundle_version', | |
375 type='int', | |
376 default=None, | |
377 help='Required: Version number for the bundle.') | |
378 parser.add_option( | |
379 '-B', '--bundle-revision', dest='bundle_revision', | |
380 type='int', | |
381 default=None, | |
382 help='Required: Revision number for the bundle.') | |
383 parser.add_option( | |
384 '-d', '--description', dest='desc', | |
385 default=None, | |
386 help='Required: Description for this bundle.') | |
387 parser.add_option( | |
388 '-f', '--manifest-file', dest='manifest_file', | |
389 default=os.path.join(buildtools_dir, 'json', | |
390 sdk_update.MANIFEST_FILENAME), | |
391 help='location of manifest file to read and update') | |
392 parser.add_option( | |
393 '-g', '--gsutil', dest='gsutil', | |
394 default='gsutil', help='location of gsutil tool for uploading bundles') | |
395 parser.add_option( | |
396 '-L', '--linux-archive', dest='linux_arch_url', | |
397 default=None, | |
398 help='URL for the Linux archive.') | |
399 parser.add_option( | |
400 '-M', '--mac-archive', dest='mac_arch_url', | |
401 default=None, | |
402 help='URL for the Mac archive.') | |
403 parser.add_option( | |
404 '-n', '--bundle-name', dest='bundle_name', | |
405 default=None, | |
406 help='Required: Name of the bundle.') | |
407 parser.add_option( | |
408 '-r', '--recommended', dest='recommended', | |
409 choices=sdk_update.YES_NO_LITERALS, | |
410 default=None, | |
411 help='Required: whether this bundle is recommended. One of "yes" or "no"') | |
412 parser.add_option( | |
413 '-R', '--root-url', dest='root_url', | |
414 default='http://commondatastorage.googleapis.com/nativeclient-mirror/' | |
415 'nacl/nacl_sdk', | |
416 help='Root url for uploading') | |
417 parser.add_option( | |
418 '-s', '--stability', dest='stability', | |
419 choices=sdk_update.STABILITY_LITERALS, | |
420 default=None, | |
421 help='Required: Stability for this bundle; one of. ' | |
422 '"obsolete", "post_stable", "stable", "beta", "dev", "canary".') | |
423 parser.add_option( | |
424 '-u', '--desc-url', dest='bundle_desc_url', | |
425 default=None, | |
426 help='Optional: URL to follow to read additional bundle info.') | |
427 parser.add_option( | |
428 '-U', '--upload', dest='upload', default=False, action='store_true', | |
429 help='Indicates whether to upload bundle to server') | |
430 parser.add_option( | |
431 '-v', '--manifest-version', dest='manifest_version', | |
432 type='int', | |
433 default=None, | |
434 help='Required for new manifest files: ' | |
435 'Version number for the manifest.') | |
436 parser.add_option( | |
437 '-W', '--win-archive', dest='win_arch_url', | |
438 default=None, | |
439 help='URL for the Windows archive.') | |
440 | |
441 # Parse options and arguments and check. | |
442 (options, args) = parser.parse_args(argv) | |
443 manifest_file = UpdateSDKManifestFile(options) | |
444 if len(args) == 0: | |
445 manifest_file.HandleBundles() | |
446 manifest_file.UpdateWithOptions() | |
447 return 0 | |
448 | |
449 COMMANDS = { | |
450 'push': CommandPush | |
451 } | |
452 def CommandUnknown(options, args, manifest_file): | |
453 raise Error("Unknown command %s" % args[0]) | |
454 try: | |
455 COMMANDS.get(args[0], CommandUnknown)(options, args, manifest_file) | |
456 except Error as error: | |
457 print "Error: %s" % error | |
458 return 1 | |
459 return 0 | |
460 | |
461 | |
462 if __name__ == '__main__': | |
463 sys.exit(main(sys.argv[1:])) | |
OLD | NEW |