Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(324)

Side by Side Diff: native_client_sdk/src/build_tools/sdk_tools/sdk_update_main.py

Issue 11228013: [NaCl SDK] Refactor sdk_update*. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: fix build_updater Created 8 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 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 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 '''A simple tool to update the Native Client SDK to the latest version''' 6 # CMD code copied from git_cl.py in depot_tools.
7 7
8 import config
8 import cStringIO 9 import cStringIO
9 import cygtar 10 import download
10 import json 11 import json
11 import manifest_util 12 import logging
12 import optparse 13 import optparse
13 import os 14 import os
14 from sdk_update_common import RenameDir, RemoveDir, Error 15 import re
15 import shutil 16 import sdk_update_common
16 import subprocess 17 from sdk_update_common import Error
17 import sys 18 import sys
18 import tempfile
19 # when pylint runs the third_party module is the one from depot_tools
20 # pylint: disable=E0611
21 from third_party import fancy_urllib
22 import urllib2 19 import urllib2
23 import urlparse 20
24 21 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
25 # pylint: disable=C0301 22 PARENT_DIR = os.path.dirname(SCRIPT_DIR)
26 23
27 #------------------------------------------------------------------------------ 24 sys.path.append(os.path.dirname(SCRIPT_DIR))
28 # Constants 25 import manifest_util
26
27
28 # Import late so each command script can find our imports
29 import command.info
30 import command.list
31 import command.sources
32 import command.update
29 33
30 # This revision number is autogenerated from the Chrome revision. 34 # This revision number is autogenerated from the Chrome revision.
31 REVISION = '{REVISION}' 35 REVISION = '{REVISION}'
32 36
33 GLOBAL_HELP = '''Usage: naclsdk [options] command [command_options] 37 GSTORE_URL = 'https://commondatastorage.googleapis.com/nativeclient-mirror'
34
35 naclsdk is a simple utility that updates the Native Client (NaCl)
36 Software Developer's Kit (SDK). Each component is kept as a 'bundle' that
37 this utility can download as as subdirectory into the SDK.
38
39 Commands:
40 help [command] - Get either general or command-specific help
41 info - Displays information about a bundle
42 list - Lists the available bundles
43 update/install - Updates/installs bundles in the SDK
44 sources - Manage external package sources
45
46 Example Usage:
47 naclsdk info pepper_canary
48 naclsdk list
49 naclsdk update --force pepper_17
50 naclsdk install recommended
51 naclsdk help update
52 naclsdk sources --list'''
53
54 CONFIG_FILENAME = 'naclsdk_config.json' 38 CONFIG_FILENAME = 'naclsdk_config.json'
55 MANIFEST_FILENAME = 'naclsdk_manifest2.json' 39 MANIFEST_FILENAME = 'naclsdk_manifest2.json'
56 SDK_TOOLS = 'sdk_tools' # the name for this tools directory 40 DEFAULT_SDK_ROOT = os.path.abspath(PARENT_DIR)
57 USER_DATA_DIR = 'sdk_cache' 41 USER_DATA_DIR = os.path.join(DEFAULT_SDK_ROOT, 'sdk_cache')
58 42
59 HTTP_CONTENT_LENGTH = 'Content-Length' # HTTP Header field for content length 43
60 44 def usage(more):
61 45 def hook(fn):
62 #------------------------------------------------------------------------------ 46 fn.usage_more = more
63 # General Utilities 47 return fn
64 48 return hook
65 49
66 _debug_mode = False 50
67 _quiet_mode = False 51 def hide(fn):
68 52 fn.hide = True
69 53 return fn
70 def DebugPrint(msg): 54
71 '''Display a message to stderr if debug printing is enabled 55
72 56 def LoadConfig(raise_on_error=False):
73 Note: This function appends a newline to the end of the string 57 path = os.path.join(USER_DATA_DIR, CONFIG_FILENAME)
74 58 if not os.path.exists(path):
75 Args: 59 return config.Config()
76 msg: A string to send to stderr in debug mode''' 60
77 if _debug_mode: 61 try:
78 sys.stderr.write("%s\n" % msg)
79 sys.stderr.flush()
80
81
82 def InfoPrint(msg):
83 '''Display an informational message to stdout if not in quiet mode
84
85 Note: This function appends a newline to the end of the string
86
87 Args:
88 mgs: A string to send to stdio when not in quiet mode'''
89 if not _quiet_mode:
90 sys.stdout.write("%s\n" % msg)
91 sys.stdout.flush()
92
93
94 def WarningPrint(msg):
95 '''Display an informational message to stderr.
96
97 Note: This function appends a newline to the end of the string
98
99 Args:
100 mgs: A string to send to stderr.'''
101 sys.stderr.write("WARNING: %s\n" % msg)
102 sys.stderr.flush()
103
104
105 def UrlOpen(url):
106 request = fancy_urllib.FancyRequest(url)
107 ca_certs = os.path.join(os.path.dirname(os.path.abspath(__file__)),
108 'cacerts.txt')
109 request.set_ssl_info(ca_certs=ca_certs)
110 url_opener = urllib2.build_opener(
111 fancy_urllib.FancyProxyHandler(),
112 fancy_urllib.FancyRedirectHandler(),
113 fancy_urllib.FancyHTTPSHandler())
114 return url_opener.open(request)
115
116 def ExtractInstaller(installer, outdir):
117 '''Extract the SDK installer into a given directory
118
119 If the outdir already exists, then this function deletes it
120
121 Args:
122 installer: full path of the SDK installer
123 outdir: output directory where to extract the installer
124
125 Raises:
126 CalledProcessError - if the extract operation fails'''
127 RemoveDir(outdir)
128
129 if os.path.splitext(installer)[1] == '.exe':
130 # If the installer has extension 'exe', assume it's a Windows NSIS-style
131 # installer that handles silent (/S) and relocated (/D) installs.
132 command = [installer, '/S', '/D=%s' % outdir]
133 subprocess.check_call(command)
134 else:
135 os.mkdir(outdir)
136 tar_file = None
137 curpath = os.getcwd()
138 try: 62 try:
139 tar_file = cygtar.CygTar(installer, 'r', verbose=True) 63 with open(path) as f:
140 if outdir: 64 return config.Config(json.loads(f.read()))
141 os.chdir(outdir) 65 except IOError as e:
142 tar_file.Extract() 66 raise Error('Unable to read config from "%s".\n %s' % (path, e))
143 finally: 67 except Exception as e:
144 if tar_file: 68 raise Error('Parsing config file from "%s" failed.\n %s' % (path, e))
145 tar_file.Close() 69 except Error as e:
146 os.chdir(curpath) 70 if raise_on_error:
147 71 raise
148 72 else:
149 class ProgressFunction(object): 73 logging.warn(str(e))
150 '''Create a progress function for a file with a given size''' 74 return config.Config()
151 75
152 def __init__(self, file_size=0): 76
153 '''Constructor 77 def WriteConfig(cfg):
154 78 path = os.path.join(USER_DATA_DIR, CONFIG_FILENAME)
155 Args: 79 try:
156 file_size: number of bytes in file. 0 indicates unknown''' 80 sdk_update_common.MakeDirs(USER_DATA_DIR)
157 self.dots = 0 81 except Exception as e:
158 self.file_size = int(file_size) 82 raise Error('Unable to create directory "%s".\n %s' % (USER_DATA_DIR, e))
159 83
160 def GetProgressFunction(self): 84 try:
161 '''Returns a progress function based on a known file size''' 85 cfg_json = cfg.ToJson()
162 def ShowKnownProgress(progress): 86 except Exception as e:
163 if progress == 0: 87 raise Error('Json encoding error writing config "%s".\n %s' % (path, e))
164 sys.stdout.write('|%s|\n' % ('=' * 48)) 88
165 else: 89 try:
166 new_dots = progress * 50 / self.file_size - self.dots 90 with open(path, 'w') as f:
167 sys.stdout.write('.' * new_dots) 91 f.write(cfg_json)
168 self.dots += new_dots 92 except IOError as e:
169 if progress == self.file_size: 93 raise Error('Unable to write config to "%s".\n %s' % (path, e))
170 sys.stdout.write('\n') 94
171 sys.stdout.flush() 95
172 96 def LoadLocalManifest(raise_on_error=False):
173 return ShowKnownProgress 97 path = os.path.join(USER_DATA_DIR, MANIFEST_FILENAME)
174 98 manifest = manifest_util.SDKManifest()
175 99 try:
176 def DownloadArchiveToFile(archive, dest_path):
177 '''Download the archive's data to a file at dest_path.
178
179 As a side effect, computes the sha1 hash and data size, both returned as a
180 tuple. Raises an Error if the url can't be opened, or an IOError exception if
181 dest_path can't be opened.
182
183 Args:
184 dest_path: Path for the file that will receive the data.
185 Return:
186 A tuple (sha1, size) with the sha1 hash and data size respectively.'''
187 sha1 = None
188 size = 0
189 with open(dest_path, 'wb') as to_stream:
190 from_stream = None
191 try: 100 try:
192 from_stream = UrlOpen(archive.url) 101 with open(path) as f:
193 except urllib2.URLError: 102 manifest_string = f.read()
194 raise Error('Cannot open "%s" for archive %s' % 103 except IOError as e:
195 (archive.url, archive.host_os)) 104 raise Error('Unable to read manifest from "%s".\n %s' % (path, e))
105
196 try: 106 try:
197 content_length = int(from_stream.info()[HTTP_CONTENT_LENGTH]) 107 manifest.LoadDataFromString(manifest_string)
198 progress_function = ProgressFunction(content_length).GetProgressFunction() 108 except Exception as e:
199 InfoPrint('Downloading %s' % archive.url) 109 raise Error('Parsing local manifest "%s" failed.\n %s' % (path, e))
200 sha1, size = manifest_util.DownloadAndComputeHash( 110 except Error as e:
201 from_stream, 111 if raise_on_error:
202 to_stream=to_stream, 112 raise
203 progress_func=progress_function) 113 else:
204 if size != content_length: 114 logging.warn(str(e))
205 raise Error('Download size mismatch for %s.\n' 115 return manifest
206 'Expected %s bytes but got %s' % 116
207 (archive.url, content_length, size)) 117
208 finally: 118 def WriteLocalManifest(manifest):
209 if from_stream: 119 path = os.path.join(USER_DATA_DIR, MANIFEST_FILENAME)
210 from_stream.close() 120 try:
211 return sha1, size 121 sdk_update_common.MakeDirs(USER_DATA_DIR)
212 122 except Exception as e:
213 123 raise Error('Unable to create directory "%s".\n %s' % (USER_DATA_DIR, e))
214 def LoadFromFile(path, obj): 124
215 '''Returns a manifest loaded from the JSON file at |path|. 125 try:
216 126 manifest_json = manifest.GetDataAsString()
217 If the path does not exist or is invalid, returns unmodified object.''' 127 except Exception as e:
218 methodlist = [m for m in dir(obj) if callable(getattr(obj, m))] 128 raise Error('Error encoding manifest "%s" to JSON.\n %s' % (path, e))
219 if 'LoadDataFromString' not in methodlist: 129
220 return obj 130 try:
221 if not os.path.exists(path): 131 with open(path, 'w') as f:
222 return obj 132 f.write(manifest_json)
223 133 except IOError as e:
224 with open(path, 'r') as f: 134 raise Error('Unable to write manifest to "%s".\n %s' % (path, e))
225 json_string = f.read() 135
226 if not json_string: 136
227 return obj 137 def LoadRemoteManifest(url):
228
229 obj.LoadDataFromString(json_string)
230 return obj
231
232
233 def LoadManifestFromURLs(urls):
234 '''Returns a manifest loaded from |urls|, merged into one manifest.'''
235 manifest = manifest_util.SDKManifest() 138 manifest = manifest_util.SDKManifest()
236 for url in urls: 139 url_stream = None
140 try:
141 manifest_stream = cStringIO.StringIO()
142 url_stream = download.UrlOpen(url)
143 download.DownloadAndComputeHash(url_stream, manifest_stream)
144 except urllib2.URLError as e:
145 raise Error('Unable to read remote manifest from URL "%s".\n %s' % (
146 url, e))
147 finally:
148 if url_stream:
149 url_stream.close()
150
151 try:
152 manifest.LoadDataFromString(manifest_stream.getvalue())
153 return manifest
154 except manifest_util.Error as e:
155 raise Error('Parsing remote manifest from URL "%s" failed.\n %s' % (
156 url, e,))
157
158
159 def LoadCombinedRemoteManifest(default_manifest_url, cfg):
160 manifest = LoadRemoteManifest(default_manifest_url)
161 for source in cfg.sources:
162 manifest.MergeManifest(LoadRemoteManifest(source))
163 return manifest
164
165
166 # Commands #####################################################################
167
168
169 @usage('<bundle names...>')
170 def CMDinfo(parser, args):
171 """display information about a bundle"""
172 options, args = parser.parse_args(args)
173 if len(args) == 0:
174 parser.error('No bundles given')
175 return 0
176 cfg = LoadConfig()
177 remote_manifest = LoadCombinedRemoteManifest(options.manifest_url, cfg)
178 command.info.Info(remote_manifest, args)
179 return 0
180
181
182 def CMDlist(parser, args):
183 """list all available bundles"""
184 parser.add_option('-r', '--revision', action='store_true',
185 help='display revision numbers')
186 options, args = parser.parse_args(args)
187 if args:
188 parser.error('Unsupported argument(s): %s' % ', '.join(args))
189 local_manifest = LoadLocalManifest()
190 cfg = LoadConfig()
191 remote_manifest = LoadCombinedRemoteManifest(options.manifest_url, cfg)
192 command.list.List(remote_manifest, local_manifest, options.revision)
193 return 0
194
195
196 @usage('<bundle names...>')
197 def CMDupdate(parser, args):
198 """update a bundle in the SDK to the latest version"""
199 parser.add_option(
200 '-F', '--force', action='store_true',
201 help='Force updating existing components that already exist')
202 options, args = parser.parse_args(args)
203 local_manifest = LoadLocalManifest()
204 cfg = LoadConfig()
205 remote_manifest = LoadCombinedRemoteManifest(options.manifest_url, cfg)
206
207 if not args:
208 args = [command.update.RECOMMENDED]
209
210 try:
211 delegate = command.update.RealUpdateDelegate(USER_DATA_DIR,
212 DEFAULT_SDK_ROOT)
213 command.update.Update(delegate, remote_manifest, local_manifest, args,
214 options.force)
215 finally:
216 # Always write out the local manifest, we may have successfully updated one
217 # or more bundles before failing.
237 try: 218 try:
238 url_stream = UrlOpen(url) 219 WriteLocalManifest(local_manifest)
239 except urllib2.URLError as e: 220 except Error as e:
240 raise Error('Unable to open %s. [%s]' % (url, e)) 221 # Log the error writing to the manifest, but propagate the original
241 222 # exception.
242 manifest_stream = cStringIO.StringIO() 223 logging.error(str(e))
243 manifest_util.DownloadAndComputeHash(url_stream, manifest_stream) 224
244 temp_manifest = manifest_util.SDKManifest() 225 return 0
245 temp_manifest.LoadDataFromString(manifest_stream.getvalue()) 226
246 227
247 manifest.MergeManifest(temp_manifest) 228 def CMDinstall(parser, args):
248 229 """install a bundle in the SDK"""
249 def BundleFilter(bundle): 230 # For now, forward to CMDupdate. We may want different behavior for this
250 # Only add this bundle if it's supported on this platform. 231 # in the future, though...
251 return bundle.GetHostOSArchive() 232 return CMDupdate(parser, args)
252 233
253 manifest.FilterBundles(BundleFilter) 234
254 return manifest 235 def CMDsources(parser, args):
255 236 """manage external package sources"""
256 237 parser.add_option('-a', '--add', dest='url_to_add',
257 def WriteToFile(path, obj): 238 help='Add an additional package source')
258 '''Write |manifest| to a JSON file at |path|.'''
259 methodlist = [m for m in dir(obj) if callable(getattr(obj, m))]
260 if 'GetDataAsString' not in methodlist:
261 raise Error('Unable to write object to file')
262 json_string = obj.GetDataAsString()
263
264 # Write the JSON data to a temp file.
265 temp_file_name = None
266 # TODO(dspringer): Use file locks here so that multiple sdk_updates can
267 # run at the same time.
268 with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
269 f.write(json_string)
270 temp_file_name = f.name
271 # Move the temp file to the actual file.
272 if os.path.exists(path):
273 os.remove(path)
274 shutil.move(temp_file_name, path)
275
276
277 class SDKConfig(object):
278 '''This class contains utilities for manipulating an SDK config
279 '''
280
281 def __init__(self):
282 '''Create a new SDKConfig object with default contents'''
283 self._data = {
284 'sources': [],
285 }
286
287 def AddSource(self, string):
288 '''Add a source file to load packages from.
289
290 Args:
291 string: a URL to an external package manifest file.'''
292 # For now whitelist only the following location for external sources:
293 # https://commondatastorage.googleapis.com/nativeclient-mirror/nacl/nacl_sdk
294 (scheme, host, path, _, _, _) = urlparse.urlparse(string)
295 if (host != 'commondatastorage.googleapis.com' or
296 scheme != 'https' or
297 not path.startswith('/nativeclient-mirror/nacl/nacl_sdk')):
298 WarningPrint('Only whitelisted sources from '
299 '\'https://commondatastorage.googleapis.com/nativeclient-'
300 'mirror/nacl/nacl_sdk\' are currently allowed.')
301 return
302 if string in self._data['sources']:
303 WarningPrint('source \''+string+'\' already exists in config.')
304 return
305 try:
306 UrlOpen(string)
307 except urllib2.URLError:
308 WarningPrint('Unable to fetch manifest URL \'%s\'. Exiting...' % string)
309 return
310
311 self._data['sources'].append(string)
312 InfoPrint('source \''+string+'\' added to config.')
313
314 def RemoveSource(self, string):
315 '''Remove a source file to load packages from.
316
317 Args:
318 string: a URL to an external SDK manifest file.'''
319 if string not in self._data['sources']:
320 WarningPrint('source \''+string+'\' doesn\'t exist in config.')
321 else:
322 self._data['sources'].remove(string)
323 InfoPrint('source \''+string+'\' removed from config.')
324
325 def RemoveAllSources(self):
326 if len(self.GetSources()) == 0:
327 InfoPrint('There are no external sources to remove.')
328 # Copy the list because RemoveSource modifies the underlying list
329 sources = list(self.GetSources())
330 for source in sources:
331 self.RemoveSource(source)
332
333
334 def ListSources(self):
335 '''List all external sources in config.'''
336 if len(self._data['sources']):
337 InfoPrint('Installed sources:')
338 for s in self._data['sources']:
339 InfoPrint(' '+s)
340 else:
341 InfoPrint('No external sources installed')
342
343 def GetSources(self):
344 '''Return a list of external sources'''
345 return self._data['sources']
346
347 def LoadDataFromString(self, string):
348 ''' Load a JSON config string. Raises an exception if string
349 is not well-formed JSON.
350
351 Args:
352 string: a JSON-formatted string containing the previous config'''
353 self._data = json.loads(string)
354
355
356 def GetDataAsString(self):
357 '''Returns the current JSON manifest object, pretty-printed'''
358 pretty_string = json.dumps(self._data, sort_keys=False, indent=2)
359 # json.dumps sometimes returns trailing whitespace and does not put
360 # a newline at the end. This code fixes these problems.
361 pretty_lines = pretty_string.split('\n')
362 return '\n'.join([line.rstrip() for line in pretty_lines]) + '\n'
363
364
365 #------------------------------------------------------------------------------
366 # Commands
367
368
369 def Info(options, argv, config):
370 '''Usage: %prof [global_options] info [options] bundle_names...
371
372 Displays information about a SDK bundle.'''
373
374 DebugPrint("Running List command with: %s, %s" %(options, argv))
375
376 parser = optparse.OptionParser(usage=Info.__doc__)
377 (_, args) = parser.parse_args(argv)
378
379 if not args:
380 parser.print_help()
381 return
382
383 manifest = LoadManifestFromURLs([options.manifest_url] + config.GetSources())
384 valid_bundles = [bundle.name for bundle in manifest.GetBundles()]
385 valid_args = set(args) & set(valid_bundles)
386 invalid_args = set(args) - valid_args
387 if invalid_args:
388 InfoPrint('Unknown bundle(s): %s\n' % (', '.join(invalid_args)))
389
390 for bundle_name in args:
391 if bundle_name not in valid_args:
392 continue
393
394 bundle = manifest.GetBundle(bundle_name)
395
396 InfoPrint('%s' % bundle.name)
397 for key, value in bundle.iteritems():
398 if key == manifest_util.ARCHIVES_KEY:
399 archive = bundle.GetHostOSArchive()
400 InfoPrint(' Archive:')
401 for archive_key, archive_value in archive.iteritems():
402 InfoPrint(' %s: %s' % (archive_key, archive_value))
403 elif key not in (manifest_util.ARCHIVES_KEY, manifest_util.NAME_KEY):
404 InfoPrint(' %s: %s' % (key, value))
405 InfoPrint('')
406
407
408 def List(options, argv, config):
409 '''Usage: %prog [global_options] list [options]
410
411 Lists the available SDK bundles that are available for download.'''
412
413 def PrintBundle(local_bundle, bundle, needs_update, display_revisions):
414 installed = local_bundle is not None
415 # If bundle is None, there is no longer a remote bundle with this name.
416 if bundle is None:
417 bundle = local_bundle
418
419 if display_revisions:
420 if needs_update:
421 revision = ' (r%s -> r%s)' % (local_bundle.revision, bundle.revision)
422 else:
423 revision = ' (r%s)' % (bundle.revision,)
424 else:
425 revision = ''
426
427 InfoPrint(' %s%s %s (%s)%s' % (
428 'I' if installed else ' ',
429 '*' if needs_update else ' ',
430 bundle.name,
431 bundle.stability,
432 revision))
433
434
435 DebugPrint("Running List command with: %s, %s" %(options, argv))
436
437 parser = optparse.OptionParser(usage=List.__doc__)
438 parser.add_option(
439 '-r', '--revision', dest='revision',
440 default=False, action='store_true',
441 help='display revision numbers')
442 (list_options, _) = parser.parse_args(argv)
443
444 manifest = LoadManifestFromURLs([options.manifest_url] + config.GetSources())
445 manifest_path = os.path.join(options.user_data_dir, options.manifest_filename)
446 local_manifest = LoadFromFile(manifest_path, manifest_util.SDKManifest())
447
448 any_bundles_need_update = False
449 InfoPrint('Bundles:')
450 InfoPrint(' I: installed\n *: update available\n')
451 for bundle in manifest.GetBundles():
452 local_bundle = local_manifest.GetBundle(bundle.name)
453 needs_update = local_bundle and local_manifest.BundleNeedsUpdate(bundle)
454 if needs_update:
455 any_bundles_need_update = True
456
457 PrintBundle(local_bundle, bundle, needs_update, list_options.revision)
458
459 if not any_bundles_need_update:
460 InfoPrint('\nAll installed bundles are up-to-date.')
461
462 local_only_bundles = set([b.name for b in local_manifest.GetBundles()])
463 local_only_bundles -= set([b.name for b in manifest.GetBundles()])
464 if local_only_bundles:
465 InfoPrint('\nBundles installed locally that are not available remotely:')
466 for bundle_name in local_only_bundles:
467 local_bundle = local_manifest.GetBundle(bundle_name)
468 PrintBundle(local_bundle, None, False, list_options.revision)
469
470
471 def Update(options, argv, config):
472 '''Usage: %prog [global_options] update [options] [target]
473
474 Updates the Native Client SDK to a specified version. By default, this
475 command updates all the recommended components. The update process works
476 like this:
477 1. Fetch the manifest from the mirror.
478 2. Load manifest from USER_DATA_DIR - if there is no local manifest file,
479 make an empty manifest object.
480 3. Update each the bundle:
481 for bundle in bundles:
482 # Compare bundle versions & revisions.
483 # Test if local version.revision < mirror OR local doesn't exist.
484 if local_manifest < mirror_manifest:
485 update(bundle)
486 update local_manifest with mirror_manifest for bundle
487 write manifest to disk. Use locks.
488 else:
489 InfoPrint('bundle is up-to-date')
490
491 Targets:
492 recommended: (default) Install/Update all recommended components
493 all: Install/Update all available components
494 bundle_name: Install/Update only the given bundle
495 '''
496 DebugPrint("Running Update command with: %s, %s" % (options, argv))
497 ALL = 'all' # Update all bundles
498 RECOMMENDED = 'recommended' # Only update the bundles with recommended=yes
499
500 parser = optparse.OptionParser(usage=Update.__doc__)
501 parser.add_option(
502 '-F', '--force', dest='force',
503 default=False, action='store_true',
504 help='Force updating existing components that already exist')
505 (update_options, args) = parser.parse_args(argv)
506
507 if len(args) == 0:
508 args = [RECOMMENDED]
509
510 manifest = LoadManifestFromURLs([options.manifest_url] + config.GetSources())
511 bundles = manifest.GetBundles()
512 local_manifest_path = os.path.join(options.user_data_dir,
513 options.manifest_filename)
514 local_manifest = LoadFromFile(local_manifest_path,
515 manifest_util.SDKManifest())
516
517 # Validate the arg list against the available bundle names. Raises an
518 # error if any invalid bundle names or args are detected.
519 valid_args = set([ALL, RECOMMENDED] + [bundle.name for bundle in bundles])
520 bad_args = set(args) - valid_args
521 if len(bad_args) > 0:
522 raise Error("Unrecognized bundle name or argument: '%s'" %
523 ', '.join(bad_args))
524
525 if SDK_TOOLS in args and not options.update_sdk_tools:
526 # We only want sdk_tools to be updated by sdk_update.py. If the user
527 # tries to update directly, we just ignore the request.
528 InfoPrint('Updating sdk_tools happens automatically.\n'
529 'Ignoring manual update request.')
530 args.remove(SDK_TOOLS)
531
532 for bundle in bundles:
533 bundle_path = os.path.join(options.sdk_root_dir, bundle.name)
534 bundle_update_path = '%s_update' % bundle_path
535 if not (bundle.name in args or
536 ALL in args or (RECOMMENDED in args and
537 bundle[RECOMMENDED] == 'yes')):
538 continue
539
540 if bundle.name == SDK_TOOLS and not options.update_sdk_tools:
541 continue
542
543 def UpdateBundle():
544 '''Helper to install a bundle'''
545 archive = bundle.GetHostOSArchive()
546 (_, _, path, _, _, _) = urlparse.urlparse(archive['url'])
547 dest_filename = os.path.join(options.user_data_dir, path.split('/')[-1])
548 sha1, size = DownloadArchiveToFile(archive, dest_filename)
549 if sha1 != archive.GetChecksum():
550 raise Error("SHA1 checksum mismatch on '%s'. Expected %s but got %s" %
551 (bundle.name, archive.GetChecksum(), sha1))
552 if size != archive.size:
553 raise Error("Size mismatch on Archive. Expected %s but got %s bytes" %
554 (archive.size, size))
555 InfoPrint('Updating bundle %s to version %s, revision %s' % (
556 (bundle.name, bundle.version, bundle.revision)))
557 ExtractInstaller(dest_filename, bundle_update_path)
558 if bundle.name != SDK_TOOLS:
559 repath = bundle.get('repath', None)
560 if repath:
561 bundle_move_path = os.path.join(bundle_update_path, repath)
562 else:
563 bundle_move_path = bundle_update_path
564 RenameDir(bundle_move_path, bundle_path)
565 if os.path.exists(bundle_update_path):
566 RemoveDir(bundle_update_path)
567 os.remove(dest_filename)
568 local_manifest.MergeBundle(bundle)
569 WriteToFile(local_manifest_path, local_manifest)
570 # Test revision numbers, update the bundle accordingly.
571 # TODO(dspringer): The local file should be refreshed from disk each
572 # iteration thought this loop so that multiple sdk_updates can run at the
573 # same time.
574 if local_manifest.BundleNeedsUpdate(bundle):
575 if (not update_options.force and os.path.exists(bundle_path) and
576 bundle.name != SDK_TOOLS):
577 WarningPrint('%s already exists, but has an update available.\n'
578 'Run update with the --force option to overwrite the '
579 'existing directory.\nWarning: This will overwrite any '
580 'modifications you have made within this directory.'
581 % bundle.name)
582 else:
583 UpdateBundle()
584 else:
585 InfoPrint('%s is already up-to-date.' % bundle.name)
586
587 def Sources(options, argv, config):
588 '''Usage: %prog [global_options] sources [options] [--list,--add URL,--remove URL]
589
590 Manage additional package sources. URL should point to a valid package
591 manifest file for download.
592 '''
593 DebugPrint("Running Sources command with: %s, %s" % (options, argv))
594
595 parser = optparse.OptionParser(usage=Sources.__doc__)
596 parser.add_option(
597 '-a', '--add', dest='url_to_add',
598 default=None,
599 help='Add additional package source')
600 parser.add_option( 239 parser.add_option(
601 '-r', '--remove', dest='url_to_remove', 240 '-r', '--remove', dest='url_to_remove',
602 default=None,
603 help='Remove package source (use \'all\' for all additional sources)') 241 help='Remove package source (use \'all\' for all additional sources)')
604 parser.add_option( 242 parser.add_option('-l', '--list', dest='do_list', action='store_true',
605 '-l', '--list', dest='do_list', 243 help='List additional package sources')
606 default=False, action='store_true', 244 options, args = parser.parse_args(args)
607 help='List additional package sources') 245
608 source_options, _ = parser.parse_args(argv) 246 cfg = LoadConfig(True)
609
610 write_config = False 247 write_config = False
611 if source_options.url_to_add: 248 if options.url_to_add:
612 config.AddSource(source_options.url_to_add) 249 command.sources.AddSource(cfg, options.url_to_add)
613 write_config = True 250 write_config = True
614 elif source_options.url_to_remove: 251 elif options.url_to_remove:
615 if source_options.url_to_remove == 'all': 252 command.sources.RemoveSource(cfg, options.url_to_remove)
616 config.RemoveAllSources()
617 else:
618 config.RemoveSource(source_options.url_to_remove)
619 write_config = True 253 write_config = True
620 elif source_options.do_list: 254 elif options.do_list:
621 config.ListSources() 255 command.sources.ListSources(cfg)
622 else: 256 else:
623 parser.print_help() 257 parser.print_help()
624 258
625 if write_config: 259 if write_config:
626 WriteToFile(os.path.join(options.user_data_dir, options.config_filename), 260 WriteConfig(cfg)
627 config) 261
628 262 return 0
629 #------------------------------------------------------------------------------ 263
630 # Command-line interface 264
265 def CMDversion(parser, args):
266 """display version information"""
267 _, _ = parser.parse_args(args)
268 print "Native Client SDK Updater, version r%s" % REVISION
269 return 0
270
271
272 def CMDhelp(parser, args):
273 """print list of commands or help for a specific command"""
274 _, args = parser.parse_args(args)
275 if len(args) == 1:
276 return main(args + ['--help'])
277 parser.print_help()
278 return 0
279
280
281 def Command(name):
282 return globals().get('CMD' + name, None)
283
284
285 def GenUsage(parser, cmd):
286 """Modify an OptParse object with the function's documentation."""
287 obj = Command(cmd)
288 more = getattr(obj, 'usage_more', '')
289 if cmd == 'help':
290 cmd = '<command>'
291 else:
292 # OptParser.description prefer nicely non-formatted strings.
293 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
294 parser.set_usage('usage: %%prog %s [options] %s' % (cmd, more))
295
296
297 def UpdateSDKTools(options, args):
298 """update the sdk_tools bundle"""
299
300 local_manifest = LoadLocalManifest()
301 cfg = LoadConfig()
302 remote_manifest = LoadCombinedRemoteManifest(options.manifest_url, cfg)
303
304 try:
305 delegate = command.update.RealUpdateDelegate(USER_DATA_DIR,
306 DEFAULT_SDK_ROOT)
307 command.update.UpdateBundleIfNeeded(
308 delegate,
309 remote_manifest,
310 local_manifest,
311 command.update.SDK_TOOLS,
312 force=True)
313 finally:
314 # Always write out the local manifest, we may have successfully updated one
315 # or more bundles before failing.
316 WriteLocalManifest(local_manifest)
317 return 0
631 318
632 319
633 def main(argv): 320 def main(argv):
634 '''Main entry for the sdk_update utility''' 321 # Get all commands...
635 parser = optparse.OptionParser(usage=GLOBAL_HELP, add_help_option=False) 322 cmds = [fn[3:] for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]
636 DEFAULT_SDK_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 323 # Remove hidden commands...
637 324 cmds = filter(lambda fn: not getattr(Command(fn), 'hide', 0), cmds)
638 # Manually add help options so we can ignore it when auto-updating. 325 # Format for CMDhelp usage.
326 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
327 ' %-10s %s' % (fn, Command(fn).__doc__.split('\n')[0].strip())
328 for fn in cmds]))
329
330 # Create the option parse and add --verbose support.
331 parser = optparse.OptionParser()
639 parser.add_option( 332 parser.add_option(
640 '-h', '--help', dest='help', action='store_true', 333 '-v', '--verbose', action='count', default=0,
641 help='show this help message and exit') 334 help='Use 2 times for more debugging info')
642 parser.add_option( 335 parser.add_option('-U', '--manifest-url', dest='manifest_url',
643 '-U', '--manifest-url', dest='manifest_url', 336 default=GSTORE_URL + '/nacl/nacl_sdk/' + MANIFEST_FILENAME,
644 default='https://commondatastorage.googleapis.com/nativeclient-mirror/' 337 metavar='URL', help='override the default URL for the NaCl manifest file')
645 'nacl/nacl_sdk/%s' % MANIFEST_FILENAME, 338 parser.add_option('--update-sdk-tools', action='store_true',
646 help='override the default URL for the NaCl manifest file') 339 dest='update_sdk_tools', help=optparse.SUPPRESS_HELP)
647 parser.add_option( 340
648 '-d', '--debug', dest='debug', 341 old_parser_args = parser.parse_args
649 default=False, action='store_true', 342 def Parse(args):
650 help='enable displaying debug information to stderr') 343 options, args = old_parser_args(args)
651 parser.add_option( 344 if options.verbose >= 2:
652 '-q', '--quiet', dest='quiet', 345 loglevel = logging.DEBUG
653 default=False, action='store_true', 346 elif options.verbose:
654 help='suppress displaying informational prints to stdout') 347 loglevel = logging.INFO
655 parser.add_option(
656 '-u', '--user-data-dir', dest='user_data_dir',
657 # TODO(mball): the default should probably be in something like
658 # ~/.naclsdk (linux), or ~/Library/Application Support/NaClSDK (mac),
659 # or %HOMEPATH%\Application Data\NaClSDK (i.e., %APPDATA% on windows)
660 default=os.path.join(DEFAULT_SDK_ROOT, USER_DATA_DIR),
661 help="specify location of NaCl SDK's data directory")
662 parser.add_option(
663 '-s', '--sdk-root-dir', dest='sdk_root_dir',
664 default=DEFAULT_SDK_ROOT,
665 help="location where the SDK bundles are installed")
666 parser.add_option(
667 '-v', '--version', dest='show_version',
668 action='store_true',
669 help='show version information and exit')
670 parser.add_option(
671 '-m', '--manifest', dest='manifest_filename',
672 default=MANIFEST_FILENAME,
673 help="name of local manifest file relative to user-data-dir")
674 parser.add_option(
675 '-c', '--config', dest='config_filename',
676 default=CONFIG_FILENAME,
677 help="name of the local config file relative to user-data-dir")
678 parser.add_option(
679 '--update-sdk-tools', dest='update_sdk_tools',
680 default=False, action='store_true')
681
682
683 COMMANDS = {
684 'info': Info,
685 'list': List,
686 'update': Update,
687 'install': Update,
688 'sources': Sources,
689 }
690
691 # Separate global options from command-specific options
692 global_argv = argv
693 command_argv = []
694 for index, arg in enumerate(argv):
695 if arg in COMMANDS:
696 global_argv = argv[:index]
697 command_argv = argv[index:]
698 break
699
700 (options, args) = parser.parse_args(global_argv)
701 args += command_argv
702
703 global _debug_mode, _quiet_mode
704 _debug_mode = options.debug
705 _quiet_mode = options.quiet
706
707 def PrintHelpAndExit(unused_options=None, unused_args=None):
708 parser.print_help()
709 exit(1)
710
711 if options.update_sdk_tools:
712 # Ignore all other commands, and just update the sdk tools.
713 args = ['update', 'sdk_tools']
714 # Leave the rest of the options alone -- they may be needed to update
715 # correctly.
716 options.show_version = False
717 options.sdk_root_dir = DEFAULT_SDK_ROOT
718
719 if options.show_version:
720 print "Native Client SDK Updater, version r%s" % (REVISION,)
721 exit(0)
722
723
724 if not args:
725 print "Need to supply a command"
726 PrintHelpAndExit()
727
728 if options.help:
729 PrintHelpAndExit()
730
731 def DefaultHandler(unused_options=None, unused_args=None, unused_config=None):
732 print "Unknown Command: %s" % args[0]
733 PrintHelpAndExit()
734
735 def InvokeCommand(args):
736 command = COMMANDS.get(args[0], DefaultHandler)
737 # Load the config file before running commands
738 config = LoadFromFile(os.path.join(options.user_data_dir,
739 options.config_filename),
740 SDKConfig())
741 command(options, args[1:], config)
742
743 if args[0] == 'help':
744 if len(args) == 1:
745 PrintHelpAndExit()
746 else: 348 else:
747 InvokeCommand([args[1], '-h']) 349 loglevel = logging.WARNING
748 else: 350
749 # Make sure the user_data_dir exists. 351 fmt = '%(levelname)s:%(message)s'
750 if not os.path.exists(options.user_data_dir): 352 logging.basicConfig(stream=sys.stdout, level=loglevel, format=fmt)
751 os.makedirs(options.user_data_dir) 353
752 InvokeCommand(args) 354 # If --update-sdk-tools is passed, circumvent any other command running.
753 355 if options.update_sdk_tools:
754 return 0 # Success 356 UpdateSDKTools(options, args)
357 sys.exit(1)
358
359 return options, args
360 parser.parse_args = Parse
361
362 if argv:
363 cmd = Command(argv[0])
364 if cmd:
365 # "fix" the usage and the description now that we know the subcommand.
366 GenUsage(parser, argv[0])
367 return cmd(parser, argv[1:])
368
369 # Not a known command. Default to help.
370 GenUsage(parser, 'help')
371 return CMDhelp(parser, argv)
755 372
756 373
757 if __name__ == '__main__': 374 if __name__ == '__main__':
758 try: 375 try:
759 sys.exit(main(sys.argv[1:])) 376 sys.exit(main(sys.argv[1:]))
760 except Error as error: 377 except Error as e:
761 print "Error: %s" % error 378 logging.error(str(e))
762 sys.exit(1) 379 sys.exit(1)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698