1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Copyright 2017, Data61
5# Commonwealth Scientific and Industrial Research Organisation (CSIRO)
6# ABN 41 687 119 230.
7#
8# This software may be distributed and modified according to the terms of
9# the BSD 2-Clause license. Note that NO WARRANTY is provided.
10# See "LICENSE_BSD2.txt" for details.
11#
12# @TAG(DATA61_BSD)
13#
14
15'''
16Dependency checker for CAmkES.
17'''
18
19from __future__ import absolute_import, division, print_function, \
20    unicode_literals
21
22# This script can only import parts of the Python standard library, or it
23# becomes useless as a dependency checker.
24import abc, argparse, importlib, os, re, shutil, subprocess, sys, tempfile
25
26class CheckDepException(Exception):
27    pass
28
29class Package(object):
30
31    # Note that the following line has no effect in Python 3, but we just live
32    # with this rather than using the `six` wrapper as that would introduce a
33    # dependency on `six` that the user may not have installed.
34    __metaclass__ = abc.ABCMeta
35
36    def __init__(self, name, description):
37        self.name = name
38        self.description = description
39
40    @abc.abstractmethod
41    def exists(self):
42        raise NotImplementedError
43
44class Binary(Package):
45    def exists(self):
46        with open(os.devnull, 'wt') as f:
47            return subprocess.call(['which', self.name], stdout=f, stderr=f) \
48                == 0
49
50class Pylint(Binary):
51    def __init__(self, name, description, min_version):
52        super(Pylint, self).__init__(name, description)
53        self.min_version = min_version
54
55    def exists(self):
56        if not super(Pylint, self).exists():
57            return False
58        with open(os.devnull, 'wt') as f:
59            output = subprocess.check_output(['pylint', '--version'], stderr=f)
60        m = re.search(r'^pylint\s+(\d+\.\d+)', output, flags=re.MULTILINE)
61        if m is None:
62            raise CheckDepException('cannot determine version')
63        version = float(m.group(1))
64        if version < self.min_version:
65            raise CheckDepException('found version %0.1f but need at least '
66                'version %0.1f' % (version, self.min_version))
67        return True
68
69class PythonModule(Package):
70    def exists(self):
71        try:
72            importlib.import_module(self.name)
73            return True
74        except ImportError:
75            return False
76
77class PythonModuleWith(PythonModule):
78    def __init__(self, name, description, attr):
79        super(PythonModuleWith, self).__init__(name, description)
80        self.attr = attr
81
82    def exists(self):
83        if not super(PythonModuleWith, self).exists():
84            return False
85        mod = importlib.import_module(self.name)
86        if not hasattr(mod, self.attr):
87            raise CheckDepException('module exists, but %s.%s not found '
88                '(upgrade required?)' % (self.name, self.attr))
89        return True
90
91class CLibrary(Package):
92    def exists(self):
93        with open(os.devnull, 'wt') as f:
94            return subprocess.call(['pkg-config', '--cflags', self.name],
95                stdout=f, stderr=f) == 0
96
97class Or(Package):
98    def __init__(self, *packages):
99        self.name = ' or '.join(p.name for p in packages)
100        self.description = '...'
101        self.packages = packages
102
103    def exists(self):
104        return any(p.exists() for p in self.packages)
105
106class CUnit(Package):
107    def exists(self):
108        # CUnit is misconfigured for use with `pkg-config`, so test for its
109        # existence manually.
110        tmp = tempfile.mkdtemp()
111        try:
112            source = os.path.join(tmp, 'main.c')
113            with open(source, 'wt') as f:
114                f.write('''
115                    #include <CUnit/Basic.h>
116
117                    int main(void) {
118                        CU_initialize_registry();
119                        CU_cleanup_registry();
120                        return 0;
121                    }''')
122            with open(os.devnull, 'wt') as f:
123                subprocess.check_call(['gcc', 'main.c', '-lcunit'], cwd=tmp,
124                    stdout=f, stderr=f)
125            return True
126        except subprocess.CalledProcessError:
127            return False
128        finally:
129            shutil.rmtree(tmp)
130
131def green(string):
132    return '\033[32;1m%s\033[0m' % string
133
134def red(string):
135    return '\033[31;1m%s\033[0m' % string
136
137def yellow(string):
138    return '\033[33m%s\033[0m' % string
139
140DEPENDENCIES = {
141    'CAmkES runner':(PythonModule('jinja2', 'Python templating module'),
142                     PythonModule('plyplus', 'Python parsing module'),
143                     PythonModule('ply', 'Python parsing module'),
144                     PythonModule('elftools', 'Python ELF parsing module'),
145                     PythonModule('orderedset', 'Python OrderedSet module (orderedset)'),
146                     PythonModuleWith('six', 'Python 2/3 compatibility layer', 'assertCountEqual'),
147                     PythonModule('sqlite3', 'Python SQLite module'),
148                     PythonModule('pyfdt', 'Python flattened device tree parser')),
149    'seL4':(Binary('gcc', 'C compiler'),
150            PythonModule('tempita', 'Python templating module'),
151            Binary('xmllint', 'XML validator'),
152            Binary('bash', 'shell'),
153            Binary('make', 'GNU Make build tool'),
154            Binary('cpio', 'CPIO file system tool')),
155    'CapDL translator':(Binary('stack', 'Haskell version manager'),),
156    'CAmkES test suite':(Binary('expect', 'automation utility'),
157                         Pylint('pylint', 'Python linter', 1.4),
158                         Binary('qemu-system-arm', 'ARM emulator'),
159                         Binary('qemu-system-i386', 'IA32 emulator'),
160                         PythonModule('pycparser', 'Python C parsing module'),
161                         Binary('gcc', 'C compiler'),
162                         Binary('spin', 'model checker'),
163                         Binary('sha256sum', 'file hashing utility')),
164}
165
166EXTRAS = frozenset((
167    (Binary('sponge', 'input coalescer from moreutils'),
168        'installing this will give a marginal improvement in compilation times'),
169    (Binary('qemu-system-arm', 'ARM emulator'),
170        'this is required to simulate ARM systems'),
171    (Binary('qemu-system-i386', 'IA32 emulator'),
172        'this is required to simulate IA32 systems'),
173    (Binary('ccache', 'C compiler accelerator'),
174        'installing this will speed up your C compilation times'),
175    (Binary('clang-format', 'Clang code reformatter'),
176        'installing this will reflow generated C code to make it more readable'),
177    (CLibrary('ncurses', 'terminal menus library'),
178        'you will need to install this if you want to run menuconfig'),
179    (Or(Binary('arm-none-eabi-gcc', 'ARM C compiler'),
180        Binary('arm-linux-gnueabi-gcc', 'ARM C compiler'),
181        Binary('arm-linux-gnu-gcc', 'ARM C compiler')),
182        'you will need one of these if you want to target ARM systems'),
183    (Binary('pandoc', 'document format translator'),
184        'you will need this if you want to build the CAmkES documentation'),
185    (Binary('astyle', 'code reformater'),
186        'installing this will allow you to use the "style" Makefile targets to reformat C code'),
187    (Binary('c-parser', 'NICTA C-to-Simpl parser'),
188        'you will need this installed if you want to validate code for verification'),
189    (Or(Binary('arm-none-eabi-objdump', 'ARM disassembler'),
190        Binary('arm-linux-gnueabi-objdump', 'ARM disassembler'),
191        Binary('arm-linux-gnu-objdump', 'ARM disassembler')),
192        'installing one of these will speed up CapDL generation times for ARM builds'),
193    (Binary('objdump', 'disassembler'),
194        'installing this will speed up CapDL generation times for IA32 builds'),
195    (Binary('VBoxManage', 'VirtualBox administration tool'),
196        'you will need this installed if you want to build VMWare images'),
197    (Binary('syslinux', 'Linux bootloader tool'),
198        'you will need this installed if you want to build QEMU images for IA32'),
199    (Binary('mpartition', 'partitioning tool for MSDOS disks'),
200        'you will need this installed if you want to build QEMU images for IA32'),
201    (Binary('mformat', 'formatting tool for MSDOS disks'),
202        'you will need this installed if you want to build QEMU images for IA32'),
203    (Binary('mcopy', 'copying tool for MSDOS disks'),
204        'you will need this installed if you want to build QEMU images for IA32'),
205    (Binary('figleaf', 'code coverage tool for Python'),
206        'you will need this installed if you want to measure code coverage within CAmkES'),
207    (Binary('python-coverage', 'code coverage tool for Python'),
208        'you will need this installed if you want to measure code coverage within CAmkES'),
209    (Binary('clang', 'C compiler'),
210        'you will need this installed to efficiently use large DMA pools on ARM'),
211
212))
213
214def main(argv):
215    parser = argparse.ArgumentParser(description='CAmkES dependency checker')
216    parser.add_argument('--component', '-c', action='append',
217        choices=list(DEPENDENCIES.keys()), help='component whose dependecies '
218        'should be checked (default: all)')
219    options = parser.parse_args(argv[1:])
220
221    ret = 0
222
223    for k, v in sorted(DEPENDENCIES.items()):
224        if options.component is not None and k not in options.component:
225            continue
226        ok = True
227        sys.stdout.write('Dependencies of %s\n' % k)
228        for p in v:
229            sys.stdout.write(' %s (%s)... ' % (p.name, p.description))
230            try:
231                if p.exists():
232                    sys.stdout.write(green('Found\n'))
233                else:
234                    raise CheckDepException('Not found')
235            except CheckDepException as e:
236                ok = False
237                ret = -1
238                sys.stdout.write(red('%s\n' % e))
239        if not ok:
240            sys.stdout.write(red('You will not be able to build/run this component\n'))
241        sys.stdout.write('\n')
242
243    printed_header = False
244    for p, note in EXTRAS:
245        if not p.exists():
246            if not printed_header:
247                sys.stdout.write('Suggestions:\n')
248                printed_header = True
249            sys.stdout.write(yellow(' %s (%s): %s\n' % (p.name, p.description, note)))
250
251    return ret
252
253if __name__ == '__main__':
254    sys.exit(main(sys.argv))
255