| # 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 |
| |
| import hjson |
| import mistletoe as mk |
| |
| import dashboard.dashboard_validate as dashboard_validate |
| |
| |
| 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', |
| 'V3': 'Verification Complete', |
| # DIF Stages (S for Software) |
| 'S0': 'Initial Work', |
| 'S1': 'Functional', |
| 'S2': 'Complete', |
| 'S3': 'Stable', |
| } |
| |
| # TODO: This is relative to the dashboard, which is currently located at |
| # hw/_index.md. |
| docs_server = "../.." |
| |
| |
| def convert_stage(stagestr): |
| return STAGE_STRINGS.get(stagestr, "UNKNOWN") |
| |
| |
| # Link module name with its design spec doc. |
| def get_linked_design_spec(obj): |
| result = "" |
| if 'design_spec' in obj.keys(): |
| url = docs_server + "/" + html.escape(obj['design_spec']) |
| result = "<span title='Design Spec'><a href='{}'>".format(url) |
| result += "<code>{}</code></a></span>".format(html.escape(obj['name'])) |
| else: |
| result = html.escape(obj['name']) |
| |
| return result |
| |
| |
| # Provide the link to the DV plan. |
| def get_linked_dv_plan(obj): |
| if 'dv_plan' in obj.keys(): |
| url = docs_server + "/" + html.escape(obj['dv_plan']) |
| return "<span title='DV Plan'><a href=\"{}\">DV</a></span>".format(url) |
| 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 = "{}/{}{}".format(docs_server, html.escape(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 = "{}/{}{}".format(docs_server, html.escape(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_mapping = convert_stage(life_stage) |
| separator = " : " |
| |
| if life_stage != 'L0' and 'design_stage' in rev: |
| design_stage = rev['design_stage'] |
| design_stage_mapping = convert_stage(design_stage) |
| else: |
| design_stage = None |
| |
| if life_stage != 'L0' and 'verification_stage' in rev: |
| verification_stage = rev['verification_stage'] |
| verification_stage_mapping = convert_stage(verification_stage) |
| else: |
| verification_stage = None |
| |
| if life_stage != 'L0' and 'dif_stage' in rev: |
| dif_stage = rev['dif_stage'] |
| dif_stage_mapping = convert_stage(dif_stage) |
| else: |
| dif_stage = None |
| |
| result = "<span title='{}'>{}</span>".format( |
| html.escape(life_stage_mapping), html.escape(life_stage)) |
| |
| if design_stage: |
| result += separator |
| result += "<span title='{}'>{}</span>".format( |
| html.escape(design_stage_mapping), |
| get_linked_checklist(obj, rev, 'design_stage', is_latest_rev)) |
| |
| if verification_stage: |
| result += separator |
| result += "<span title='{}'>{}</span>".format( |
| html.escape(verification_stage_mapping), |
| get_linked_checklist(obj, rev, 'verification_stage', |
| is_latest_rev)) |
| |
| if dif_stage: |
| result += separator |
| result += "<span title='{}'>{}</span>".format( |
| html.escape(dif_stage_mapping), |
| get_linked_sw_checklist(obj, rev, 'dif_stage', |
| is_latest_rev)) |
| |
| return result |
| |
| |
| # 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") |
| |
| # If `revisions` field doesn't exist, the tool assumes the Hjson |
| # as the previous project format, which has only one version entry. |
| if not "revisions" 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): |
| life_stage = obj['life_stage'] |
| life_stage_mapping = convert_stage(obj['life_stage']) |
| |
| # yapf: disable |
| genout(outfile, " <tr>\n") |
| name = html.escape(obj['name']) |
| genout(outfile, " <td class=\"fixleft\">" + |
| get_linked_design_spec(obj) + "</td>\n") |
| genout(outfile, " <td class=\"hw-stage\">" + |
| get_linked_dv_plan(obj) + "</td>\n") |
| genout(outfile, " <td class=\"hw-stage\">" + |
| get_linked_version(obj) + "</td>\n") |
| genout(outfile, " <td class=\"hw-stage\"><span class='hw-stage'>" + |
| get_development_stage(obj, obj) + "</span></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='hw-stage'>" |
| outstr += get_linked_dv_plan(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_plan(obj) + "</td>\n" |
| |
| # Version |
| outstr += " <td class=\"hw-stage\">" |
| outstr += get_linked_version(rev) + "</td>\n" |
| |
| # Development Stage |
| outstr += " <td class=\"hw-stage\"><span class='hw-stage'>" |
| outstr += get_development_stage(obj, rev, (i == latest_rev)) |
| 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 plan references, check for existence below |
| design_spec_md = re.sub(r'/data/', '/doc/', |
| re.sub(r'\.prj\.hjson', '.md', str(hjson_path))) |
| dv_plan_md = re.sub( |
| r'/data/', '/doc/', |
| re.sub(r'\.prj\.hjson', '_dv_plan.md', str(hjson_path))) |
| design_spec_html = re.sub( |
| r'/data/', '/doc/', |
| re.sub(r'\.prj\.hjson', '.html', str(rel_hjson_path))) |
| dv_plan_html = re.sub( |
| r'/data/', '/doc/', |
| re.sub(r'\.prj\.hjson', '_dv_plan.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_plan_md): |
| genout(outfile, |
| " <td class=\"fixleft\"><a href=\"" + |
| html.escape(dv_plan_html) + "\">" + |
| "DV plan</a>\n") |
| else: |
| genout(outfile, |
| " <td> </td>\n") |
| genout(outfile, " </tr>\n") |
| # yapf: enable |
| return |