blob: 9387828a8c579c340cdc25221ded151ee7be8752 [file] [log] [blame]
# 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)