blob: 0682dd49e6d408886926928e29080dde8646d14d [file] [log] [blame]
Imre Kis2d137c52019-07-09 18:30:58 +02001#!/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)