Philipp Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 1 | # Copyright lowRISC contributors. |
| 2 | # Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| 3 | # SPDX-License-Identifier: Apache-2.0 |
| 4 | |
| 5 | import os |
| 6 | import shutil |
| 7 | import time |
| 8 | from pathlib import Path |
Philipp Wagner | 2288008 | 2021-09-27 17:20:49 +0100 | [diff] [blame] | 9 | from typing import Any, Dict, Optional, Union |
Philipp Wagner | 1f0923b | 2021-09-27 17:41:40 +0100 | [diff] [blame] | 10 | import logging |
Philipp Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 11 | |
| 12 | import reggen.gen_rtl |
| 13 | from mako import exceptions as mako_exceptions # type: ignore |
| 14 | from mako.lookup import TemplateLookup as MakoTemplateLookup # type: ignore |
| 15 | from reggen.ip_block import IpBlock |
Michael Schaffner | 5fb5d14 | 2022-01-21 19:42:50 -0800 | [diff] [blame^] | 16 | from reggen.countermeasure import CounterMeasure |
Philipp Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 17 | |
Philipp Wagner | b246f27 | 2021-09-27 17:53:22 +0100 | [diff] [blame] | 18 | from .lib import IpConfig, IpTemplate, TemplateParameter |
Philipp Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 19 | |
| 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 Wagner | 1f0923b | 2021-09-27 17:41:40 +0100 | [diff] [blame] | 25 | log = logging.getLogger(__name__) |
Philipp Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 26 | |
Philipp Wagner | 2288008 | 2021-09-27 17:20:49 +0100 | [diff] [blame] | 27 | |
Philipp Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 28 | class TemplateRenderError(Exception): |
Philipp Wagner | 2288008 | 2021-09-27 17:20:49 +0100 | [diff] [blame] | 29 | 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 Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 42 | |
| 43 | |
| 44 | class 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 Wagner | e1b1349 | 2021-09-27 18:22:48 +0100 | [diff] [blame] | 64 | def get_template_parameter_values(self) -> Dict[str, Union[str, int, object]]: |
Philipp Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 65 | """ 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 Wagner | 1f0923b | 2021-09-27 17:41:40 +0100 | [diff] [blame] | 73 | log.info(f"Using default value for template parameter {name}") |
Philipp Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 74 | val = template_param.default |
| 75 | |
Philipp Wagner | b246f27 | 2021-09-27 17:53:22 +0100 | [diff] [blame] | 76 | assert template_param.param_type in TemplateParameter.VALID_PARAM_TYPES |
Philipp Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 77 | try: |
| 78 | if template_param.param_type == 'string': |
Philipp Wagner | e1b1349 | 2021-09-27 18:22:48 +0100 | [diff] [blame] | 79 | val_typed = str(val) # type: Union[int, str, object] |
Philipp Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 80 | 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 Wagner | e1b1349 | 2021-09-27 18:22:48 +0100 | [diff] [blame] | 85 | elif template_param.param_type == 'object': |
| 86 | val_typed = val |
Philipp Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 87 | 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 Wagner | 5d21c9a | 2021-10-04 20:57:03 +0100 | [diff] [blame] | 111 | 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 Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 114 | raise TemplateRenderError( |
| 115 | f"{template_vlnv_str} isn't a valid FuseSoC VLNV. " |
Philipp Wagner | 5d21c9a | 2021-10-04 20:57:03 +0100 | [diff] [blame] | 116 | "Required format: 'vendor:library:name[:version]'") |
Philipp Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 117 | template_core_name = template_vlnv[2] |
Philipp Wagner | 5d21c9a | 2021-10-04 20:57:03 +0100 | [diff] [blame] | 118 | template_core_version = (template_vlnv[3] |
| 119 | if len(template_vlnv) == 4 else None) |
Philipp Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 120 | |
| 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 Wagner | 5d21c9a | 2021-10-04 20:57:03 +0100 | [diff] [blame] | 129 | 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 Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 136 | |
| 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 Wagner | 2288008 | 2021-09-27 17:20:49 +0100 | [diff] [blame] | 158 | except Exception: |
Philipp Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 159 | raise TemplateRenderError( |
| 160 | "Unable to render template: " + |
Philipp Wagner | 2288008 | 2021-09-27 17:20:49 +0100 | [diff] [blame] | 161 | mako_exceptions.text_error_template().render(), |
| 162 | self.get_template_parameter_values()) from None |
Philipp Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 163 | |
| 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 | |
| 185 | class 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 Wagner | 5ebecb9 | 2021-10-21 15:23:22 +0100 | [diff] [blame] | 207 | raise TemplateRenderError( |
Philipp Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 208 | "Neither a IP description template at {}, " |
| 209 | "nor an IP description at {} exist!".format( |
| 210 | hjson_tpl_path, hjson_path)) |
| 211 | |
| 212 | |
| 213 | class 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 Wagner | 273d4a3 | 2021-10-02 15:50:57 +0100 | [diff] [blame] | 245 | # 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 Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 248 | |
| 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 Wagner | eb0ebe3 | 2021-10-21 15:24:56 +0100 | [diff] [blame] | 265 | 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 Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 269 | rtl_path = output_dir_staging / 'rtl' |
Philipp Wagner | 067dc20 | 2021-10-21 15:25:40 +0100 | [diff] [blame] | 270 | rtl_path.mkdir(exist_ok=True) |
Michael Schaffner | 5fb5d14 | 2022-01-21 19:42:50 -0800 | [diff] [blame^] | 271 | |
| 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 Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 281 | # TODO: Pass on template parameters to reggen? Or enable the user |
| 282 | # to set a different set of parameters in the renderer? |
Michael Schaffner | 5fb5d14 | 2022-01-21 19:42:50 -0800 | [diff] [blame^] | 283 | reggen.gen_rtl.gen_rtl(obj, str(rtl_path)) |
Philipp Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 284 | |
| 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 Wagner | 5813a94 | 2021-09-29 19:08:34 +0100 | [diff] [blame] | 317 | 'Unable to delete the backup directory ' |
| 318 | f'{output_dir_existing_bak} of the overwritten data. ' |
Philipp Wagner | dcc5f9f | 2021-03-10 22:21:39 +0000 | [diff] [blame] | 319 | '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) |