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