| # Copyright lowRISC contributors. |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| import os |
| import shutil |
| import time |
| from pathlib import Path |
| from typing import Any, Dict, Optional, Union |
| import logging |
| |
| import reggen.gen_rtl |
| from mako import exceptions as mako_exceptions # type: ignore |
| from mako.lookup import TemplateLookup as MakoTemplateLookup # type: ignore |
| from reggen.ip_block import IpBlock |
| from reggen.countermeasure import CounterMeasure |
| |
| from .lib import IpConfig, IpTemplate, TemplateParameter |
| |
| _HJSON_LICENSE_HEADER = ("""// Copyright lowRISC contributors. |
| // Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| // SPDX-License-Identifier: Apache-2.0 |
| """) |
| |
| log = logging.getLogger(__name__) |
| |
| |
| class TemplateRenderError(Exception): |
| def __init__(self, message, template_vars: Any = None) -> None: |
| self.message = message |
| self.template_vars = template_vars |
| |
| def verbose_str(self) -> str: |
| """ Get a verbose human-readable representation of the error. """ |
| |
| from pprint import PrettyPrinter |
| if self.template_vars is not None: |
| return (self.message + "\n" + |
| "Template variables:\n" + |
| PrettyPrinter().pformat(self.template_vars)) |
| return self.message |
| |
| |
| class IpTemplateRendererBase: |
| """ Render an IpTemplate into an IP block. """ |
| |
| ip_template: IpTemplate |
| ip_config: IpConfig |
| |
| _lookup: Optional[MakoTemplateLookup] = None |
| |
| def __init__(self, ip_template: IpTemplate, ip_config: IpConfig) -> None: |
| self.ip_template = ip_template |
| self.ip_config = ip_config |
| self._check_param_values() |
| |
| def _check_param_values(self) -> None: |
| """ Check if all parameters in IpConfig are defined in the template. """ |
| |
| for name in self.ip_config.param_values: |
| if name not in self.ip_template.params: |
| raise KeyError("No parameter named {!r} exists.".format(name)) |
| |
| def get_template_parameter_values(self) -> Dict[str, Union[str, int, object]]: |
| """ Get a typed mapping of all template parameters and their values. |
| """ |
| ret = {} |
| |
| for name, template_param in self.ip_template.params.items(): |
| if name in self.ip_config.param_values: |
| val = self.ip_config.param_values[name] |
| else: |
| log.info(f"Using default value for template parameter {name}") |
| val = template_param.default |
| |
| assert template_param.param_type in TemplateParameter.VALID_PARAM_TYPES |
| try: |
| if template_param.param_type == 'string': |
| val_typed = str(val) # type: Union[int, str, object] |
| elif template_param.param_type == 'int': |
| if not isinstance(val, int): |
| val_typed = int(val, 0) |
| else: |
| val_typed = val |
| elif template_param.param_type == 'object': |
| val_typed = val |
| except (ValueError, TypeError): |
| raise TemplateRenderError( |
| "For parameter {} cannot convert value {!r} " |
| "to {}.".format(name, val, |
| template_param.param_type)) from None |
| |
| ret[name] = val_typed |
| |
| return ret |
| |
| def _get_mako_template_lookup(self) -> MakoTemplateLookup: |
| """ Get a Mako TemplateLookup object """ |
| |
| if self._lookup is None: |
| # Define the directory containing the IP template as "base" |
| # directory, allowing templates to include other templates within |
| # this directory using relative paths. |
| # Use strict_undefined to throw a NameError if undefined variables |
| # are used within a template. |
| self._lookup = MakoTemplateLookup( |
| directories=[str(self.ip_template.template_path)], |
| strict_undefined=True) |
| return self._lookup |
| |
| def _tplfunc_instance_vlnv(self, template_vlnv_str: str) -> str: |
| template_vlnv = template_vlnv_str.split(':') |
| if len(template_vlnv) != 3 and len(template_vlnv) != 4: |
| raise TemplateRenderError( |
| f"{template_vlnv_str} isn't a valid FuseSoC VLNV. " |
| "Required format: 'vendor:library:name[:version]'") |
| template_core_name = template_vlnv[2] |
| template_core_version = (template_vlnv[3] |
| if len(template_vlnv) == 4 else None) |
| |
| # Remove the template name from the start of the core name. |
| # For example, a core name `rv_plic_component` will result in an |
| # instance name 'my_instance_component' (instead of |
| # 'my_instance_rv_plic_component'). |
| if "module_instance_name" in self.ip_config.param_values: |
| assert template_core_name.startswith( |
| self.ip_config.param_values["module_instance_name"]) |
| idx = len(self.ip_config.param_values["module_instance_name"]) |
| template_core_name = template_core_name[idx:] |
| elif template_core_name.startswith(self.ip_template.name): |
| template_core_name = template_core_name[len(self.ip_template.name):] |
| |
| instance_core_name = self.ip_config.instance_name + template_core_name |
| instance_vlnv = ['lowrisc', 'opentitan', instance_core_name] |
| |
| # Keep the version component if it was present before. |
| if template_core_version is not None: |
| instance_vlnv.append(template_core_version) |
| |
| return ':'.join(instance_vlnv) |
| |
| def _render_mako_template_to_str(self, template_filepath: Path) -> str: |
| """ Render a template and return the rendered text. """ |
| |
| lookup = self._get_mako_template_lookup() |
| template_filepath_rel = template_filepath.relative_to( |
| self.ip_template.template_path) |
| template = lookup.get_template(str(template_filepath_rel)) |
| |
| helper_funcs = {} |
| helper_funcs['instance_vlnv'] = self._tplfunc_instance_vlnv |
| |
| # TODO: Introduce namespacing for the template parameters and other |
| # parameters, and/or pass the IpConfig instance to the template after |
| # we have converted more IP blocks to ipgen. |
| tpl_args = { |
| "instance_name": self.ip_config.instance_name, # type: ignore |
| **helper_funcs, # type: ignore |
| **self.get_template_parameter_values() # type: ignore |
| } |
| try: |
| return template.render(**tpl_args) |
| except Exception: |
| raise TemplateRenderError( |
| "Unable to render template: " + |
| mako_exceptions.text_error_template().render(), |
| self.get_template_parameter_values()) from None |
| |
| def _render_mako_template_to_file(self, template_filepath: Path, |
| outdir_path: Path) -> None: |
| """ Render a file template into a file in outdir_path. |
| |
| The name of the output file matches the file name of the template file, |
| without the '.tpl' suffix. |
| """ |
| |
| assert outdir_path.is_dir() |
| |
| outfile_name = self._filename_without_tpl_suffix(template_filepath) |
| |
| with open(outdir_path / outfile_name, 'w') as f: |
| f.write(self._render_mako_template_to_str(template_filepath)) |
| |
| def _filename_without_tpl_suffix(self, filepath: Path) -> str: |
| """ Get the name of the file without a '.tpl' suffix. """ |
| assert filepath.suffix == '.tpl' |
| filename = filepath.stem |
| if "module_instance_name" in self.ip_config.param_values: |
| filename = self.ip_config.param_values[ |
| "module_instance_name"] + filename[len(self.ip_template.name):] |
| return filename |
| |
| |
| class IpDescriptionOnlyRenderer(IpTemplateRendererBase): |
| """ Generate the IP description only. |
| |
| The IP description is the content of what is typically stored |
| data/ip_name.hjson. |
| """ |
| def render(self) -> str: |
| template_path = self.ip_template.template_path |
| |
| # Look for a data/ip_name.hjson.tpl template file and render it. |
| hjson_tpl_path = template_path / 'data' / (self.ip_template.name + |
| '.hjson.tpl') |
| if hjson_tpl_path.is_file(): |
| return self._render_mako_template_to_str(hjson_tpl_path) |
| |
| # Otherwise, if a data/ip_name.hjson file exists, use that. |
| hjson_path = template_path / 'data' / (self.ip_template.name + |
| '.hjson') |
| try: |
| with open(hjson_path, 'r') as f: |
| return f.read() |
| except FileNotFoundError: |
| raise TemplateRenderError( |
| "Neither a IP description template at {}, " |
| "nor an IP description at {} exist!".format( |
| hjson_tpl_path, hjson_path)) |
| |
| |
| class IpBlockRenderer(IpTemplateRendererBase): |
| """ Produce a full IP block directory from a template. |
| |
| - Copy everything but Mako templates (files ending with '.tpl') into the |
| destination directory. |
| - Process all Mako templates and write the results to the output directory. |
| A template at <PATH>.tpl will write results to <PATH> in the output |
| directory. |
| - Run reggen to generate the register interface. |
| """ |
| def render(self, output_dir: Path, overwrite_output_dir: bool) -> None: |
| """ Render the IP template into output_dir. """ |
| |
| # Ensure that we operate on an absolute path for output_dir. |
| output_dir = output_dir.resolve() |
| |
| if not overwrite_output_dir and output_dir.exists(): |
| raise TemplateRenderError( |
| "Output directory '{}' exists and should not be overwritten.". |
| format(output_dir)) |
| |
| # Prepare the IP directory in a staging area to later atomically move it |
| # to the final destination. |
| output_dir_staging = output_dir.parent / f".~{output_dir.stem}.staging" |
| if output_dir_staging.is_dir(): |
| raise TemplateRenderError( |
| "Output staging directory '{}' already exists. Remove it and " |
| "try again.".format(output_dir_staging)) |
| |
| template_path = self.ip_template.template_path |
| |
| try: |
| # Copy everything but the templates and the template description. |
| ignore = shutil.ignore_patterns('*.tpl', '*.tpldesc.hjson') |
| shutil.copytree(template_path, output_dir_staging, ignore=ignore) |
| |
| # Render templates. |
| for template_filepath in template_path.glob('**/*.tpl'): |
| template_filepath_rel = template_filepath.relative_to( |
| template_path) |
| |
| # Put the output file into the same relative directory as the |
| # template. The output file will also have the same name as the |
| # template, just without the '.tpl' suffix. |
| outdir_path = output_dir_staging / template_filepath_rel.parent |
| |
| self._render_mako_template_to_file(template_filepath, |
| outdir_path) |
| |
| # Generate register interface through reggen. |
| hjson_path = (output_dir_staging / 'data' / |
| (self.ip_template.name + '.hjson')) |
| if "module_instance_name" in self.ip_config.param_values: |
| hjson_path = ( |
| output_dir_staging / 'data' / |
| (self.ip_config.param_values["module_instance_name"] + |
| '.hjson')) |
| if not hjson_path.exists(): |
| raise TemplateRenderError( |
| "Invalid template: The IP description file " |
| f"{str(hjson_path)!r} does not exist.") |
| rtl_path = output_dir_staging / 'rtl' |
| rtl_path.mkdir(exist_ok=True) |
| |
| obj = IpBlock.from_path(str(hjson_path), []) |
| |
| # If this block has countermeasures, we grep for RTL annotations in |
| # all .sv implementation files and check whether they match up |
| # with what is defined inside the Hjson. |
| sv_files = rtl_path.glob('*.sv') |
| rtl_names = CounterMeasure.search_rtl_files(sv_files) |
| obj.check_cm_annotations(rtl_names, str(hjson_path)) |
| |
| # TODO: Pass on template parameters to reggen? Or enable the user |
| # to set a different set of parameters in the renderer? |
| reggen.gen_rtl.gen_rtl(obj, str(rtl_path)) |
| |
| # Write IP configuration (to reproduce the generation process). |
| # TODO: Should the ipconfig file be written to the instance name, |
| # or the template name? |
| self.ip_config.to_file( |
| output_dir_staging / |
| 'data/{}.ipconfig.hjson'.format(self.ip_config.instance_name), |
| header=_HJSON_LICENSE_HEADER) |
| |
| # Safely overwrite the existing directory if necessary: |
| # |
| # - First move the existing directory out of the way. |
| # - Then move the staging directory with the new content in place. |
| # - Finally remove the old directory. |
| # |
| # If anything goes wrong in the meantime we are left with either |
| # the old or the new directory, and potentially some backups of |
| # outdated files. |
| do_overwrite = overwrite_output_dir and output_dir.exists() |
| output_dir_existing_bak = output_dir.with_suffix( |
| '.bak~' + str(int(time.time()))) |
| if do_overwrite: |
| os.rename(output_dir, output_dir_existing_bak) |
| |
| # Move the staging directory to the final destination. |
| os.rename(output_dir_staging, output_dir) |
| |
| # Remove the old/"overwritten" data. |
| if do_overwrite: |
| try: |
| shutil.rmtree(output_dir_existing_bak) |
| except Exception as e: |
| msg = ( |
| 'Unable to delete the backup directory ' |
| f'{output_dir_existing_bak} of the overwritten data. ' |
| 'Please remove it manually.') |
| raise TemplateRenderError(msg).with_traceback( |
| e.__traceback__) |
| |
| finally: |
| # Ensure that the staging directory is removed at the end. Ignore |
| # errors as the directory should not exist at this point actually. |
| shutil.rmtree(output_dir_staging, ignore_errors=True) |