swc-installation-test-2.py: Add MakeDependency class
[swc-setup-installation-test.git] / swc-installation-test-2.py
1 #!/usr/bin/env python
2
3 """Test script to check for required functionality.
4
5 Execute this code at the command line by typing:
6
7   python swc-installation-test-2.py
8
9 Run the script and follow the instructions it prints at the end.
10
11 This script requires at least Python 2.6.  You can check the version
12 of Python that you have installed with 'swc-installation-test-1.py'.
13 """
14
15 from __future__ import print_function  # for Python 2.6 compatibility
16
17 import distutils.ccompiler as _distutils_ccompiler
18 try:  # Python 2.7 and 3.x
19     import importlib as _importlib
20 except ImportError:  # Python 2.6 and earlier
21     class _Importlib (object):
22         """Minimal workarounds for functions we need
23         """
24         @staticmethod
25         def import_module(name):
26             module = __import__(name)
27             for n in name.split('.')[1:]:
28                 module = getattr(module, n)
29             return module
30     _importlib = _Importlib()
31 import logging as _logging
32 import os as _os
33 import platform as _platform
34 import re as _re
35 import shlex as _shlex
36 import subprocess as _subprocess
37 import sys as _sys
38
39
40 if not hasattr(_shlex, 'quote'):  # Python versions older than 3.3
41     # Use the undocumented pipes.quote()
42     import pipes as _pipes
43     _shlex.quote = _pipes.quote
44
45
46 __version__ = '0.1'
47
48 # Comment out any entries you don't need
49 CHECKS = [
50 # Shell
51     'virtual-shell',
52 # Editors
53     'virtual-editor',
54 # Browsers
55     'virtual-browser',
56 # Version control
57     'git',
58     'hg',              # Command line tool
59     'mercurial',       # Python package
60 # Build tools and packaging
61     'make',
62     'easy_install',
63     'setuptools',
64 # Testing
65     'nosetests',       # Command line tool
66     'nose',            # Python package
67 # SQL
68     'sqlite3',         # Command line tool
69     'sqlite3-python',  # Python package
70 # Python
71     'python',
72     'IPython',
73     'numpy',
74     'scipy',
75     'matplotlib',
76     'sympy',
77     'Cython',
78     'networkx',
79     'mayavi.mlab',
80     ]
81
82 CHECKER = {}
83
84
85 class DependencyError (Exception):
86     def _get_message(self):
87         return self._message
88     def _set_message(self, message):
89         self._message = message
90     message = property(_get_message, _set_message)
91
92     def __init__(self, checker, message):
93         super(DependencyError, self).__init__(message)
94         self.checker = checker
95         self.message = message
96
97     def __str__(self):
98         url = 'http://software-carpentry.org/setup/'  # TODO: per-package URL
99         return 'check for {0} failed:\n{1}\n{2}\n{3}'.format(
100             self.checker.full_name(), self.message,
101             'For instructions on installing an up-to-date version, see',
102             url)
103
104
105 def check(checks=None):
106     successes = []
107     failures = []
108     if not checks:
109         checks = CHECKS
110     for check in checks:
111         checker = CHECKER[check]
112         _sys.stdout.write('check {0}...\t'.format(checker.full_name()))
113         try:
114             version = checker.check()
115         except DependencyError as e:
116             failures.append(e)
117             _sys.stdout.write('fail\n')
118         else:
119             _sys.stdout.write('pass\n')
120             successes.append((checker, version))
121     if successes:
122         print('\nSuccesses:\n')
123         for checker,version in successes:
124             print('{0} {1}'.format(
125                     checker.full_name(),
126                     version or 'unknown'))
127     if failures:
128         print('\nFailures:')
129         printed = []
130         for failure in failures:
131             if failure not in printed:
132                 print()
133                 print(failure)
134                 printed.append(failure)
135         return False
136     return True
137
138
139 class Dependency (object):
140     def __init__(self, name, long_name=None, minimum_version=None,
141                  version_delimiter='.', and_dependencies=None,
142                  or_dependencies=None):
143         self.name = name
144         self.long_name = long_name or name
145         self.minimum_version = minimum_version
146         self.version_delimiter = version_delimiter
147         if not and_dependencies:
148             and_dependencies = []
149         self.and_dependencies = and_dependencies
150         if not or_dependencies:
151             or_dependencies = []
152         self.or_dependencies = or_dependencies
153         self._check_error = None
154
155     def __str__(self):
156         return '<{0} {1}>'.format(type(self).__name__, self.name)
157
158     def full_name(self):
159         if self.name == self.long_name:
160             return self.name
161         else:
162             return '{0} ({1})'.format(self.long_name, self.name)
163
164     def check(self):
165         if self._check_error:
166             raise self._check_error
167         try:
168             self._check_dependencies()
169             return self._check()
170         except DependencyError as e:
171             self._check_error = e  # cache for future calls
172             raise
173
174     def _check_dependencies(self):
175         for dependency in self.and_dependencies:
176             if not hasattr(dependency, 'check'):
177                 dependency = CHECKER[dependency]
178             dependency.check()
179         self.or_pass = or_error = None
180         for dependency in self.or_dependencies:
181             if not hasattr(dependency, 'check'):
182                 dependency = CHECKER[dependency]
183             try:
184                 version = dependency.check()
185             except DependencyError as e:
186                 or_error = e
187             else:
188                 self.or_pass = {
189                     'dependency': dependency,
190                     'version': version,
191                     }
192                 break  # no need to test other dependencies
193         if self.or_dependencies and not self.or_pass:
194             raise or_error
195
196     def _check(self):
197         version = self._get_version()
198         parsed_version = None
199         if hasattr(self, '_get_parsed_version'):
200             parsed_version = self._get_parsed_version()
201         if self.minimum_version:
202             self._check_version(version=version, parsed_version=parsed_version)
203         return version
204
205     def _get_version(self):
206         raise NotImplementedError(self)
207
208     def _check_version(self, version, parsed_version=None):
209         if not parsed_version:
210             parsed_version = self._parse_version(version=version)
211         if not parsed_version or parsed_version < self.minimum_version:
212             raise DependencyError(
213                 checker=self,
214                 message='outdated version of {0}: {1} (need >= {2})'.format(
215                     self.full_name(), version,
216                     self.version_delimiter.join(
217                         str(part) for part in self.minimum_version)))
218
219     def _parse_version(self, version):
220         if not version:
221             return None
222         parsed_version = []
223         for part in version.split(self.version_delimiter):
224             try:
225                 parsed_version.append(int(part))
226             except ValueError as e:
227                 raise NotImplementedError((version, part))# from e
228         return tuple(parsed_version)
229
230
231 class PythonDependency (Dependency):
232     def __init__(self, name='python', long_name='Python version',
233                  minimum_version=(2, 6), **kwargs):
234         super(PythonDependency, self).__init__(
235             name=name, long_name=long_name, minimum_version=minimum_version,
236             **kwargs)
237
238     def _get_version(self):
239         return _sys.version
240
241     def _get_parsed_version(self):
242         return _sys.version_info
243
244
245 CHECKER['python'] = PythonDependency()
246
247
248 class CommandDependency (Dependency):
249     exe_extension = _distutils_ccompiler.new_compiler().exe_extension
250
251     def __init__(self, command, version_options=('--version',), stdin=None,
252                  version_regexp=None, version_stream='stdout', **kwargs):
253         if 'name' not in kwargs:
254             kwargs['name'] = command
255         super(CommandDependency, self).__init__(**kwargs)
256         self.command = command
257         self.version_options = version_options
258         self.stdin = None
259         if not version_regexp:
260             regexp = r'([\d][\d{0}]*[\d])'.format(self.version_delimiter)
261             version_regexp = _re.compile(regexp)
262         self.version_regexp = version_regexp
263         self.version_stream = version_stream
264
265     def _get_version_stream(self, stdin=None, expect=(0,)):
266         if not stdin:
267             stdin = self.stdin
268         if stdin:
269             popen_stdin = _subprocess.PIPE
270         else:
271             popen_stdin = None
272         command = self.command + (self.exe_extension or '')
273         try:
274             p = _subprocess.Popen(
275                 [command] + list(self.version_options), stdin=popen_stdin,
276                 stdout=_subprocess.PIPE, stderr=_subprocess.PIPE,
277                 close_fds=True, shell=False, universal_newlines=True)
278         except OSError as e:
279             raise DependencyError(
280                 checker=self,
281                 message="could not find '{0}' executable".format(command),
282                 )# from e
283         stdout,stderr = p.communicate(stdin)
284         status = p.wait()
285         if status not in expect:
286             lines = [
287                 "failed to execute: {0} {1}".format(
288                     command,
289                     ' '.join(_shlex.quote(arg)
290                              for arg in self.version_options)),
291                 'status: {0}'.format(status),
292                 ]
293             for name,string in [('stdout', stdout), ('stderr', stderr)]:
294                 if string:
295                     lines.extend([name + ':', string])
296             raise DependencyError(checker=self, message='\n'.join(lines))
297         for name,string in [('stdout', stdout), ('stderr', stderr)]:
298             if name == self.version_stream:
299                 return string
300         raise NotImplementedError(self.version_stream)
301
302     def _get_version(self):
303         version_stream = self._get_version_stream()
304         match = self.version_regexp.search(version_stream)
305         if not match:
306             raise DependencyError(
307                 checker=self,
308                 message='no version string in output:\n{0}'.format(
309                     version_stream))
310         return match.group(1)
311
312
313 for command,long_name,minimum_version in [
314         ('sh', 'Bourne Shell', None),
315         ('ash', 'Almquist Shell', None),
316         ('bash', 'Bourne Again Shell', None),
317         ('csh', 'C Shell', None),
318         ('ksh', 'KornShell', None),
319         ('dash', 'Debian Almquist Shell', None),
320         ('tcsh', 'TENEX C Shell', None),
321         ('zsh', 'Z Shell', None),
322         ('git', 'Git', (1, 7, 0)),
323         ('hg', 'Mercurial', (2, 0, 0)),
324         ('sqlite3', 'SQLite 3', None),
325         ('nosetests', 'Nose', (1, 0, 0)),
326         ('emacs', 'Emacs', None),
327         ('xemacs', 'XEmacs', None),
328         ('vim', 'Vim', None),
329         ('vi', None, None),
330         ('nano', 'Nano', None),
331         ('kate', 'Kate', None),
332         ('notepad++', 'Notepad++', None),
333         ('firefox', 'Firefox', None),
334         ('google-chrome', 'Google Chrome', None),
335         ('chromium', 'Chromium', None),
336         ]:
337     if not long_name:
338         long_name = command
339     CHECKER[command] = CommandDependency(
340         command=command, long_name=long_name, minimum_version=minimum_version)
341 del command, long_name, minimum_version  # cleanup namespace
342
343
344 class MakeDependency (CommandDependency):
345     makefile = '\n'.join([
346             'all:',
347             '\t@echo "MAKE_VERSION=$(MAKE_VERSION)"',
348             '\t@echo "MAKE=$(MAKE)"',
349             '',
350             ])
351
352     def _get_version(self):
353         try:
354             return super(MakeDependency, self)._get_version()
355         except DependencyError as e:
356             version_options = self.version_options
357             self.version_options = ['-f', '-']
358             try:
359                 stream = self._get_version_stream(stdin=self.makefile)
360                 info = {}
361                 for line in stream.splitlines():
362                     try:
363                         key,value = line.split('=', 1)
364                     except ValueError as ve:
365                         raise e# from NotImplementedError(stream)
366                     info[key] = value
367                 if info.get('MAKE_VERSION', None):
368                     return info['MAKE_VERSION']
369                 elif info.get('MAKE', None):
370                     return None
371                 raise e
372             finally:
373                 self.version_options = version_options
374
375
376 CHECKER['make'] = MakeDependency(command='make', minimum_version=None)
377
378
379 class EasyInstallDependency (CommandDependency):
380     def _get_version(self):
381         try:
382             return super(EasyInstallDependency, self)._get_version()
383         except DependencyError as e:
384             version_stream = self.version_stream
385             try:
386                 self.version_stream = 'stderr'
387                 stream = self._get_version_stream(expect=(1,))
388                 if 'option --version not recognized' in stream:
389                     return 'unknown (possibly Setuptools?)'
390             finally:
391                 self.version_stream = version_stream
392
393
394 CHECKER['easy_install'] = EasyInstallDependency(
395     command='easy_install', long_name='Setuptools easy_install',
396     minimum_version=None)
397
398
399 class PythonPackageDependency (Dependency):
400     def __init__(self, package, **kwargs):
401         if 'name' not in kwargs:
402             kwargs['name'] = package
403         if 'and_dependencies' not in kwargs:
404             kwargs['and_dependencies'] = []
405         if 'python' not in kwargs['and_dependencies']:
406             kwargs['and_dependencies'].append('python')
407         super(PythonPackageDependency, self).__init__(**kwargs)
408         self.package = package
409
410     def _get_version(self):
411         package = self._get_package(self.package)
412         return self._get_version_from_package(package)
413
414     def _get_package(self, package):
415         try:
416             return _importlib.import_module(package)
417         except ImportError as e:
418             raise DependencyError(
419                 checker=self,
420                 message="could not import the '{0}' package for {1}".format(
421                     package, self.full_name()),
422                 )# from e
423
424     def _get_version_from_package(self, package):
425         try:
426             version = package.__version__
427         except AttributeError:
428             version = None
429         return version
430
431
432 for package,name,long_name,minimum_version in [
433         ('nose', None, 'Nose Python package',
434          CHECKER['nosetests'].minimum_version),
435         ('IPython', None, None, None),
436         ('numpy', None, 'NumPy', None),
437         ('scipy', None, 'SciPy', None),
438         ('matplotlib', None, 'Matplotlib', None),
439         ('sympy', None, 'SymPy', None),
440         ('Cython', None, None, None),
441         ('networkx', None, 'NetworkX', None),
442         ('mayavi.mlab', None, 'MayaVi', None),
443         ('setuptools', None, 'Setuptools', None),
444         ]:
445     if not name:
446         name = package
447     if not long_name:
448         long_name = name
449     CHECKER[name] = PythonPackageDependency(
450         package=package, name=name, long_name=long_name,
451         minimum_version=minimum_version)
452 del package, name, long_name, minimum_version  # cleanup namespace
453
454
455 class MercurialPythonPackage (PythonPackageDependency):
456     def _get_version(self):
457         try:  # mercurial >= 1.2
458             package = _importlib.import_module('mercurial.util')
459         except ImportError as e:  # mercurial <= 1.1.2
460             package = self._get_package('mercurial.version')
461             return package.get_version()
462         else:
463             return package.version()
464
465
466 CHECKER['mercurial'] = MercurialPythonPackage(
467     package='mercurial.util', name='mercurial',
468     long_name='Mercurial Python package',
469     minimum_version=CHECKER['hg'].minimum_version)
470
471
472 class SQLitePythonPackage (PythonPackageDependency):
473     def _get_version_from_package(self, package):
474         return _sys.version
475
476     def _get_parsed_version(self):
477         return _sys.version_info
478
479
480 CHECKER['sqlite3-python'] = SQLitePythonPackage(
481     package='sqlite3', name='sqlite3-python',
482     long_name='SQLite Python package',
483     minimum_version=CHECKER['sqlite3'].minimum_version)
484
485
486 class VirtualDependency (Dependency):
487     def _check(self):
488         return '{0} {1}'.format(
489             self.or_pass['dependency'].full_name(),
490             self.or_pass['version'])
491
492
493 for name,dependencies in [
494         ('virtual-shell', (
495             'bash',
496             'dash',
497             'ash',
498             'zsh',
499             'ksh',
500             'csh',
501             'tcsh',
502             'sh',
503             )),
504         ('virtual-editor', (
505             'emacs',
506             'xemacs',
507             'vim',
508             'vi',
509             'nano',
510             'kate',
511             'notepad++',
512             )),
513         ('virtual-browser', (
514             'firefox',
515             'google-chrome',
516             'chromium',
517             )),
518         ]:
519     CHECKER[name] = VirtualDependency(
520         name=name, long_name=name, or_dependencies=dependencies)
521 del name, dependencies  # cleanup namespace
522
523
524 def _print_info(key, value, indent=19):
525     print('{0}{1}: {2}'.format(key, ' '*(indent-len(key)), value))
526
527 def print_system_info():
528     print("If you do not understand why the above failures occurred,")
529     print("copy and send the *entire* output (all info above and summary")
530     print("below) to the instructor for help.")
531     print()
532     print('==================')
533     print('System information')
534     print('==================')
535     _print_info('os.name', _os.name)
536     _print_info('os.uname', _platform.uname())
537     _print_info('platform', _sys.platform)
538     _print_info('platform+', _platform.platform())
539     for pversion in (
540             'linux_distribution',
541             'mac_ver',
542             'win32_ver',
543             ):
544         value = getattr(_platform, pversion)()
545         if value[0]:
546             _print_info(pversion, value)
547     _print_info('prefix', _sys.prefix)
548     _print_info('exec_prefix', _sys.exec_prefix)
549     _print_info('executable', _sys.executable)
550     _print_info('version_info', _sys.version_info)
551     _print_info('version', _sys.version)
552     _print_info('environment', '')
553     for key,value in sorted(_os.environ.items()):
554         print('  {0}={1}'.format(key, value))
555     print('==================')
556
557 def print_suggestions(instructor_fallback=True):
558     print()
559     print('For suggestions on installing missing packages, see')
560     print('http://software-carpentry.org/setup/')
561     print('')
562     print('For instructings on installing a particular package,')
563     print('see the failure message for that package printed above.')
564     if instructor_fallback:
565         print('')
566         print('For help, email the *entire* output of this script to')
567         print('your instructor.')
568
569
570 if __name__ == '__main__':
571     if not check(_sys.argv[1:]):
572         print()
573         print_system_info()
574         print_suggestions(instructor_fallback=True)