Imre Kis | 2d137c5 | 2019-07-09 18:30:58 +0200 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # Copyright (c) 2019, Arm Limited. All rights reserved. |
| 3 | # |
| 4 | # SPDX-License-Identifier: BSD-3-Clause |
| 5 | |
| 6 | """ |
| 7 | This module contains a set of classes and a runner that can generate code for the romlib module |
| 8 | based on the templates in the 'templates' directory. |
| 9 | """ |
| 10 | |
| 11 | import argparse |
| 12 | import os |
| 13 | import re |
| 14 | import subprocess |
| 15 | import string |
| 16 | import sys |
| 17 | |
| 18 | class 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 | |
| 87 | class 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 | |
| 122 | class 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 | |
| 156 | class 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 | |
| 185 | class 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 | |
| 233 | class 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 | |
| 259 | if __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) |