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