| OLD | NEW |
| 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) |
| OLD | NEW |