import json import logging import math import os import shutil 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, rewrite_image_paths_for_website from project import Project class Docs: def __init__(self, config: Config, projects: List[Project]): self.config = config self.projects = projects self.script_dir = os.path.dirname(os.path.realpath(__file__)) # stuff related to docs def build_index(self, filename: str = "shuttle_index.md"): logging.info(f"building {filename}") repo = git.Repo(".") readme = self.load_doc_template("shuttle_index_header.md.mustache") with open(filename, "w") as fh: fh.write( chevron.render( readme, { "name": self.config["name"], "git_repo": get_first_remote(repo), "git_commit": repo.head.commit.hexsha, }, ) ) fh.write("| Address | Author | Title | Type | Git Repo |\n") fh.write("| ------- | ------ | ------| -----| ---------|\n") self.projects.sort(key=lambda x: x.mux_address) for project in self.projects: fh.write(project.get_index_row()) def update_image(self): ruby = os.path.join(self.script_dir, "caravel_template", "dump_pic.rb") klayoutrc = os.path.join(self.script_dir, "caravel_template", "klayoutrc") lyp = os.path.join(self.script_dir, "caravel_template", "caravel.lyp") cmd = f"klayout -l {lyp} gds/user_project_wrapper.gds* -r {ruby} -c {klayoutrc}" logging.info(cmd) os.system(cmd) def load_doc_template(self, name: str) -> str: root = os.path.join(self.script_dir, "docs") doc_path = os.path.join(root, name) image_root = os.path.relpath(os.path.dirname(doc_path), ".") doc = frontmatter.load(doc_path) doc.content = rewrite_image_paths(doc.content, image_root) if "title" in doc: doc["title"] = rewrite_image_paths(doc["title"], image_root) if len(doc.keys()) > 0: return frontmatter.dumps(doc) + "\n" else: return doc.content + "\n" def build_datasheet( self, template_version: str, tapeout_index_path: str, ): logging.info(f"building datasheet with version {template_version}") 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"] ) 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) 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, ), ) # 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) # 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") 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, ), ) # 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) # clear subtiles for next group temp_subtile_projects = {} 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() def build_hugo_content(self, hugo_root: str) -> None: hugo_images = os.path.join(hugo_root, "images") shutil.rmtree(hugo_root) os.makedirs(hugo_root) os.makedirs(hugo_images) with open( os.path.join(self.script_dir, "docs", "hugo_template.md.mustache") ) as fh: doc_template = fh.read() with open( os.path.join(self.script_dir, "docs", "hugo_index_template.md.mustache") ) as fh: index_template = fh.read() # copy image # TODO, need to get image from somewhere # shutil.copyfile( # "tt/docs/pics/tinytapeout_numbered.png", # os.path.join(hugo_images, f'tinytapeout-{self.config["id"]}.png'), # ) # index page logging.info("building pages") hugo_tag = {"hugo": lambda text, render: "{{" + render(text) + "}}"} shuttle_info = { "shuttle_name": self.config["name"], "shuttle_id": self.config["id"], "project_count": len(self.projects), "end_date": self.config["end_date"], } with open(os.path.join(hugo_root, "_index.md"), "w") as fh: shuttle_info.update(hugo_tag) fh.write(chevron.render(index_template, shuttle_info)) fh.write("# All projects\n") fh.write("| Index | Title | Author |\n") fh.write("| ----- | ----- | -------|\n") self.projects.sort(key=lambda x: x.mux_address) for project in self.projects: logging.info(project) fh.write(project.get_hugo_row()) project_dir = os.path.join(hugo_root, f"{project.mux_address :03}") project_image_dir = os.path.join(project_dir, "images") os.makedirs(project_dir) os.makedirs(project_image_dir) yaml_data = project.get_project_docs_dict() yaml_data.update( { "mux_address": project.mux_address, "index": project.index, "weight": project.index, "git_action": project.get_workflow_url_when_submitted(), "shuttle_id": self.config["id"], "user_docs": rewrite_image_paths_for_website( yaml_data["user_docs"], os.path.join(project.src_dir, "docs"), project_image_dir, ), "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) ], "is_analog": bool(project.info.pinout.ua), } ) yaml_data.update(hugo_tag) doc = chevron.render(doc_template, yaml_data) with open(os.path.join(project_dir, "_index.md"), "w") as pfh: pfh.write(doc)