1#!/usr/bin/env python 2# Copyright (c) 2012 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6# CMD code copied from git_cl.py in depot_tools. 7 8import config 9import cStringIO 10import download 11import logging 12import optparse 13import os 14import re 15import sdk_update_common 16from sdk_update_common import Error 17import sys 18import urllib2 19 20SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 21PARENT_DIR = os.path.dirname(SCRIPT_DIR) 22 23sys.path.append(os.path.dirname(SCRIPT_DIR)) 24import manifest_util 25 26 27# Import late so each command script can find our imports 28import command.info 29import command.list 30import command.sources 31import command.uninstall 32import command.update 33 34# This revision number is autogenerated from the Chrome revision. 35REVISION = '{REVISION}' 36 37GSTORE_URL = 'https://storage.googleapis.com/nativeclient-mirror' 38CONFIG_FILENAME = 'naclsdk_config.json' 39MANIFEST_FILENAME = 'naclsdk_manifest2.json' 40DEFAULT_SDK_ROOT = os.path.abspath(PARENT_DIR) 41USER_DATA_DIR = os.path.join(DEFAULT_SDK_ROOT, 'sdk_cache') 42 43 44def usage(more): 45 def hook(fn): 46 fn.usage_more = more 47 return fn 48 return hook 49 50 51def hide(fn): 52 fn.hide = True 53 return fn 54 55 56def LoadConfig(raise_on_error=False): 57 path = os.path.join(USER_DATA_DIR, CONFIG_FILENAME) 58 cfg = config.Config() 59 if not os.path.exists(path): 60 return cfg 61 62 try: 63 try: 64 with open(path) as f: 65 file_data = f.read() 66 except IOError as e: 67 raise Error('Unable to read config from "%s".\n %s' % (path, e)) 68 69 try: 70 cfg.LoadJson(file_data) 71 except Error as e: 72 raise Error('Parsing config file from "%s" failed.\n %s' % (path, e)) 73 return cfg 74 except Error as e: 75 if raise_on_error: 76 raise 77 else: 78 logging.warn(str(e)) 79 80 return cfg 81 82 83def WriteConfig(cfg): 84 path = os.path.join(USER_DATA_DIR, CONFIG_FILENAME) 85 try: 86 sdk_update_common.MakeDirs(USER_DATA_DIR) 87 except Exception as e: 88 raise Error('Unable to create directory "%s".\n %s' % (USER_DATA_DIR, e)) 89 90 cfg_json = cfg.ToJson() 91 92 try: 93 with open(path, 'w') as f: 94 f.write(cfg_json) 95 except IOError as e: 96 raise Error('Unable to write config to "%s".\n %s' % (path, e)) 97 98 99def LoadLocalManifest(raise_on_error=False): 100 path = os.path.join(USER_DATA_DIR, MANIFEST_FILENAME) 101 manifest = manifest_util.SDKManifest() 102 try: 103 try: 104 with open(path) as f: 105 manifest_string = f.read() 106 except IOError as e: 107 raise Error('Unable to read manifest from "%s".\n %s' % (path, e)) 108 109 try: 110 manifest.LoadDataFromString(manifest_string) 111 except Exception as e: 112 raise Error('Parsing local manifest "%s" failed.\n %s' % (path, e)) 113 except Error as e: 114 if raise_on_error: 115 raise 116 else: 117 logging.warn(str(e)) 118 return manifest 119 120 121def WriteLocalManifest(manifest): 122 path = os.path.join(USER_DATA_DIR, MANIFEST_FILENAME) 123 try: 124 sdk_update_common.MakeDirs(USER_DATA_DIR) 125 except Exception as e: 126 raise Error('Unable to create directory "%s".\n %s' % (USER_DATA_DIR, e)) 127 128 try: 129 manifest_json = manifest.GetDataAsString() 130 except Exception as e: 131 raise Error('Error encoding manifest "%s" to JSON.\n %s' % (path, e)) 132 133 try: 134 with open(path, 'w') as f: 135 f.write(manifest_json) 136 except IOError as e: 137 raise Error('Unable to write manifest to "%s".\n %s' % (path, e)) 138 139 140def LoadRemoteManifest(url): 141 manifest = manifest_util.SDKManifest() 142 url_stream = None 143 try: 144 manifest_stream = cStringIO.StringIO() 145 url_stream = download.UrlOpen(url) 146 download.DownloadAndComputeHash(url_stream, manifest_stream) 147 except urllib2.URLError as e: 148 raise Error('Unable to read remote manifest from URL "%s".\n %s' % ( 149 url, e)) 150 finally: 151 if url_stream: 152 url_stream.close() 153 154 try: 155 manifest.LoadDataFromString(manifest_stream.getvalue()) 156 return manifest 157 except manifest_util.Error as e: 158 raise Error('Parsing remote manifest from URL "%s" failed.\n %s' % ( 159 url, e,)) 160 161 162def LoadCombinedRemoteManifest(default_manifest_url, cfg): 163 manifest = LoadRemoteManifest(default_manifest_url) 164 for source in cfg.sources: 165 manifest.MergeManifest(LoadRemoteManifest(source)) 166 return manifest 167 168 169# Commands ##################################################################### 170 171 172@usage('<bundle names...>') 173def CMDinfo(parser, args): 174 """display information about a bundle""" 175 options, args = parser.parse_args(args) 176 if not args: 177 parser.error('No bundles given') 178 return 0 179 cfg = LoadConfig() 180 remote_manifest = LoadCombinedRemoteManifest(options.manifest_url, cfg) 181 command.info.Info(remote_manifest, args) 182 return 0 183 184 185def CMDlist(parser, args): 186 """list all available bundles""" 187 parser.add_option('-r', '--revision', action='store_true', 188 help='display revision numbers') 189 options, args = parser.parse_args(args) 190 if args: 191 parser.error('Unsupported argument(s): %s' % ', '.join(args)) 192 local_manifest = LoadLocalManifest() 193 cfg = LoadConfig() 194 remote_manifest = LoadCombinedRemoteManifest(options.manifest_url, cfg) 195 command.list.List(remote_manifest, local_manifest, options.revision) 196 return 0 197 198 199@usage('<bundle names...>') 200def CMDupdate(parser, args): 201 """update a bundle in the SDK to the latest version""" 202 parser.add_option('-F', '--force', action='store_true', 203 help='Force updating bundles that already exist. The bundle will not be ' 204 'updated if the local revision matches the remote revision.') 205 options, args = parser.parse_args(args) 206 local_manifest = LoadLocalManifest() 207 cfg = LoadConfig() 208 remote_manifest = LoadCombinedRemoteManifest(options.manifest_url, cfg) 209 210 if not args: 211 args = [command.update.RECOMMENDED] 212 213 try: 214 delegate = command.update.RealUpdateDelegate(USER_DATA_DIR, 215 DEFAULT_SDK_ROOT, cfg) 216 command.update.Update(delegate, remote_manifest, local_manifest, args, 217 options.force) 218 finally: 219 # Always write out the local manifest, we may have successfully updated one 220 # or more bundles before failing. 221 try: 222 WriteLocalManifest(local_manifest) 223 except Error as e: 224 # Log the error writing to the manifest, but propagate the original 225 # exception. 226 logging.error(str(e)) 227 228 return 0 229 230 231def CMDinstall(parser, args): 232 """install a bundle in the SDK""" 233 # For now, forward to CMDupdate. We may want different behavior for this 234 # in the future, though... 235 return CMDupdate(parser, args) 236 237 238@usage('<bundle names...>') 239def CMDuninstall(parser, args): 240 """uninstall the given bundles""" 241 _, args = parser.parse_args(args) 242 if not args: 243 parser.error('No bundles given') 244 return 0 245 local_manifest = LoadLocalManifest() 246 command.uninstall.Uninstall(DEFAULT_SDK_ROOT, local_manifest, args) 247 WriteLocalManifest(local_manifest) 248 return 0 249 250 251@usage('<bundle names...>') 252def CMDreinstall(parser, args): 253 """restore the given bundles to their original state 254 255 Note that if there is an update to a given bundle, reinstall will not 256 automatically update to the newest version. 257 """ 258 _, args = parser.parse_args(args) 259 local_manifest = LoadLocalManifest() 260 261 if not args: 262 parser.error('No bundles given') 263 return 0 264 265 cfg = LoadConfig() 266 try: 267 delegate = command.update.RealUpdateDelegate(USER_DATA_DIR, 268 DEFAULT_SDK_ROOT, cfg) 269 command.update.Reinstall(delegate, local_manifest, args) 270 finally: 271 # Always write out the local manifest, we may have successfully updated one 272 # or more bundles before failing. 273 try: 274 WriteLocalManifest(local_manifest) 275 except Error as e: 276 # Log the error writing to the manifest, but propagate the original 277 # exception. 278 logging.error(str(e)) 279 280 return 0 281 282 283def CMDsources(parser, args): 284 """manage external package sources""" 285 parser.add_option('-a', '--add', dest='url_to_add', 286 help='Add an additional package source') 287 parser.add_option( 288 '-r', '--remove', dest='url_to_remove', 289 help='Remove package source (use \'all\' for all additional sources)') 290 parser.add_option('-l', '--list', dest='do_list', action='store_true', 291 help='List additional package sources') 292 options, args = parser.parse_args(args) 293 294 cfg = LoadConfig(True) 295 write_config = False 296 if options.url_to_add: 297 command.sources.AddSource(cfg, options.url_to_add) 298 write_config = True 299 elif options.url_to_remove: 300 command.sources.RemoveSource(cfg, options.url_to_remove) 301 write_config = True 302 elif options.do_list: 303 command.sources.ListSources(cfg) 304 else: 305 parser.print_help() 306 307 if write_config: 308 WriteConfig(cfg) 309 310 return 0 311 312 313def CMDversion(parser, args): 314 """display version information""" 315 _, _ = parser.parse_args(args) 316 print "Native Client SDK Updater, version r%s" % REVISION 317 return 0 318 319 320def CMDhelp(parser, args): 321 """print list of commands or help for a specific command""" 322 _, args = parser.parse_args(args) 323 if len(args) == 1: 324 return main(args + ['--help']) 325 parser.print_help() 326 return 0 327 328 329def Command(name): 330 return globals().get('CMD' + name, None) 331 332 333def GenUsage(parser, cmd): 334 """Modify an OptParse object with the function's documentation.""" 335 obj = Command(cmd) 336 more = getattr(obj, 'usage_more', '') 337 if cmd == 'help': 338 cmd = '<command>' 339 else: 340 # OptParser.description prefer nicely non-formatted strings. 341 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__) 342 parser.set_usage('usage: %%prog %s [options] %s' % (cmd, more)) 343 344 345def UpdateSDKTools(options, args): 346 """update the sdk_tools bundle""" 347 348 local_manifest = LoadLocalManifest() 349 cfg = LoadConfig() 350 remote_manifest = LoadCombinedRemoteManifest(options.manifest_url, cfg) 351 352 try: 353 delegate = command.update.RealUpdateDelegate(USER_DATA_DIR, 354 DEFAULT_SDK_ROOT, cfg) 355 command.update.UpdateBundleIfNeeded( 356 delegate, 357 remote_manifest, 358 local_manifest, 359 command.update.SDK_TOOLS, 360 force=True) 361 finally: 362 # Always write out the local manifest, we may have successfully updated one 363 # or more bundles before failing. 364 WriteLocalManifest(local_manifest) 365 return 0 366 367 368def main(argv): 369 # Get all commands... 370 cmds = [fn[3:] for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')] 371 # Remove hidden commands... 372 cmds = filter(lambda fn: not getattr(Command(fn), 'hide', 0), cmds) 373 # Format for CMDhelp usage. 374 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([ 375 ' %-10s %s' % (fn, Command(fn).__doc__.split('\n')[0].strip()) 376 for fn in cmds])) 377 378 # Create the option parse and add --verbose support. 379 parser = optparse.OptionParser() 380 parser.add_option( 381 '-v', '--verbose', action='count', default=0, 382 help='Use 2 times for more debugging info') 383 parser.add_option('-U', '--manifest-url', dest='manifest_url', 384 default=GSTORE_URL + '/nacl/nacl_sdk/' + MANIFEST_FILENAME, 385 metavar='URL', help='override the default URL for the NaCl manifest file') 386 parser.add_option('--update-sdk-tools', action='store_true', 387 dest='update_sdk_tools', help=optparse.SUPPRESS_HELP) 388 389 old_parser_args = parser.parse_args 390 def Parse(args): 391 options, args = old_parser_args(args) 392 if options.verbose >= 2: 393 loglevel = logging.DEBUG 394 elif options.verbose: 395 loglevel = logging.INFO 396 else: 397 loglevel = logging.WARNING 398 399 fmt = '%(levelname)s:%(message)s' 400 logging.basicConfig(stream=sys.stdout, level=loglevel, format=fmt) 401 402 # If --update-sdk-tools is passed, circumvent any other command running. 403 if options.update_sdk_tools: 404 UpdateSDKTools(options, args) 405 sys.exit(1) 406 407 return options, args 408 parser.parse_args = Parse 409 410 if argv: 411 cmd = Command(argv[0]) 412 if cmd: 413 # "fix" the usage and the description now that we know the subcommand. 414 GenUsage(parser, argv[0]) 415 return cmd(parser, argv[1:]) 416 417 # Not a known command. Default to help. 418 GenUsage(parser, 'help') 419 return CMDhelp(parser, argv) 420 421 422if __name__ == '__main__': 423 try: 424 sys.exit(main(sys.argv[1:])) 425 except Error as e: 426 logging.error(str(e)) 427 sys.exit(1) 428 except KeyboardInterrupt: 429 sys.stderr.write('naclsdk: interrupted\n') 430 sys.exit(1) 431