| # Copyright lowRISC contributors. | |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| """ | |
| Generate HTML documentation from validated dashboard Hjson tree | |
| """ | |
| import html | |
| import logging as log | |
| import os.path | |
| import re | |
| import sys | |
| from pathlib import Path | |
| import dashboard.dashboard_validate as dashboard_validate | |
| import hjson | |
| import mistletoe as mk | |
| REPO_TOP = Path(__file__).parent.parent.parent.resolve().absolute() | |
| def genout(outfile, msg): | |
| outfile.write(msg) | |
| STAGE_STRINGS = { | |
| # Life Stages | |
| 'L0': 'Specification', | |
| 'L1': 'Development', | |
| 'L2': 'Signed Off', | |
| # Design Stages | |
| 'D0': 'Initial Work', | |
| 'D1': 'Functional', | |
| 'D2': 'Feature Complete', | |
| 'D3': 'Design Complete', | |
| # Verification Stages | |
| 'V0': 'Initial Work', | |
| 'V1': 'Under Test', | |
| 'V2': 'Testing Complete', | |
| 'V2S': 'Testing Complete, With Security Countermeasures Verified', | |
| 'V3': 'Verification Complete', | |
| # DIF Stages (S for Software) | |
| 'S0': 'Initial Work', | |
| 'S1': 'Functional', | |
| 'S2': 'Complete', | |
| 'S3': 'Stable', | |
| } | |
| def convert_stage(stagestr): | |
| return STAGE_STRINGS.get(stagestr, "UNKNOWN") | |
| def get_doc_url(base, url): | |
| """ Produce a URL to a document. | |
| Relative `url`s are relative to `base`, absolute `url`s are relative to the | |
| repository root. | |
| """ | |
| assert isinstance(url, str) and len(url) > 0 | |
| if url[0] == '/': | |
| return url | |
| else: | |
| return '/' + base + '/' + url | |
| # Link module name with its design spec doc. | |
| def get_linked_design_spec(obj): | |
| result = "" | |
| if 'design_spec' in obj: | |
| result = "<span title='Design Spec'><a href='{}'>".format( | |
| get_doc_url(obj['_ip_desc_hjson_dir'], obj['design_spec'])) | |
| result += "<code>{}</code></a></span>".format(html.escape(obj['name'])) | |
| else: | |
| result = html.escape(obj['name']) | |
| return result | |
| # Provide the link to the DV document. | |
| def get_linked_dv_doc(obj): | |
| if 'dv_doc' in obj: | |
| return "<span title='DV Document'><a href=\"{}\">DV</a></span>".format( | |
| get_doc_url(obj['_ip_desc_hjson_dir'], obj['dv_doc'])) | |
| else: | |
| return "" | |
| # Link the version to the commit id (if available). | |
| def get_linked_version(rev): | |
| version = html.escape(rev['version']) | |
| tree = rev['commit_id'] if 'commit_id' in rev else 'master' | |
| url = "https://github.com/lowrisc/opentitan/tree/{}".format(tree) | |
| return "<span title='{}'><a href=\"{}\">{}</a></span>".format( | |
| tree, url, version) | |
| # Link D/V stages with the checklist table. | |
| def get_linked_checklist(obj, rev, stage, is_latest_rev=True): | |
| if not stage or stage not in rev: | |
| return "" | |
| url = "" | |
| in_page_ref = "" | |
| if rev[stage] not in ["D0", "V0"]: | |
| # if in D0 or V0 stage, there is no in-page reference. | |
| in_page_ref = "#{}".format(html.escape(rev[stage]).lower()) | |
| # If the checklist is available, the commit id is available, and it is not | |
| # the latest revision, link to the committed version of the checklist. | |
| # Else, if checklist is available, then link to the current version of the | |
| # checklist html. | |
| # Else, link to the template. | |
| if 'hw_checklist' in obj and 'commit_id' in rev and not is_latest_rev: | |
| url = "https://github.com/lowrisc/opentitan/tree/{}/{}.md{}".format( | |
| rev['commit_id'], obj['hw_checklist'], in_page_ref) | |
| elif 'hw_checklist' in obj: | |
| url = get_doc_url(obj['_ip_desc_hjson_dir'], | |
| obj['hw_checklist'] + in_page_ref) | |
| else: | |
| # There is no checklist available, so point to the template. | |
| # doc/project/hw_checklist.md.tpl is a symlink to ip_checklist.md.tpl, | |
| # and github doesn't auto-render symlinks, so we have to use the url | |
| # where the symlink points to. | |
| url = "https://github.com/lowrisc/opentitan/tree/master/" | |
| url += "util/uvmdvgen/checklist.md.tpl" | |
| return "<a href=\"{}\">{}</a>".format(url, html.escape(rev[stage])) | |
| # Link S stages with the checklist table. | |
| def get_linked_sw_checklist(obj, rev, stage, is_latest_rev=True): | |
| if not stage or stage not in rev: | |
| return "" | |
| url = "" | |
| in_page_ref = "" | |
| if rev[stage] not in ["S0"]: | |
| # if in D0 or V0 stage, there is no in-page reference. | |
| in_page_ref = "#{}".format(html.escape(rev[stage]).lower()) | |
| # If the checklist is available, the commit id is available, and it is not | |
| # the latest revision, link to the committed version of the checklist. | |
| # Else, if checklist is available, then link to the current version of the | |
| # checklist html. | |
| # Else, link to the template. | |
| if 'sw_checklist' in obj and 'commit_id' in rev and not is_latest_rev: | |
| url = "https://github.com/lowrisc/opentitan/tree/{}/{}.md{}".format( | |
| rev['commit_id'], obj['sw_checklist'], in_page_ref) | |
| elif 'sw_checklist' in obj: | |
| url = get_doc_url(obj['_ip_desc_hjson_dir'], | |
| obj['sw_checklist'] + in_page_ref) | |
| else: | |
| # There is no checklist available, so point to the template. | |
| url = "https://github.com/lowrisc/opentitan/tree/master/" | |
| url += "doc/project/sw_checklist.md.tpl" | |
| return "<a href=\"{}\">{}</a>".format(url, html.escape(rev[stage])) | |
| # Link development stages in "L# : D# : V# : S#" format. | |
| # Hover text over each L, D, V, S indicates the stage mapping. | |
| # D, V, and S stages link to actual checklist items. | |
| def get_development_stage(obj, rev, is_latest_rev=True): | |
| if "life_stage" not in rev: | |
| return " " | |
| life_stage = rev['life_stage'] | |
| life_stage_html = "<span title='{}'>{}</span>".format( | |
| html.escape(convert_stage(life_stage)), html.escape(life_stage)) | |
| if life_stage != 'L0' and 'design_stage' in rev: | |
| design_stage = rev['design_stage'] | |
| design_stage_html = "<span title='{}'>{}</span>".format( | |
| html.escape(convert_stage(design_stage)), | |
| get_linked_checklist(obj, rev, 'design_stage', is_latest_rev)) | |
| else: | |
| design_stage_html = "-" | |
| if life_stage != 'L0' and 'verification_stage' in rev: | |
| verification_stage = rev['verification_stage'] | |
| verification_stage_html = "<span title='{}'>{}</span>".format( | |
| html.escape(convert_stage(verification_stage)), | |
| get_linked_checklist(obj, rev, 'verification_stage', | |
| is_latest_rev)) | |
| else: | |
| verification_stage_html = "-" | |
| if life_stage != 'L0' and 'dif_stage' in rev: | |
| dif_stage = rev['dif_stage'] | |
| dif_stage_html = "<span title='{}'>{}</span>".format( | |
| html.escape(convert_stage(dif_stage)), | |
| get_linked_sw_checklist(obj, rev, 'dif_stage', is_latest_rev)) | |
| else: | |
| dif_stage_html = "-" | |
| return [ | |
| life_stage_html, design_stage_html, verification_stage_html, | |
| dif_stage_html | |
| ] | |
| # Create dashboard of hardware IP development status | |
| def gen_dashboard_html(hjson_path, outfile): | |
| with hjson_path: | |
| prjfile = open(str(hjson_path)) | |
| try: | |
| obj = hjson.load(prjfile) | |
| except ValueError: | |
| raise SystemExit(sys.exc_info()[1]) | |
| if dashboard_validate.validate(obj) == 0: | |
| log.info("Generated dashboard object for " + str(hjson_path)) | |
| else: | |
| log.fail("hjson file import failed\n") | |
| ip_desc_hjson_dir = hjson_path.parent.relative_to(REPO_TOP) | |
| obj['_ip_desc_hjson_dir'] = str(ip_desc_hjson_dir) | |
| # If `revisions` field doesn't exist, the tool assumes the Hjson | |
| # as the previous project format, which has only one version entry. | |
| if "revisions" not in obj: | |
| print_version1_format(obj, outfile) | |
| else: | |
| print_multiversion_format(obj, outfile) | |
| return | |
| # Version 1 (single version) format | |
| def print_version1_format(obj, outfile): | |
| # yapf: disable | |
| genout(outfile, " <tr>\n") | |
| genout(outfile, " <td class=\"fixleft\">" + | |
| get_linked_design_spec(obj) + "</td>\n") | |
| genout(outfile, " <td class=\"dv-doc\">" + | |
| get_linked_dv_doc(obj) + "</td>\n") | |
| genout(outfile, " <td class=\"version\">" + | |
| get_linked_version(obj) + "</td>\n") | |
| for stage_html in get_development_stage(obj, obj): | |
| genout(outfile, | |
| " <td class=\"hw-stage\">" + stage_html + "</td>\n") | |
| if 'notes' in obj: | |
| genout(outfile, | |
| " <td>" + mk.markdown(obj['notes']).rstrip() + "</td>\n") | |
| else: | |
| genout(outfile, | |
| " <td><p> </p></td>\n") | |
| genout(outfile, " </tr>\n") | |
| # yapf: enable | |
| def print_multiversion_format(obj, outfile): | |
| # Sort the revision list based on the version field. | |
| # TODO: If minor version goes up gte than 10? | |
| revisions = sorted(obj["revisions"], key=lambda x: x["version"]) | |
| latest_rev = len(revisions) - 1 | |
| outstr = "" | |
| for i, rev in enumerate(revisions): | |
| outstr += " <tr>\n" | |
| # If only one entry in `revisions`, no need of `rowspan`. | |
| if len(revisions) == 1: | |
| outstr += " <td class='fixleft'>" | |
| outstr += get_linked_design_spec(obj) + "</td>\n" | |
| outstr += " <td class='dv-doc'>" | |
| outstr += get_linked_dv_doc(obj) + "</td>\n" | |
| # Print out the module name in the first entry only | |
| elif i == 0: | |
| outstr += " <td class='fixleft' rowspan='{}'>".format( | |
| len(revisions)) | |
| outstr += get_linked_design_spec(obj) + "</td>\n" | |
| outstr += " <td class='hw-stage' rowspan='{}'>".format( | |
| len(revisions)) | |
| outstr += get_linked_dv_doc(obj) + "</td>\n" | |
| # Version | |
| outstr += " <td class=\"version\">" | |
| outstr += get_linked_version(rev) + "</td>\n" | |
| # Development Stage | |
| for stage_html in get_development_stage(obj, rev, (i == latest_rev)): | |
| outstr += " <td class=\"hw-stage\"><span class='hw-stage'>" | |
| outstr += stage_html | |
| outstr += "</span></td>\n" | |
| # Notes | |
| if 'notes' in rev and rev['notes'] != '': | |
| outstr += " <td>" + mk.markdown( | |
| rev['notes']).rstrip() + "</td>\n" | |
| else: | |
| outstr += " <td><p> </p></td>\n" | |
| outstr += " </tr>\n" | |
| genout(outfile, outstr) | |
| # Create table of hardware specifications | |
| def gen_specboard_html(hjson_path, rel_hjson_path, outfile): | |
| with hjson_path: | |
| prjfile = open(str(hjson_path)) | |
| try: | |
| obj = hjson.load(prjfile) | |
| except ValueError: | |
| raise SystemExit(sys.exc_info()[1]) | |
| if dashboard_validate.validate(obj) == 0: | |
| log.info("Generated dashboard object for " + str(hjson_path)) | |
| else: | |
| log.error("hjson file import failed") | |
| # create design spec and DV doc references, check for existence below | |
| design_spec_md = re.sub(r'/data/', '/doc/', | |
| re.sub(r'\.prj\.hjson', '.md', str(hjson_path))) | |
| dv_doc_md = re.sub( | |
| r'/data/', '/doc/dv', | |
| re.sub(r'\.prj\.hjson', 'index.md', str(hjson_path))) | |
| design_spec_html = re.sub( | |
| r'/data/', '/doc/', | |
| re.sub(r'\.prj\.hjson', '.html', str(rel_hjson_path))) | |
| dv_doc_html = re.sub( | |
| r'/data/', '/doc/dv', | |
| re.sub(r'\.prj\.hjson', 'index.html', str(rel_hjson_path))) | |
| # yapf: disable | |
| genout(outfile, " <tr>\n") | |
| genout(outfile, " <td class=\"fixleft\">" + | |
| html.escape(obj['name']) + "</td>\n") | |
| if os.path.exists(design_spec_md): | |
| genout(outfile, " <td class=\"fixleft\"><a href=\"" + | |
| html.escape(design_spec_html) + "\">" + | |
| "design spec</a>\n") | |
| else: | |
| genout(outfile, " <td> </td>\n") | |
| if os.path.exists(dv_doc_md): | |
| genout(outfile, " <td class=\"fixleft\"><a href=\"" + | |
| html.escape(dv_doc_html) + "\">" + | |
| "DV document</a>\n") | |
| else: | |
| genout(outfile, " <td> </td>\n") | |
| genout(outfile, " </tr>\n") | |
| # yapf: enable | |
| return |