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

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: windows fixes Created 8 years, 2 months 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 # pylint thinks this is commands from the python library.
30 import commands
31
32 # Module commands has no member... (this is because pylint thinks commands is
33 # from the python standard library).
34 # pylint: disable=E1101
29 35
30 # This revision number is autogenerated from the Chrome revision. 36 # This revision number is autogenerated from the Chrome revision.
31 REVISION = '{REVISION}' 37 REVISION = '{REVISION}'
32 38
33 GLOBAL_HELP = '''Usage: naclsdk [options] command [command_options] 39 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' 40 CONFIG_FILENAME = 'naclsdk_config.json'
55 MANIFEST_FILENAME = 'naclsdk_manifest2.json' 41 MANIFEST_FILENAME = 'naclsdk_manifest2.json'
56 SDK_TOOLS = 'sdk_tools' # the name for this tools directory 42 DEFAULT_SDK_ROOT = os.path.abspath(PARENT_DIR)
57 USER_DATA_DIR = 'sdk_cache' 43 USER_DATA_DIR = os.path.join(DEFAULT_SDK_ROOT, 'sdk_cache')
58 44
59 HTTP_CONTENT_LENGTH = 'Content-Length' # HTTP Header field for content length 45
60 46 def usage(more):
61 47 def hook(fn):
62 #------------------------------------------------------------------------------ 48 fn.usage_more = more
63 # General Utilities 49 return fn
64 50 return hook
65 51
66 _debug_mode = False 52
67 _quiet_mode = False 53 def hide(fn):
68 54 fn.hide = True
69 55 return fn
70 def DebugPrint(msg): 56
71 '''Display a message to stderr if debug printing is enabled 57
72 58 def LoadConfig(raise_on_error=False):
73 Note: This function appends a newline to the end of the string 59 path = os.path.join(USER_DATA_DIR, CONFIG_FILENAME)
74 60 if not os.path.exists(path):
75 Args: 61 return config.Config()
76 msg: A string to send to stderr in debug mode''' 62
77 if _debug_mode: 63 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: 64 try:
139 tar_file = cygtar.CygTar(installer, 'r', verbose=True) 65 with open(path) as f:
140 if outdir: 66 return config.Config(json.loads(f.read()))
141 os.chdir(outdir) 67 except IOError as e:
142 tar_file.Extract() 68 raise Error('Unable to read config from "%s".\n %s' % (path, e))
143 finally: 69 except Exception as e:
144 if tar_file: 70 raise Error('Parsing config file from "%s" failed.\n %s' % (path, e))
145 tar_file.Close() 71 except Error as e:
146 os.chdir(curpath) 72 if raise_on_error:
147 73 raise
148 74 else:
149 class ProgressFunction(object): 75 logging.warn(str(e))
150 '''Create a progress function for a file with a given size''' 76 return config.Config()
151 77
152 def __init__(self, file_size=0): 78
153 '''Constructor 79 def WriteConfig(cfg):
154 80 path = os.path.join(USER_DATA_DIR, CONFIG_FILENAME)
155 Args: 81 try:
156 file_size: number of bytes in file. 0 indicates unknown''' 82 sdk_update_common.MakeDirs(USER_DATA_DIR)
157 self.dots = 0 83 except Exception as e:
158 self.file_size = int(file_size) 84 raise Error('Unable to create directory "%s".\n %s' % (USER_DATA_DIR, e))
159 85
160 def GetProgressFunction(self): 86 try:
161 '''Returns a progress function based on a known file size''' 87 cfg_json = cfg.ToJson()
162 def ShowKnownProgress(progress): 88 except Exception as e:
163 if progress == 0: 89 raise Error('Json encoding error writing config "%s".\n %s' % (path, e))
164 sys.stdout.write('|%s|\n' % ('=' * 48)) 90
165 else: 91 try:
166 new_dots = progress * 50 / self.file_size - self.dots 92 with open(path, 'w') as f:
167 sys.stdout.write('.' * new_dots) 93 f.write(cfg_json)
168 self.dots += new_dots 94 except IOError as e:
169 if progress == self.file_size: 95 raise Error('Unable to write config to "%s".\n %s' % (path, e))
170 sys.stdout.write('\n') 96
171 sys.stdout.flush() 97
172 98 def LoadLocalManifest(raise_on_error=False):
173 return ShowKnownProgress 99 path = os.path.join(USER_DATA_DIR, MANIFEST_FILENAME)
174 100 manifest = manifest_util.SDKManifest()
175 101 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: 102 try:
192 from_stream = UrlOpen(archive.url) 103 with open(path) as f:
193 except urllib2.URLError: 104 manifest_string = f.read()
194 raise Error('Cannot open "%s" for archive %s' % 105 except IOError as e:
195 (archive.url, archive.host_os)) 106 raise Error('Unable to read manifest from "%s".\n %s' % (path, e))
107
196 try: 108 try:
197 content_length = int(from_stream.info()[HTTP_CONTENT_LENGTH]) 109 manifest.LoadDataFromString(manifest_string)
198 progress_function = ProgressFunction(content_length).GetProgressFunction() 110 except Exception as e:
199 InfoPrint('Downloading %s' % archive.url) 111 raise Error('Parsing local manifest "%s" failed.\n %s' % (path, e))
200 sha1, size = manifest_util.DownloadAndComputeHash( 112 except Error as e:
201 from_stream, 113 if raise_on_error:
202 to_stream=to_stream, 114 raise
203 progress_func=progress_function) 115 else:
204 if size != content_length: 116 logging.warn(str(e))
205 raise Error('Download size mismatch for %s.\n' 117 return manifest
206 'Expected %s bytes but got %s' % 118
207 (archive.url, content_length, size)) 119
208 finally: 120 def WriteLocalManifest(manifest):
209 if from_stream: 121 path = os.path.join(USER_DATA_DIR, MANIFEST_FILENAME)
210 from_stream.close() 122 try:
211 return sha1, size 123 sdk_update_common.MakeDirs(USER_DATA_DIR)
212 124 except Exception as e:
213 125 raise Error('Unable to create directory "%s".\n %s' % (USER_DATA_DIR, e))
214 def LoadFromFile(path, obj): 126
215 '''Returns a manifest loaded from the JSON file at |path|. 127 try:
216 128 manifest_json = manifest.GetDataAsString()
217 If the path does not exist or is invalid, returns unmodified object.''' 129 except Exception as e:
218 methodlist = [m for m in dir(obj) if callable(getattr(obj, m))] 130 raise Error('Error encoding manifest "%s" to JSON.\n %s' % (path, e))
219 if 'LoadDataFromString' not in methodlist: 131
220 return obj 132 try:
221 if not os.path.exists(path): 133 with open(path, 'w') as f:
222 return obj 134 f.write(manifest_json)
223 135 except IOError as e:
224 with open(path, 'r') as f: 136 raise Error('Unable to write manifest to "%s".\n %s' % (path, e))
225 json_string = f.read() 137
226 if not json_string: 138
227 return obj 139 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() 140 manifest = manifest_util.SDKManifest()
236 for url in urls: 141 url_stream = None
142 try:
143 manifest_stream = cStringIO.StringIO()
144 url_stream = download.UrlOpen(url)
145 download.DownloadAndComputeHash(url_stream, manifest_stream)
146 except urllib2.URLError as e:
147 raise Error('Unable to read remote manifest from URL "%s".\n %s' % (
148 url, e))
149 finally:
150 if url_stream:
151 url_stream.close()
152
153 try:
154 manifest.LoadDataFromString(manifest_stream.getvalue())
155 return manifest
156 except manifest_util.Error as e:
157 raise Error('Parsing remote manifest from URL "%s" failed.\n %s' % (
158 url, e,))
159
160
161 def LoadCombinedRemoteManifest(default_manifest_url, cfg):
162 manifest = LoadRemoteManifest(default_manifest_url)
163 for source in cfg.sources:
164 manifest.MergeManifest(LoadRemoteManifest(source))
165 return manifest
166
167
168 # Commands #####################################################################
169
170
171 @usage('<bundle names...>')
172 def CMDinfo(parser, args):
173 """display information about a bundle"""
174 options, args = parser.parse_args(args)
175 if len(args) == 0:
176 parser.error('No bundles given')
177 return 0
178 cfg = LoadConfig()
179 remote_manifest = LoadCombinedRemoteManifest(options.manifest_url, cfg)
180 commands.Info(remote_manifest, args)
181 return 0
182
183
184 def CMDlist(parser, args):
185 """list all available bundles"""
186 parser.add_option('-r', '--revision', action='store_true',
187 help='display revision numbers')
188 options, args = parser.parse_args(args)
189 if args:
190 parser.error('Unsupported argument(s): %s' % ', '.join(args))
191 local_manifest = LoadLocalManifest()
192 cfg = LoadConfig()
193 remote_manifest = LoadCombinedRemoteManifest(options.manifest_url, cfg)
194 commands.List(remote_manifest, local_manifest, options.revision)
195 return 0
196
197
198 @usage('<bundle names...>')
199 def CMDupdate(parser, args):
200 """update a bundle in the SDK to the latest version"""
201 parser.add_option(
202 '-F', '--force', action='store_true',
203 help='Force updating existing components that already exist')
204 options, args = parser.parse_args(args)
205 local_manifest = LoadLocalManifest()
206 cfg = LoadConfig()
207 remote_manifest = LoadCombinedRemoteManifest(options.manifest_url, cfg)
208
209 if not args:
210 args = [commands.RECOMMENDED]
211
212 try:
213 delegate = commands.RealUpdateDelegate(USER_DATA_DIR, DEFAULT_SDK_ROOT)
214 commands.Update(delegate, remote_manifest, local_manifest, args,
215 options.force)
216 finally:
217 # Always write out the local manifest, we may have successfully updated one
218 # or more bundles before failing.
237 try: 219 try:
238 url_stream = UrlOpen(url) 220 WriteLocalManifest(local_manifest)
239 except urllib2.URLError as e: 221 except Error as e:
240 raise Error('Unable to open %s. [%s]' % (url, e)) 222 # Log the error writing to the manifest, but propagate the original
241 223 # exception.
242 manifest_stream = cStringIO.StringIO() 224 logging.error(str(e))
243 manifest_util.DownloadAndComputeHash(url_stream, manifest_stream) 225
244 temp_manifest = manifest_util.SDKManifest() 226 return 0
245 temp_manifest.LoadDataFromString(manifest_stream.getvalue()) 227
246 228
247 manifest.MergeManifest(temp_manifest) 229 def CMDinstall(parser, args):
248 230 """install a bundle in the SDK"""
249 def BundleFilter(bundle): 231 # For now, forward to CMDupdate. We may want different behavior for this
250 # Only add this bundle if it's supported on this platform. 232 # in the future, though...
251 return bundle.GetHostOSArchive() 233 return CMDupdate(parser, args)
252 234
253 manifest.FilterBundles(BundleFilter) 235
254 return manifest 236 def CMDsources(parser, args):
255 237 """manage external package sources"""
256 238 parser.add_option('-a', '--add', dest='url_to_add',
257 def WriteToFile(path, obj): 239 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( 240 parser.add_option(
601 '-r', '--remove', dest='url_to_remove', 241 '-r', '--remove', dest='url_to_remove',
602 default=None,
603 help='Remove package source (use \'all\' for all additional sources)') 242 help='Remove package source (use \'all\' for all additional sources)')
604 parser.add_option( 243 parser.add_option('-l', '--list', dest='do_list', action='store_true',
605 '-l', '--list', dest='do_list', 244 help='List additional package sources')
606 default=False, action='store_true', 245 options, args = parser.parse_args(args)
607 help='List additional package sources') 246
608 source_options, _ = parser.parse_args(argv) 247 cfg = LoadConfig(True)
609
610 write_config = False 248 write_config = False
611 if source_options.url_to_add: 249 if options.url_to_add:
612 config.AddSource(source_options.url_to_add) 250 commands.AddSource(cfg, options.url_to_add)
613 write_config = True 251 write_config = True
614 elif source_options.url_to_remove: 252 elif options.url_to_remove:
615 if source_options.url_to_remove == 'all': 253 commands.RemoveSource(cfg, options.url_to_remove)
616 config.RemoveAllSources()
617 else:
618 config.RemoveSource(source_options.url_to_remove)
619 write_config = True 254 write_config = True
620 elif source_options.do_list: 255 elif options.do_list:
621 config.ListSources() 256 commands.ListSources(cfg)
622 else: 257 else:
623 parser.print_help() 258 parser.print_help()
624 259
625 if write_config: 260 if write_config:
626 WriteToFile(os.path.join(options.user_data_dir, options.config_filename), 261 WriteConfig(cfg)
627 config) 262
628 263 return 0
629 #------------------------------------------------------------------------------ 264
630 # Command-line interface 265
266 def CMDversion(parser, args):
267 """display version information"""
268 _, _ = parser.parse_args(args)
269 print "Native Client SDK Updater, version r%s" % REVISION
270 return 0
271
272
273 def CMDhelp(parser, args):
274 """print list of commands or help for a specific command"""
275 _, args = parser.parse_args(args)
276 if len(args) == 1:
277 return main(args + ['--help'])
278 parser.print_help()
279 return 0
280
281
282 def Command(name):
283 return globals().get('CMD' + name, None)
284
285
286 def GenUsage(parser, command):
287 """Modify an OptParse object with the function's documentation."""
288 obj = Command(command)
289 more = getattr(obj, 'usage_more', '')
290 if command == 'help':
291 command = '<command>'
292 else:
293 # OptParser.description prefer nicely non-formatted strings.
294 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
295 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
296
297
298 def UpdateSDKTools(options, args):
299 """update the sdk_tools bundle"""
300
301 local_manifest = LoadLocalManifest()
302 cfg = LoadConfig()
303 remote_manifest = LoadCombinedRemoteManifest(options.manifest_url, cfg)
304
305 try:
306 delegate = commands.RealUpdateDelegate(USER_DATA_DIR, DEFAULT_SDK_ROOT)
307 commands.UpdateBundleIfNeeded(delegate, remote_manifest, local_manifest,
308 commands.SDK_TOOLS, force=True)
309 finally:
310 # Always write out the local manifest, we may have successfully updated one
311 # or more bundles before failing.
312 WriteLocalManifest(local_manifest)
313 return 0
631 314
632 315
633 def main(argv): 316 def main(argv):
634 '''Main entry for the sdk_update utility''' 317 # Get all commands...
635 parser = optparse.OptionParser(usage=GLOBAL_HELP, add_help_option=False) 318 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__))) 319 # Remove hidden commands...
637 320 cmds = filter(lambda fn: not getattr(Command(fn), 'hide', 0), cmds)
638 # Manually add help options so we can ignore it when auto-updating. 321 # Format for CMDhelp usage.
322 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
323 ' %-10s %s' % (fn, Command(fn).__doc__.split('\n')[0].strip())
324 for fn in cmds]))
325
326 # Create the option parse and add --verbose support.
327 parser = optparse.OptionParser()
639 parser.add_option( 328 parser.add_option(
640 '-h', '--help', dest='help', action='store_true', 329 '-v', '--verbose', action='count', default=0,
641 help='show this help message and exit') 330 help='Use 2 times for more debugging info')
642 parser.add_option( 331 parser.add_option('-U', '--manifest-url', dest='manifest_url',
643 '-U', '--manifest-url', dest='manifest_url', 332 default=GSTORE_URL + '/nacl/nacl_sdk/' + MANIFEST_FILENAME,
644 default='https://commondatastorage.googleapis.com/nativeclient-mirror/' 333 metavar='URL', help='override the default URL for the NaCl manifest file')
645 'nacl/nacl_sdk/%s' % MANIFEST_FILENAME, 334 parser.add_option('--update-sdk-tools', action='store_true',
646 help='override the default URL for the NaCl manifest file') 335 dest='update_sdk_tools', help=optparse.SUPPRESS_HELP)
647 parser.add_option( 336
648 '-d', '--debug', dest='debug', 337 old_parser_args = parser.parse_args
649 default=False, action='store_true', 338 def Parse(args):
650 help='enable displaying debug information to stderr') 339 options, args = old_parser_args(args)
651 parser.add_option( 340 if options.verbose >= 2:
652 '-q', '--quiet', dest='quiet', 341 loglevel = logging.DEBUG
653 default=False, action='store_true', 342 elif options.verbose:
654 help='suppress displaying informational prints to stdout') 343 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: 344 else:
747 InvokeCommand([args[1], '-h']) 345 loglevel = logging.WARNING
748 else: 346
749 # Make sure the user_data_dir exists. 347 fmt = '%(levelname)s:%(message)s'
750 if not os.path.exists(options.user_data_dir): 348 logging.basicConfig(stream=sys.stdout, level=loglevel, format=fmt)
751 os.makedirs(options.user_data_dir) 349
752 InvokeCommand(args) 350 # If --update-sdk-tools is passed, circumvent any other command running.
753 351 if options.update_sdk_tools:
754 return 0 # Success 352 UpdateSDKTools(options, args)
353 sys.exit(1)
354
355 return options, args
356 parser.parse_args = Parse
357
358 if argv:
359 command = Command(argv[0])
360 if command:
361 # "fix" the usage and the description now that we know the subcommand.
362 GenUsage(parser, argv[0])
363 return command(parser, argv[1:])
364
365 # Not a known command. Default to help.
366 GenUsage(parser, 'help')
367 return CMDhelp(parser, argv)
755 368
756 369
757 if __name__ == '__main__': 370 if __name__ == '__main__':
758 try: 371 try:
759 sys.exit(main(sys.argv[1:])) 372 sys.exit(main(sys.argv[1:]))
760 except Error as error: 373 except Error as e:
761 print "Error: %s" % error 374 logging.error(str(e))
762 sys.exit(1) 375 sys.exit(1)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698