blob: 4231e7414927145975f5d599d2f72f9463defa9e [file] [log] [blame]
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +00001# Copyright lowRISC contributors.
2# Licensed under the Apache License, Version 2.0, see LICENSE for details.
3# SPDX-License-Identifier: Apache-2.0
4
5import os
6import shutil
7import time
8from pathlib import Path
Philipp Wagner22880082021-09-27 17:20:49 +01009from typing import Any, Dict, Optional, Union
Philipp Wagner1f0923b2021-09-27 17:41:40 +010010import logging
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +000011
12import reggen.gen_rtl
13from mako import exceptions as mako_exceptions # type: ignore
14from mako.lookup import TemplateLookup as MakoTemplateLookup # type: ignore
15from reggen.ip_block import IpBlock
Michael Schaffner5fb5d142022-01-21 19:42:50 -080016from reggen.countermeasure import CounterMeasure
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +000017
Philipp Wagnerb246f272021-09-27 17:53:22 +010018from .lib import IpConfig, IpTemplate, TemplateParameter
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +000019
20_HJSON_LICENSE_HEADER = ("""// Copyright lowRISC contributors.
21// Licensed under the Apache License, Version 2.0, see LICENSE for details.
22// SPDX-License-Identifier: Apache-2.0
23""")
24
Philipp Wagner1f0923b2021-09-27 17:41:40 +010025log = logging.getLogger(__name__)
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +000026
Philipp Wagner22880082021-09-27 17:20:49 +010027
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +000028class TemplateRenderError(Exception):
Philipp Wagner22880082021-09-27 17:20:49 +010029 def __init__(self, message, template_vars: Any = None) -> None:
30 self.message = message
31 self.template_vars = template_vars
32
33 def verbose_str(self) -> str:
34 """ Get a verbose human-readable representation of the error. """
35
36 from pprint import PrettyPrinter
37 if self.template_vars is not None:
38 return (self.message + "\n" +
39 "Template variables:\n" +
40 PrettyPrinter().pformat(self.template_vars))
41 return self.message
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +000042
43
44class IpTemplateRendererBase:
45 """ Render an IpTemplate into an IP block. """
46
47 ip_template: IpTemplate
48 ip_config: IpConfig
49
50 _lookup: Optional[MakoTemplateLookup] = None
51
52 def __init__(self, ip_template: IpTemplate, ip_config: IpConfig) -> None:
53 self.ip_template = ip_template
54 self.ip_config = ip_config
55 self._check_param_values()
56
57 def _check_param_values(self) -> None:
58 """ Check if all parameters in IpConfig are defined in the template. """
59
60 for name in self.ip_config.param_values:
61 if name not in self.ip_template.params:
62 raise KeyError("No parameter named {!r} exists.".format(name))
63
Philipp Wagnere1b13492021-09-27 18:22:48 +010064 def get_template_parameter_values(self) -> Dict[str, Union[str, int, object]]:
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +000065 """ Get a typed mapping of all template parameters and their values.
66 """
67 ret = {}
68
69 for name, template_param in self.ip_template.params.items():
70 if name in self.ip_config.param_values:
71 val = self.ip_config.param_values[name]
72 else:
Philipp Wagner1f0923b2021-09-27 17:41:40 +010073 log.info(f"Using default value for template parameter {name}")
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +000074 val = template_param.default
75
Philipp Wagnerb246f272021-09-27 17:53:22 +010076 assert template_param.param_type in TemplateParameter.VALID_PARAM_TYPES
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +000077 try:
78 if template_param.param_type == 'string':
Philipp Wagnere1b13492021-09-27 18:22:48 +010079 val_typed = str(val) # type: Union[int, str, object]
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +000080 elif template_param.param_type == 'int':
81 if not isinstance(val, int):
82 val_typed = int(val, 0)
83 else:
84 val_typed = val
Philipp Wagnere1b13492021-09-27 18:22:48 +010085 elif template_param.param_type == 'object':
86 val_typed = val
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +000087 except (ValueError, TypeError):
88 raise TemplateRenderError(
89 "For parameter {} cannot convert value {!r} "
90 "to {}.".format(name, val,
91 template_param.param_type)) from None
92
93 ret[name] = val_typed
94
95 return ret
96
97 def _get_mako_template_lookup(self) -> MakoTemplateLookup:
98 """ Get a Mako TemplateLookup object """
99
100 if self._lookup is None:
101 # Define the directory containing the IP template as "base"
102 # directory, allowing templates to include other templates within
103 # this directory using relative paths.
104 # Use strict_undefined to throw a NameError if undefined variables
105 # are used within a template.
106 self._lookup = MakoTemplateLookup(
107 directories=[str(self.ip_template.template_path)],
108 strict_undefined=True)
109 return self._lookup
110
Philipp Wagner5d21c9a2021-10-04 20:57:03 +0100111 def _tplfunc_instance_vlnv(self, template_vlnv_str: str) -> str:
112 template_vlnv = template_vlnv_str.split(':')
113 if len(template_vlnv) != 3 and len(template_vlnv) != 4:
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000114 raise TemplateRenderError(
115 f"{template_vlnv_str} isn't a valid FuseSoC VLNV. "
Philipp Wagner5d21c9a2021-10-04 20:57:03 +0100116 "Required format: 'vendor:library:name[:version]'")
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000117 template_core_name = template_vlnv[2]
Philipp Wagner5d21c9a2021-10-04 20:57:03 +0100118 template_core_version = (template_vlnv[3]
119 if len(template_vlnv) == 4 else None)
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000120
121 # Remove the template name from the start of the core name.
122 # For example, a core name `rv_plic_component` will result in an
123 # instance name 'my_instance_component' (instead of
124 # 'my_instance_rv_plic_component').
125 if template_core_name.startswith(self.ip_template.name):
126 template_core_name = template_core_name[len(self.ip_template.name):]
127
128 instance_core_name = self.ip_config.instance_name + template_core_name
Philipp Wagner5d21c9a2021-10-04 20:57:03 +0100129 instance_vlnv = ['lowrisc', 'opentitan', instance_core_name]
130
131 # Keep the version component if it was present before.
132 if template_core_version is not None:
133 instance_vlnv.append(template_core_version)
134
135 return ':'.join(instance_vlnv)
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000136
137 def _render_mako_template_to_str(self, template_filepath: Path) -> str:
138 """ Render a template and return the rendered text. """
139
140 lookup = self._get_mako_template_lookup()
141 template_filepath_rel = template_filepath.relative_to(
142 self.ip_template.template_path)
143 template = lookup.get_template(str(template_filepath_rel))
144
145 helper_funcs = {}
146 helper_funcs['instance_vlnv'] = self._tplfunc_instance_vlnv
147
148 # TODO: Introduce namespacing for the template parameters and other
149 # parameters, and/or pass the IpConfig instance to the template after
150 # we have converted more IP blocks to ipgen.
151 tpl_args = {
152 "instance_name": self.ip_config.instance_name, # type: ignore
153 **helper_funcs, # type: ignore
154 **self.get_template_parameter_values() # type: ignore
155 }
156 try:
157 return template.render(**tpl_args)
Philipp Wagner22880082021-09-27 17:20:49 +0100158 except Exception:
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000159 raise TemplateRenderError(
160 "Unable to render template: " +
Philipp Wagner22880082021-09-27 17:20:49 +0100161 mako_exceptions.text_error_template().render(),
162 self.get_template_parameter_values()) from None
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000163
164 def _render_mako_template_to_file(self, template_filepath: Path,
165 outdir_path: Path) -> None:
166 """ Render a file template into a file in outdir_path.
167
168 The name of the output file matches the file name of the template file,
169 without the '.tpl' suffix.
170 """
171
172 assert outdir_path.is_dir()
173
174 outfile_name = self._filename_without_tpl_suffix(template_filepath)
175
176 with open(outdir_path / outfile_name, 'w') as f:
177 f.write(self._render_mako_template_to_str(template_filepath))
178
179 def _filename_without_tpl_suffix(self, filepath: Path) -> str:
180 """ Get the name of the file without a '.tpl' suffix. """
181 assert filepath.suffix == '.tpl'
182 return filepath.stem
183
184
185class IpDescriptionOnlyRenderer(IpTemplateRendererBase):
186 """ Generate the IP description only.
187
188 The IP description is the content of what is typically stored
189 data/ip_name.hjson.
190 """
191 def render(self) -> str:
192 template_path = self.ip_template.template_path
193
194 # Look for a data/ip_name.hjson.tpl template file and render it.
195 hjson_tpl_path = template_path / 'data' / (self.ip_template.name +
196 '.hjson.tpl')
197 if hjson_tpl_path.is_file():
198 return self._render_mako_template_to_str(hjson_tpl_path)
199
200 # Otherwise, if a data/ip_name.hjson file exists, use that.
201 hjson_path = template_path / 'data' / (self.ip_template.name +
202 '.hjson')
203 try:
204 with open(hjson_path, 'r') as f:
205 return f.read()
206 except FileNotFoundError:
Philipp Wagner5ebecb92021-10-21 15:23:22 +0100207 raise TemplateRenderError(
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000208 "Neither a IP description template at {}, "
209 "nor an IP description at {} exist!".format(
210 hjson_tpl_path, hjson_path))
211
212
213class IpBlockRenderer(IpTemplateRendererBase):
214 """ Produce a full IP block directory from a template.
215
216 - Copy everything but Mako templates (files ending with '.tpl') into the
217 destination directory.
218 - Process all Mako templates and write the results to the output directory.
219 A template at <PATH>.tpl will write results to <PATH> in the output
220 directory.
221 - Run reggen to generate the register interface.
222 """
223 def render(self, output_dir: Path, overwrite_output_dir: bool) -> None:
224 """ Render the IP template into output_dir. """
225
226 # Ensure that we operate on an absolute path for output_dir.
227 output_dir = output_dir.resolve()
228
229 if not overwrite_output_dir and output_dir.exists():
230 raise TemplateRenderError(
231 "Output directory '{}' exists and should not be overwritten.".
232 format(output_dir))
233
234 # Prepare the IP directory in a staging area to later atomically move it
235 # to the final destination.
236 output_dir_staging = output_dir.parent / f".~{output_dir.stem}.staging"
237 if output_dir_staging.is_dir():
238 raise TemplateRenderError(
239 "Output staging directory '{}' already exists. Remove it and "
240 "try again.".format(output_dir_staging))
241
242 template_path = self.ip_template.template_path
243
244 try:
Philipp Wagner273d4a32021-10-02 15:50:57 +0100245 # Copy everything but the templates and the template description.
246 ignore = shutil.ignore_patterns('*.tpl', '*.tpldesc.hjson')
247 shutil.copytree(template_path, output_dir_staging, ignore=ignore)
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000248
249 # Render templates.
250 for template_filepath in template_path.glob('**/*.tpl'):
251 template_filepath_rel = template_filepath.relative_to(
252 template_path)
253
254 # Put the output file into the same relative directory as the
255 # template. The output file will also have the same name as the
256 # template, just without the '.tpl' suffix.
257 outdir_path = output_dir_staging / template_filepath_rel.parent
258
259 self._render_mako_template_to_file(template_filepath,
260 outdir_path)
261
262 # Generate register interface through reggen.
263 hjson_path = (output_dir_staging / 'data' /
264 (self.ip_template.name + '.hjson'))
Philipp Wagnereb0ebe32021-10-21 15:24:56 +0100265 if not hjson_path.exists():
266 raise TemplateRenderError(
267 "Invalid template: The IP description file "
268 f"{str(hjson_path)!r} does not exist.")
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000269 rtl_path = output_dir_staging / 'rtl'
Philipp Wagner067dc202021-10-21 15:25:40 +0100270 rtl_path.mkdir(exist_ok=True)
Michael Schaffner5fb5d142022-01-21 19:42:50 -0800271
272 obj = IpBlock.from_path(str(hjson_path), [])
273
274 # If this block has countermeasures, we grep for RTL annotations in
275 # all .sv implementation files and check whether they match up
276 # with what is defined inside the Hjson.
277 sv_files = rtl_path.glob('*.sv')
278 rtl_names = CounterMeasure.search_rtl_files(sv_files)
279 obj.check_cm_annotations(rtl_names, str(hjson_path))
280
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000281 # TODO: Pass on template parameters to reggen? Or enable the user
282 # to set a different set of parameters in the renderer?
Michael Schaffner5fb5d142022-01-21 19:42:50 -0800283 reggen.gen_rtl.gen_rtl(obj, str(rtl_path))
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000284
285 # Write IP configuration (to reproduce the generation process).
286 # TODO: Should the ipconfig file be written to the instance name,
287 # or the template name?
288 self.ip_config.to_file(
289 output_dir_staging /
290 'data/{}.ipconfig.hjson'.format(self.ip_config.instance_name),
291 header=_HJSON_LICENSE_HEADER)
292
293 # Safely overwrite the existing directory if necessary:
294 #
295 # - First move the existing directory out of the way.
296 # - Then move the staging directory with the new content in place.
297 # - Finally remove the old directory.
298 #
299 # If anything goes wrong in the meantime we are left with either
300 # the old or the new directory, and potentially some backups of
301 # outdated files.
302 do_overwrite = overwrite_output_dir and output_dir.exists()
303 output_dir_existing_bak = output_dir.with_suffix(
304 '.bak~' + str(int(time.time())))
305 if do_overwrite:
306 os.rename(output_dir, output_dir_existing_bak)
307
308 # Move the staging directory to the final destination.
309 os.rename(output_dir_staging, output_dir)
310
311 # Remove the old/"overwritten" data.
312 if do_overwrite:
313 try:
314 shutil.rmtree(output_dir_existing_bak)
315 except Exception as e:
316 msg = (
Philipp Wagner5813a942021-09-29 19:08:34 +0100317 'Unable to delete the backup directory '
318 f'{output_dir_existing_bak} of the overwritten data. '
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000319 'Please remove it manually.')
320 raise TemplateRenderError(msg).with_traceback(
321 e.__traceback__)
322
323 finally:
324 # Ensure that the staging directory is removed at the end. Ignore
325 # errors as the directory should not exist at this point actually.
326 shutil.rmtree(output_dir_staging, ignore_errors=True)