feat(docs): add new datasheet generation method (#123)

This commit is contained in:
kris j
2025-10-16 00:11:07 +02:00
committed by htfab
parent 13c23e047d
commit c36badfb60
37 changed files with 2664 additions and 121 deletions
+198 -85
View File
@@ -1,13 +1,16 @@
import json
import logging
import math
import os
import subprocess
from typing import List, Optional
from typing import List
import chevron
import frontmatter # type: ignore
import git # type: ignore
import yaml
from config import Config
from doc_utils import DocsHelper
from git_utils import get_first_remote
from markdown_utils import rewrite_image_paths
from project import Project
@@ -62,97 +65,207 @@ class Docs:
else:
return doc.content + "\n"
def write_datasheet(self, markdown_file: str, pdf_file: Optional[str] = None):
doc_header = self.load_doc_template("doc_header.md.mustache")
doc_chip_map = self.load_doc_template("../../docs/chip_map.md")
doc_template = self.load_doc_template("doc_template.md.mustache")
doc_pinout = self.load_doc_template("PINOUT.md")
doc_info = self.load_doc_template("../../tt-multiplexer/docs/INFO.md")
doc_credits = self.load_doc_template("CREDITS.md")
def build_datasheet(
self,
template_version: str,
tapeout_index_path: str,
):
logging.info(f"building datasheet with version {template_version}")
with open(markdown_file, "w") as fh:
repo = git.Repo(".")
fh.write(
chevron.render(
doc_header,
{
"name": self.config["name"],
"repo": get_first_remote(repo),
},
)
tapeout_index = None
# if given a path but the file doesn't exist
if tapeout_index_path is not None and not os.path.isfile(tapeout_index_path):
raise FileNotFoundError("unable to find tapeout index at given path")
# if given a path but it does exist
elif tapeout_index_path is not None:
with open(os.path.abspath(tapeout_index_path), "r") as f:
index = json.load(f)
tapeout_index = sorted(
index["projects"], key=lambda project: project["address"]
)
fh.write(doc_chip_map)
fh.write("# Projects\n")
elif tapeout_index_path is None:
logging.warning("tapeout index not provided, using project list")
self.projects.sort(key=lambda x: x.mux_address)
tapeout_index = map(DocsHelper.normalise_project_info, self.projects)
for project in self.projects:
yaml_data = project.get_project_docs_dict()
analog_pins = project.info.analog_pins
yaml_data.update(
{
"user_docs": rewrite_image_paths(
yaml_data["user_docs"],
f"projects/{project.get_macro_name()}/docs",
),
"mux_address": project.mux_address,
"pins": [
{
"pin_index": str(i),
"ui": project.info.pinout.ui[i],
"uo": project.info.pinout.uo[i],
"uio": project.info.pinout.uio[i],
}
for i in range(8)
],
"analog_pins": [
{
"ua_index": str(i),
"analog_index": str(project.analog_pins[i]),
"desc": desc,
}
for i, desc in enumerate(
project.info.pinout.ua[:analog_pins]
)
],
"is_analog": analog_pins > 0,
}
datasheet_content_config = None
if "datasheet_config" in self.config:
logging.info("using datasheet config in config.yaml")
datasheet_content_config = self.config["datasheet_config"]
with open(os.path.join(self.script_dir, "docs/datasheet.typ.mustache")) as f:
datasheet_template = f.read()
DocsHelper.configure_datasheet(
self.config, datasheet_template, template_version
)
danger_info = {}
if not os.path.isfile("./projects/danger_level.yaml"):
logging.warning("danger_level.yaml not found")
else:
with open(os.path.abspath("./projects/danger_level.yaml"), "r") as f:
content = yaml.safe_load(f)
# yaml.safe_load() returns None for an empty file
# check before overwriting since we rely on danger_info being a dict later
if content is not None:
danger_info = content
with open(os.path.join(self.script_dir, "docs/user_project.typ.mustache")) as f:
project_template = f.read()
datasheet_manifest = [f'#import "/tt/docs/typst/src/tt.typ" as tt\n']
# handle art
current_project = 0
art_index = 0
total_available_art = None
if (
datasheet_content_config is not None
and "artwork" in datasheet_content_config
):
total_available_art = len(datasheet_content_config["artwork"])
if total_available_art is not None:
if total_available_art > 0:
insert_art_after = math.floor(len(self.projects) / total_available_art)
temp_subtile_projects = {}
for project in tapeout_index:
logging.info(
f"processing datasheet for [{project['address']} : {project['repo']}]"
)
# fetch info.yaml
yaml_path = os.path.abspath(f"./projects/{project['macro']}/info.yaml")
if "type" in project:
if project["type"] == "subtile":
yaml_path = os.path.abspath(
f"./projects/{project['subtile_group']}/docs/{project['macro']}/info.yaml"
)
try:
with open(yaml_path, "r") as f:
yaml_info = yaml.safe_load(f)
except FileNotFoundError:
logging.warning(
f"unable to read {yaml_path}... project exists in tapeout index but not in local directory? skipping"
)
continue
info = DocsHelper.format_project_info(yaml_info, project)
# decide what to do given project type
if info["type"] == "subtile":
# defer for later (when we hit the group module)
# we want the group module to appear first in the datasheet
temp_subtile_projects[info["subtile_addr"]] = info
continue
elif info["type"] == "group":
# write group doc
group_md_path = f"projects/{info['macro']}/docs/info.md"
group_typ_path = f"projects/{info['macro']}/docs/doc.typ"
DocsHelper.write_doc(
path=os.path.abspath(group_typ_path),
template=project_template,
content=DocsHelper.populate_template_tags(
info=info,
danger_info=danger_info,
docs=DocsHelper.get_docs_as_typst(group_md_path),
template_version=template_version,
),
)
logging.info(f"building datasheet for {project}")
# add group doc to manifest
include_str = f'#include "{group_typ_path}"\n'
if (
datasheet_content_config is not None
and "disabled" in datasheet_content_config
):
if info["macro"] in datasheet_content_config["disabled"]:
logging.warning(
f"datasheet disabled for [{info['address']} : {info['git_url']}"
)
include_str = "// " + include_str
datasheet_manifest.append(include_str)
# ensure that optional fields are set
for key in [
"author",
"description",
"clock_hz",
"git_url",
"doc_link",
]:
if key not in yaml_data:
yaml_data[key] = ""
# write subtile doc
for _, subtile_info in temp_subtile_projects.items():
partial_doc_path = f"projects/{subtile_info['subtile_group']}/docs/{subtile_info['macro']}"
typ_path = os.path.join(partial_doc_path, "doc.typ")
md_path = os.path.join(partial_doc_path, "info.md")
# now build the doc & print it
try:
doc = chevron.render(doc_template, yaml_data)
fh.write(doc)
fh.write("\n```{=latex}\n\\clearpage\n```\n")
except IndexError:
logging.warning("missing pins in info.yaml, skipping")
DocsHelper.write_doc(
path=typ_path,
template=project_template,
content=DocsHelper.populate_template_tags(
info=subtile_info,
danger_info=danger_info,
docs=DocsHelper.get_docs_as_typst(md_path),
template_version=template_version,
),
)
# ending
fh.write(doc_pinout)
fh.write("\n```{=latex}\n\\clearpage\n```\n")
fh.write(doc_info)
fh.write("\n```{=latex}\n\\clearpage\n```\n")
fh.write(doc_credits)
# add subtile doc to manifest
include_str = f'#include "{typ_path}"\n'
if DocsHelper.project_is_disabled(
datasheet_content_config, subtile_info["macro"]
):
logging.warning(
f"datasheet disabled for [{subtile_info['address']} : {subtile_info['git_url']}]"
)
include_str = "// " + include_str
datasheet_manifest.append(include_str)
logging.info(f"wrote markdown to {markdown_file}")
# clear subtiles for next group
temp_subtile_projects = {}
if pdf_file is not None:
pdf_cmd = f"pandoc --toc --toc-depth 2 --pdf-engine=xelatex -i {markdown_file} -o {pdf_file} --from gfm+raw_attribute+smart+attributes"
logging.info(pdf_cmd)
p = subprocess.run(pdf_cmd, shell=True)
if p.returncode != 0:
logging.error("pdf generation failed")
raise RuntimeError(f"pdf generation failed with code {p.returncode}")
elif info["type"] == "project":
project_md_path = f"projects/{info['macro']}/docs/info.md"
project_typ_path = f"projects/{info['macro']}/docs/doc.typ"
DocsHelper.write_doc(
path=project_typ_path,
template=project_template,
content=DocsHelper.populate_template_tags(
info=info,
danger_info=danger_info,
docs=DocsHelper.get_docs_as_typst(project_md_path),
template_version=template_version,
),
)
# add project doc to manifest
include_str = f'#include "{project_typ_path}"\n'
if DocsHelper.project_is_disabled(
datasheet_content_config, info["macro"]
):
logging.warning(
f"datasheet disabled for [{info['address']} : {info['git_url']}]"
)
include_str = "// " + include_str
datasheet_manifest.append(include_str)
else:
logging.error(f"unhandled project type! what is \"{info['type']}\"?")
exit(1)
# insert artwork
current_project += 1
if total_available_art is not None:
if not (art_index >= len(datasheet_content_config["artwork"])):
if current_project % insert_art_after == 0:
details = datasheet_content_config["artwork"][art_index]
datasheet_manifest.append(
f"#tt.datasheet.art(\"{details['id']}\", rot:{details['rotate']})\n"
)
art_index += 1
with open("datasheet_manifest.typ", "w") as f:
f.writelines(datasheet_manifest)
DocsHelper.compile()