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