| # 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) |