1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4#
5# Copyright 2017, Data61
6# Commonwealth Scientific and Industrial Research Organisation (CSIRO)
7# ABN 41 687 119 230.
8#
9# This software may be distributed and modified according to the terms of
10# the BSD 2-Clause license. Note that NO WARRANTY is provided.
11# See "LICENSE_BSD2.txt" for details.
12#
13# @TAG(DATA61_BSD)
14#
15
16'''Entry point for the runner (template instantiator).'''
17from __future__ import absolute_import, division, print_function, \
18    unicode_literals
19
20import \
21    six, string, sys, traceback, pickle
22import re
23import os
24import numbers
25import locale
26import jinja2
27import functools
28import argparse
29from capdl import ObjectType, ObjectAllocator, CSpaceAllocator, \
30    lookup_architecture, AddressSpaceAllocator
31from camkes.runner.Renderer import Renderer
32from capdl.Allocator import RenderState
33from camkes.internal.exception import CAmkESError
34import camkes.internal.log as log
35from camkes.templates import TemplateError
36from simpleeval import simple_eval
37
38import yaml
39
40from camkes.internal.seven import zip
41
42# Excuse this horrible prelude. When running under a different interpreter we
43# have an import path that doesn't include dependencies like elftools and
44# Jinja. We need to juggle the import path prior to importing them. Note, this
45# code has no effect when running under the standard Python interpreter.
46import platform
47import subprocess
48import sys
49
50from capdl.Object import register_object_sizes
51
52if platform.python_implementation() != 'CPython':
53    path = eval(subprocess.check_output(['python', '-c',
54                                         'import sys; sys.stdout.write(\'%s\' % sys.path)'],
55                                        universal_newlines=True))
56    for p in path:
57        if p not in sys.path:
58            sys.path.append(p)
59
60
61def _die(options, message):
62
63    if isinstance(message, (list, tuple)):
64        for line in message:
65            log.error(line)
66    else:
67        log.error(message)
68
69    tb = traceback.format_exc()
70    log.debug('\n --- Python traceback ---\n%s ------------------------\n' % tb)
71    sys.exit(-1)
72
73
74def parse_args(argv, out, err):
75    parser = argparse.ArgumentParser(prog='python -m camkes.runner',
76                                     description='instantiate templates based on a CAmkES specification')
77    parser.add_argument('--quiet', '-q', help='No diagnostics.', dest='verbosity',
78                        default=1, action='store_const', const=0)
79    parser.add_argument('--verbose', '-v', help='Verbose diagnostics.',
80                        dest='verbosity', action='store_const', const=2)
81    parser.add_argument('--debug', '-D', help='Extra verbose diagnostics.',
82                        dest='verbosity', action='store_const', const=3)
83    parser.add_argument('--outfile', '-O', help='Output to the given file.',
84                        type=argparse.FileType('w'), required=True, action='append', default=[])
85    parser.add_argument('--verification-base-name', type=str,
86                        help='Prefix to use when generating Isabelle theory files.')
87    parser.add_argument('--item', '-T', help='AST entity to produce code for.',
88                        required=True, action='append', default=[])
89    parser.add_argument('--template', help='template to use to produce code.',
90                        required=True, action='append', default=[])
91    parser.add_argument('--templates', '-t', help='Extra directories to '
92                        'search for templates (before builtin templates).', action='append',
93                        default=[])
94    parser.add_argument('--frpc-lock-elision', action='store_true',
95                        default=True, help='Enable lock elision optimisation in seL4RPC '
96                        'connector.')
97    parser.add_argument('--fno-rpc-lock-elision', action='store_false',
98                        dest='frpc_lock_elision', help='Disable lock elision optimisation in '
99                        'seL4RPC connector.')
100    parser.add_argument('--fprovide-tcb-caps', action='store_true',
101                        default=True, help='Hand out TCB caps to components, allowing them to '
102                        'exit cleanly.')
103    parser.add_argument('--fno-provide-tcb-caps', action='store_false',
104                        dest='fprovide_tcb_caps', help='Do not hand out TCB caps, causing '
105                        'components to fault on exiting.')
106    parser.add_argument('--default-priority', type=int, default=254,
107                        help='Default component thread priority.')
108    parser.add_argument('--default-max-priority', type=int, default=254,
109                        help='Default component thread maximum priority.')
110    parser.add_argument('--default-affinity', type=int, default=0,
111                        help='Default component thread affinity.')
112    parser.add_argument('--default-period', type=int, default=10000,
113                        help='Default component thread scheduling context period.')
114    parser.add_argument('--default-budget', type=int, default=10000,
115                        help='Default component thread scheduling context budget.')
116    parser.add_argument('--default-data', type=int, default=0,
117                        help='Default component thread scheduling context data.')
118    parser.add_argument('--default-size_bits', type=int, default=8,
119                        help='Default scheduling context size bits.')
120    parser.add_argument('--default-stack-size', type=int, default=16384,
121                        help='Default stack size of each thread.')
122    parser.add_argument('--largeframe', action='store_true',
123                        help='Use large frames (for non-DMA pools) when possible.')
124    parser.add_argument('--architecture', '--arch', default='aarch32',
125                        type=lambda x: type('')(x).lower(),
126                        choices=('aarch32', 'arm_hyp', 'ia32', 'x86_64',
127                                 'aarch64', 'riscv32', 'riscv64'),
128                        help='Target architecture.')
129    parser.add_argument('--makefile-dependencies', '-MD',
130                        type=argparse.FileType('w'), help='Write Makefile dependency rule to '
131                        'FILE')
132    parser.add_argument('--debug-fault-handlers', action='store_true',
133                        help='Provide fault handlers to decode cap and VM faults for '
134                        'debugging purposes.')
135    parser.add_argument('--largeframe-dma', action='store_true',
136                        help='Use large frames for DMA pools when possible.')
137    parser.add_argument('--realtime', action='store_true',
138                        help='Target realtime seL4.')
139    parser.add_argument('--object-sizes', type=argparse.FileType('r'),
140                        help='YAML file specifying the object sizes for any seL4 objects '
141                        'used in this invocation of the runner.')
142
143    object_state_group = parser.add_mutually_exclusive_group()
144    object_state_group.add_argument('--load-object-state', type=argparse.FileType('rb'),
145                                    help='Load previously-generated cap and object state.')
146    object_state_group.add_argument('--save-object-state', type=argparse.FileType('wb'),
147                                    help='Save generated cap and object state to this file.')
148
149    # To get the AST, there should be either a pickled AST or a file to parse
150    parser.add_argument('--load-ast', type=argparse.FileType('rb'),
151                        help='Load specification AST from this file.', required=True)
152
153    # Juggle the standard streams either side of parsing command-line arguments
154    # because argparse provides no mechanism to control this.
155    old_out = sys.stdout
156    old_err = sys.stderr
157    sys.stdout = out
158    sys.stderr = err
159    options = parser.parse_args(argv[1:])
160
161    sys.stdout = old_out
162    sys.stderr = old_err
163
164    # Check that verification_base_name would be a valid identifer before
165    # our templates try to use it
166    if options.verification_base_name is not None:
167        if not re.match(r'[a-zA-Z][a-zA-Z0-9_]*$', options.verification_base_name):
168            parser.error('Not a valid identifer for --verification-base-name: %r' %
169                         options.verification_base_name)
170
171    return options
172
173
174def rendering_error(item, exn):
175    """Helper to format an error message for template rendering errors."""
176    tb = '\n'.join(traceback.format_tb(sys.exc_info()[2]))
177    return (['While rendering %s: %s' % (item, line) for line in exn.args] +
178            ''.join(tb).splitlines())
179
180
181def main(argv, out, err):
182
183    # We need a UTF-8 locale, so bail out if we don't have one. More
184    # specifically, things like the version() computation traverse the file
185    # system and, if they hit a UTF-8 filename, they try to decode it into your
186    # preferred encoding and trigger an exception.
187    encoding = locale.getpreferredencoding().lower()
188    if encoding not in ('utf-8', 'utf8'):
189        err.write('CAmkES uses UTF-8 encoding, but your locale\'s preferred '
190                  'encoding is %s. You can override your locale with the LANG '
191                  'environment variable.\n' % encoding)
192        return -1
193
194    options = parse_args(argv, out, err)
195
196    # register object sizes with loader
197    if options.object_sizes:
198        register_object_sizes(yaml.load(options.object_sizes, Loader=yaml.FullLoader))
199
200    # Ensure we were supplied equal items, outfiles and templates
201    if len(options.outfile) != len(options.item) != len(options.template):
202        err.write('Different number of items and outfiles. Required one outfile location '
203                  'per item requested.\n')
204        return -1
205
206    # No duplicates in outfiles
207    if len(set(options.outfile)) != len(options.outfile):
208        err.write('Duplicate outfiles requrested through --outfile.\n')
209        return -1
210
211    # Save us having to pass debugging everywhere.
212    die = functools.partial(_die, options)
213
214    log.set_verbosity(options.verbosity)
215
216    ast = pickle.load(options.load_ast)
217
218    # Locate the assembly.
219    assembly = ast.assembly
220    if assembly is None:
221        die('No assembly found')
222
223    # Do some extra checks if the user asked for verbose output.
224    if options.verbosity >= 2:
225
226        # Try to catch type mismatches in attribute settings. Note that it is
227        # not possible to conclusively evaluate type correctness because the
228        # attributes' type system is (deliberately) too loose. That is, the
229        # type of an attribute can be an uninterpreted C type the user will
230        # provide post hoc.
231        for i in assembly.composition.instances:
232            for a in i.type.attributes:
233                value = assembly.configuration[i.name].get(a.name)
234                if value is not None:
235                    if a.type == 'string' and not \
236                            isinstance(value, six.string_types):
237                        log.warning('attribute %s.%s has type string but is '
238                                    'set to a value that is not a string' % (i.name,
239                                                                             a.name))
240                    elif a.type == 'int' and not \
241                            isinstance(value, numbers.Number):
242                        log.warning('attribute %s.%s has type int but is set '
243                                    'to a value that is not an integer' % (i.name,
244                                                                           a.name))
245
246    try:
247        r = Renderer(options.templates)
248    except jinja2.exceptions.TemplateSyntaxError as e:
249        die('template syntax error: %s' % e)
250
251    if options.load_object_state is not None:
252        render_state = pickle.load(options.load_object_state)
253    elif options.save_object_state is None:
254        render_state = None
255    else:
256        obj_space = ObjectAllocator()
257        obj_space.spec.arch = options.architecture
258        render_state = RenderState(obj_space=obj_space)
259
260        for i in assembly.composition.instances:
261            # Don't generate any code for hardware components.
262            if i.type.hardware:
263                continue
264
265            key = i.address_space
266
267            if key not in render_state.cspaces:
268                cnode = render_state.obj_space.alloc(ObjectType.seL4_CapTableObject,
269                                                     name="%s_cnode" % key, label=key)
270                render_state.cspaces[key] = CSpaceAllocator(cnode)
271                pd = obj_space.alloc(lookup_architecture(options.architecture).vspace().object, name="%s_group_bin_pd" % key,
272                                     label=key)
273                addr_space = AddressSpaceAllocator(
274                    re.sub(r'[^A-Za-z0-9]', '_', "%s_group_bin" % key), pd)
275                render_state.pds[key] = pd
276                render_state.addr_spaces[key] = addr_space
277
278    for (item, outfile, template) in zip(options.item, options.outfile, options.template):
279        key = item.split("/")
280        if key[0] == "component":
281            i = [x for x in assembly.composition.instances if x.name == key[1]][0]
282            obj_key = i.address_space
283        elif key[0] == "connector":
284            c = [c for c in assembly.composition.connections if c.name == key[1]][0]
285            if key[2] == "to":
286                i = c.to_ends[int(key[3])]
287            elif key[2] == "from":
288                i = c.from_ends[int(key[3])]
289            else:
290                die("Invalid connector end")
291            obj_key = i.instance.address_space
292        elif key[0] == "assembly":
293            i = assembly
294            obj_key = None
295        else:
296            die("item: \"%s\" does not have the correct formatting to render." % item)
297
298        try:
299            g = r.render(i, assembly, template, render_state, obj_key,
300                         outfile_name=outfile.name, options=options, my_pd=render_state.pds[obj_key] if obj_key else None)
301            outfile.write(g)
302            outfile.close()
303        except TemplateError as inst:
304            if hasattr(i, 'name'):
305                die(rendering_error(i.name, inst))
306            else:
307                die(rendering_error(i.parent.name, inst))
308
309    read = r.get_files_used()
310    # Write a Makefile dependency rule if requested.
311    if options.makefile_dependencies is not None:
312        options.makefile_dependencies.write('%s: \\\n  %s\n' %
313                                            (options.outfile[0].name, ' \\\n  '.join(sorted(read))))
314
315    if options.save_object_state is not None:
316        # Write the render_state to the supplied outfile
317        pickle.dump(render_state, options.save_object_state)
318
319    sys.exit(0)
320
321
322if __name__ == '__main__':
323    sys.exit(main(sys.argv, sys.stdout, sys.stderr))
324