• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# Copyright (c) 2019, Arm Limited. All rights reserved.
3#
4# SPDX-License-Identifier: BSD-3-Clause
5
6"""
7This module contains a set of classes and a runner that can generate code for the romlib module
8based on the templates in the 'templates' directory.
9"""
10
11import argparse
12import os
13import re
14import subprocess
15import string
16import sys
17
18class IndexFileParser:
19    """
20    Parses the contents of the index file into the items and dependencies variables. It
21    also resolves included files in the index files recursively with circular inclusion detection.
22    """
23
24    def __init__(self):
25        self.items = []
26        self.dependencies = {}
27        self.include_chain = []
28
29    def add_dependency(self, parent, dependency):
30        """ Adds a dependency into the dependencies variable. """
31        if parent in self.dependencies:
32            self.dependencies[parent].append(dependency)
33        else:
34            self.dependencies[parent] = [dependency]
35
36    def get_dependencies(self, parent):
37        """ Gets all the recursive dependencies of a parent file. """
38        parent = os.path.normpath(parent)
39        if parent in self.dependencies:
40            direct_deps = self.dependencies[parent]
41            deps = direct_deps
42            for direct_dep in direct_deps:
43                deps += self.get_dependencies(direct_dep)
44            return deps
45
46        return []
47
48    def parse(self, file_name):
49        """ Opens and parses index file. """
50        file_name = os.path.normpath(file_name)
51
52        if file_name not in self.include_chain:
53            self.include_chain.append(file_name)
54            self.dependencies[file_name] = []
55        else:
56            raise Exception("Circular dependency detected: " + file_name)
57
58        with open(file_name, "r") as index_file:
59            for line in index_file.readlines():
60                line_elements = line.split()
61
62                if line.startswith("#") or not line_elements:
63                    # Comment or empty line
64                    continue
65
66                if line_elements[0] == "reserved":
67                    # Reserved slot in the jump table
68                    self.items.append({"type": "reserved"})
69                elif line_elements[0] == "include" and len(line_elements) > 1:
70                    # Include other index file
71                    included_file = os.path.normpath(line_elements[1])
72                    self.add_dependency(file_name, included_file)
73                    self.parse(included_file)
74                elif len(line_elements) > 1:
75                    # Library function
76                    library_name = line_elements[0]
77                    function_name = line_elements[1]
78                    patch = bool(len(line_elements) > 2 and line_elements[2] == "patch")
79
80                    self.items.append({"type": "function", "library_name": library_name,
81                                       "function_name": function_name, "patch": patch})
82                else:
83                    raise Exception("Invalid line: '" + line + "'")
84
85        self.include_chain.pop()
86
87class RomlibApplication:
88    """ Base class of romlib applications. """
89    TEMPLATE_DIR = os.path.dirname(os.path.realpath(__file__)) + "/templates/"
90
91    def __init__(self, prog):
92        self.args = argparse.ArgumentParser(prog=prog, description=self.__doc__)
93        self.config = None
94
95    def parse_arguments(self, argv):
96        """ Parses the arguments that should come from the command line arguments. """
97        self.config = self.args.parse_args(argv)
98
99    def build_template(self, name, mapping=None, remove_comment=False):
100        """
101        Loads a template and builds it with the defined mapping. Template paths are always relative
102        to this script.
103        """
104
105        with open(self.TEMPLATE_DIR + name, "r") as template_file:
106            if remove_comment:
107                # Removing copyright comment to make the generated code more readable when the
108                # template is inserted multiple times into the output.
109                template_lines = template_file.readlines()
110                end_of_comment_line = 0
111                for index, line in enumerate(template_lines):
112                    if line.find("*/") != -1:
113                        end_of_comment_line = index
114                        break
115                template_data = "".join(template_lines[end_of_comment_line + 1:])
116            else:
117                template_data = template_file.read()
118
119            template = string.Template(template_data)
120            return template.substitute(mapping)
121
122class IndexPreprocessor(RomlibApplication):
123    """ Removes empty and comment lines from the index file and resolves includes. """
124
125    def __init__(self, prog):
126        RomlibApplication.__init__(self, prog)
127
128        self.args.add_argument("-o", "--output", help="Output file", metavar="output",
129                               default="jmpvar.s")
130        self.args.add_argument("--deps", help="Dependency file")
131        self.args.add_argument("file", help="Input file")
132
133    def main(self):
134        """
135        After parsing the input index file it generates a clean output with all includes resolved.
136        Using --deps option it also outputs the dependencies in makefile format like gcc's with -M.
137        """
138
139        index_file_parser = IndexFileParser()
140        index_file_parser.parse(self.config.file)
141
142        with open(self.config.output, "w") as output_file:
143            for item in index_file_parser.items:
144                if item["type"] == "function":
145                    patch = "\tpatch" if item["patch"] else ""
146                    output_file.write(
147                        item["library_name"] + "\t" + item["function_name"] + patch + "\n")
148                else:
149                    output_file.write("reserved\n")
150
151        if self.config.deps:
152            with open(self.config.deps, "w") as deps_file:
153                deps = [self.config.file] + index_file_parser.get_dependencies(self.config.file)
154                deps_file.write(self.config.output + ": " + " \\\n".join(deps) + "\n")
155
156class TableGenerator(RomlibApplication):
157    """ Generates the jump table by parsing the index file. """
158
159    def __init__(self, prog):
160        RomlibApplication.__init__(self, prog)
161
162        self.args.add_argument("-o", "--output", help="Output file", metavar="output",
163                               default="jmpvar.s")
164        self.args.add_argument("--bti", help="Branch Target Identification", type=int)
165        self.args.add_argument("file", help="Input file")
166
167    def main(self):
168        """
169        Inserts the jmptbl definition and the jump entries into the output file. Also can insert
170        BTI related code before entries if --bti option set. It can output a dependency file of the
171        included index files. This can be directly included in makefiles.
172        """
173
174        index_file_parser = IndexFileParser()
175        index_file_parser.parse(self.config.file)
176
177        with open(self.config.output, "w") as output_file:
178            output_file.write(self.build_template("jmptbl_header.S"))
179            bti = "_bti" if self.config.bti == 1 else ""
180
181            for item in index_file_parser.items:
182                template_name = "jmptbl_entry_" + item["type"] + bti + ".S"
183                output_file.write(self.build_template(template_name, item, True))
184
185class WrapperGenerator(RomlibApplication):
186    """
187    Generates a wrapper function for each entry in the index file except for the ones that contain
188    the keyword patch. The generated wrapper file is called <lib>_<fn_name>.s.
189    """
190
191    def __init__(self, prog):
192        RomlibApplication.__init__(self, prog)
193
194        self.args.add_argument("-b", help="Build directory", default=".", metavar="build")
195        self.args.add_argument("--bti", help="Branch Target Identification", type=int)
196        self.args.add_argument("--list", help="Only list assembly files", action="store_true")
197        self.args.add_argument("file", help="Input file")
198
199    def main(self):
200        """
201        Iterates through the items in the parsed index file and builds the template for each entry.
202        """
203
204        index_file_parser = IndexFileParser()
205        index_file_parser.parse(self.config.file)
206
207        bti = "_bti" if self.config.bti == 1 else ""
208        function_offset = 0
209        files = []
210
211        for item_index in range(0, len(index_file_parser.items)):
212            item = index_file_parser.items[item_index]
213
214            if item["type"] == "reserved" or item["patch"]:
215                continue
216
217            asm = self.config.b + "/" + item["function_name"] + ".s"
218            if self.config.list:
219                # Only listing files
220                files.append(asm)
221            else:
222                with open(asm, "w") as asm_file:
223                    # The jump instruction is 4 bytes but BTI requires and extra instruction so
224                    # this makes it 8 bytes per entry.
225                    function_offset = item_index * (8 if self.config.bti else 4)
226
227                    item["function_offset"] = function_offset
228                    asm_file.write(self.build_template("wrapper" + bti + ".S", item))
229
230        if self.config.list:
231            print(" ".join(files))
232
233class VariableGenerator(RomlibApplication):
234    """ Generates the jump table global variable with the absolute address in ROM. """
235
236    def __init__(self, prog):
237        RomlibApplication.__init__(self, prog)
238
239        self.args.add_argument("-o", "--output", help="Output file", metavar="output",
240                               default="jmpvar.s")
241        self.args.add_argument("file", help="Input file")
242
243    def main(self):
244        """
245        Runs nm -a command on the input file and inserts the address of the .text section into the
246        template as the ROM address of the jmp_table.
247        """
248        symbols = subprocess.check_output(["nm", "-a", self.config.file])
249
250        matching_symbol = re.search("([0-9A-Fa-f]+) . \\.text", str(symbols))
251        if not matching_symbol:
252            raise Exception("No '.text' section was found in %s" % self.config.file)
253
254        mapping = {"jmptbl_address": matching_symbol.group(1)}
255
256        with open(self.config.output, "w") as output_file:
257            output_file.write(self.build_template("jmptbl_glob_var.S", mapping))
258
259if __name__ == "__main__":
260    APPS = {"genvar": VariableGenerator, "pre": IndexPreprocessor,
261            "gentbl": TableGenerator, "genwrappers": WrapperGenerator}
262
263    if len(sys.argv) < 2 or sys.argv[1] not in APPS:
264        print("usage: romlib_generator.py [%s] [args]" % "|".join(APPS.keys()), file=sys.stderr)
265        sys.exit(1)
266
267    APP = APPS[sys.argv[1]]("romlib_generator.py " + sys.argv[1])
268    APP.parse_arguments(sys.argv[2:])
269    try:
270        APP.main()
271        sys.exit(0)
272    except FileNotFoundError as file_not_found_error:
273        print(file_not_found_error, file=sys.stderr)
274    except subprocess.CalledProcessError as called_process_error:
275        print(called_process_error.output, file=sys.stderr)
276
277    sys.exit(1)
278