diff --git a/.github/workflows/generate-rst.yml b/.github/workflows/generate-rst.yml new file mode 100644 index 0000000..0b3fe10 --- /dev/null +++ b/.github/workflows/generate-rst.yml @@ -0,0 +1,35 @@ +name: Generate cells docs + +on: + push: + pull_request: + +jobs: + + generate-cells: + runs-on: ubuntu-18.04 + steps: + + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Generate cells READMEs + run: | + SUBMODULE_VERSION=latest make submodules -j3 || make submodules -j1 + + git submodule foreach 'git checkout master -q' + + make env + source env/conda/bin/activate skywater-pdk-scripts + which python + python scripts/python-skywater-pdk/skywater_pdk/cells/list.py libraries/*/latest/ + python scripts/python-skywater-pdk/skywater_pdk/cells/generate/readme.py libraries/*/latest/cells/* + + git submodule foreach 'git add .' + export DAT=`date +"%Y-%m-%d %k:%M"` + + # TODO replace below with appropriate credentials + git config --global user.email "action@github.com" + git config --global user.name "Github Action" + git submodule foreach 'git commit -q -m "$DAT" || true' diff --git a/requirements.txt b/requirements.txt index fe8a2c1..b67d1dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,10 @@ flake8 # rst_include tool as GitHub doesn't support `.. include::` when rendering # previews. rst_include +docutils +dataclasses +dataclasses_json +sphinx # The Python API for the SkyWater PDK. -e scripts/python-skywater-pdk diff --git a/scripts/python-skywater-pdk/skywater_pdk/cells/generate/readme.py b/scripts/python-skywater-pdk/skywater_pdk/cells/generate/readme.py new file mode 100755 index 0000000..82a30c3 --- /dev/null +++ b/scripts/python-skywater-pdk/skywater_pdk/cells/generate/readme.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright 2020 The SkyWater PDK Authors. +# +# Use of this source code is governed by the Apache 2.0 +# license that can be found in the LICENSE file or at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +''' This is a prototype of cell documentation generation script. +''' + +import csv +import json +import os +import sys +import argparse +import pathlib +import glob +import subprocess +import textwrap + + +readme_template ="""\ +{header} +{headerUL} + +**{description}** + +*This is a stub of cell description file* + +- **Cell name**: {name} +- **Type**: {deftype} +- **Verilog name**: {verilog_name} +- **Library**: {library} +- **Inputs**: {inputs} +- **Outputs**: {outputs} + +Symbols +------- + +.. list-table:: + + * - .. figure:: {symbol1} + - + - .. figure:: {symbol2} + +Schematic +--------- + +.. figure:: {schematic} + :align: center + +GDSII Layouts +------------- + +""" + +figure_template =""" + +.. figure:: {fig} + :align: center + :width: 50% + + {name} +""" + +def write_readme(cellpath, define_data): + ''' Generates README for a given cell. + + Args: + cellpath - path to a cell [str of pathlib.Path] + define_data - cell data from json [dic] + + ''' + netlist_json = os.path.join(cellpath, define_data['file_prefix']+'.json') + assert os.path.exists(netlist_json), netlist_json + outpath = os.path.join(cellpath, 'README.rst') + + prefix = define_data['file_prefix'] + header = prefix + symbol1 = prefix + '.symbol.svg' + symbol2 = prefix + '.pp.symbol.svg' + schematic = prefix + '.schematic.svg' + inputs = [] + outputs = [] + for p in define_data['ports']: + try: + if p[0]=='signal' and p[2]=='input': + inputs.append(p[1]) + if p[0]=='signal' and p[2]=='output': + outputs.append(p[1]) + except: + pass + gdssvg = [] + svglist = list(pathlib.Path(cellpath).glob('*.svg')) + for s in svglist: + gdsfile = pathlib.Path(os.path.join(cellpath, s.stem +'.gds')) + if gdsfile.is_file(): + gdssvg.append(s) + + with open(outpath, 'w') as f: + f.write (readme_template.format ( + header = header, + headerUL = '=' * len(header), + description = define_data['description'].rstrip('.'), + name = ':cell:`' + prefix +'`', + deftype = define_data['type'], + verilog_name = define_data['verilog_name'], + library = define_data['library'], + inputs = f'{len(inputs)} (' + ', '.join(inputs) + ')', + outputs = f'{len(outputs)} (' + ', '.join(outputs) + ')', + symbol1 = symbol1, + symbol2 = symbol2, + schematic = schematic, + )) + for gs in sorted(gdssvg): + f.write (figure_template.format ( + fig = gs.name, + name = gs.stem + )) + +def process(cellpath): + ''' Processes cell indicated by path. + Opens cell definiton and calls further processing + + Args: + cellpath - path to a cell [str of pathlib.Path] + ''' + + print() + print(cellpath) + define_json = os.path.join(cellpath, 'definition.json') + if not os.path.exists(define_json): + print("No definition.json in", cellpath) + assert os.path.exists(define_json), define_json + define_data = json.load(open(define_json)) + + if define_data['type'] == 'cell': + write_readme(cellpath, define_data) + + return + + +def main(): + ''' Generates README.rst for cell.''' + + prereq_txt = '' + output_txt = 'output:\n generates README.rst' + allcellpath = '../../../libraries/*/latest/cells/*' + + parser = argparse.ArgumentParser( + description = main.__doc__, + epilog = prereq_txt +'\n\n'+ output_txt, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument( + "--all_libs", + help="process all cells in "+allcellpath, + action="store_true") + parser.add_argument( + "cell_dir", + help="path to the cell directory", + type=pathlib.Path, + nargs="*") + + args = parser.parse_args() + + if args.all_libs: + path = pathlib.Path(allcellpath).expanduser() + parts = path.parts[1:] if path.is_absolute() else path.parts + paths = pathlib.Path(path.root).glob(str(pathlib.Path("").joinpath(*parts))) + args.cell_dir = list(paths) + + cell_dirs = [d.resolve() for d in args.cell_dir if d.is_dir()] + + errors = 0 + for d in cell_dirs: + try: + process(d) + except KeyboardInterrupt: + sys.exit(1) + except (AssertionError, FileNotFoundError, ChildProcessError) as ex: + print (f'Error: {type(ex).__name__}') + print (f'{ex.args}') + errors +=1 + print (f'\n{len(cell_dirs)} files processed, {errors} errors.') + return 0 if errors else 1 + +if __name__ == "__main__": + sys.exit(main()) + diff --git a/scripts/python-skywater-pdk/skywater_pdk/cells/list.py b/scripts/python-skywater-pdk/skywater_pdk/cells/list.py new file mode 100755 index 0000000..3f051b8 --- /dev/null +++ b/scripts/python-skywater-pdk/skywater_pdk/cells/list.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright 2020 SkyWater PDK Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +import argparse +import json +import os +import pathlib +import pprint +import sys +import textwrap +from docutils import nodes +from docutils.parsers.rst import Directive +from docutils.statemachine import ViewList +from sphinx.util.nodes import nested_parse_with_titles + +from typing import Tuple, List, Dict + +# using a list-table here to allow for easier line breaks in description +rst_header_line_char = '-' +rst_header = 'List of cells in :lib:`{libname}`' +rst_template ="""\ +{header_line} +{header_underline} + +.. list-table:: + :header-rows: 1 + + * - Cell name + - Description + - Type + - Verilog name +{cell_list} +""" + + +cell_template = """\ + * - :doc:`{cell_name} <{link}>` + - {description} + - {type} + - {verilog_name} +""" + + +def collect(library_dir) -> Tuple[str, List[str]]: + """Collect the available definitions for cells in a library + + Parameters + ---------- + library_dir: str or pathlib.Path + Path to a library. + + Returns + ------- + lib : str + Library name + + cells : list of pathlib.Path + definition files for cells in the library. + """ + + if not isinstance(library_dir, pathlib.Path): + library_dir = pathlib.Path(library_dir) + + libname = None + cells = set() + + for p in library_dir.rglob("definition.json"): + if not p.is_file(): + continue + cells.add(p) + if libname is None: + with open(str(p), "r") as sample_json: + sample_def = json.load(sample_json) + libname = sample_def['library'] + + assert len(libname) > 0 + cells = list(sorted(cells)) + return libname, cells + + +def generate_rst(library_dir, library_name, cells): + """Generate the RST paragraph containing basic information about cells + + Parameters + ---------- + library_dir: str or pathlib.Path + Path to a library. + + library_name: str + Name of the library + + cells: list of pathlib.Path + List of paths to JSON description files + + Returns + ------- + paragraph: str + Generated paragraph + """ + + if not isinstance(library_dir, pathlib.Path): + library_dir = pathlib.Path(library_dir) + + paragraph = "" + cell_list = "" + + for cell in cells: + with open(str(cell), "r") as c: + cell_json = json.load(c) + cell_list = cell_list + cell_template.format( + cell_name = cell_json['name'], + #description = cell_json['description'].replace("\n", "\n "), + description = textwrap.indent(cell_json['description'], ' ').lstrip(), + type = cell_json['type'], + verilog_name = cell_json['verilog_name'], + #link = str(cell.resolve()).rpartition('/')[0] + '/README' + link = 'cells/' + str(cell).rpartition('/')[0].rpartition('/')[2] + '/README' + ) + + header = rst_header.format(libname = library_name) + paragraph = rst_template.format( + header_line = header, + header_underline = rst_header_line_char * len(header), + cell_list = cell_list + ) + return paragraph + + +def AppendToReadme (celllistfile): + ''' Prototype of library README builder ''' + readmefile = pathlib.Path(celllistfile.parents[0], 'README.rst') + old = '' + if readmefile.exists(): + with open(str(readmefile), "r") as f: + for i, l in enumerate(f): + if i<5: old += l + + with open(str(readmefile), "w+") as f: + f.write(old) + tableinc = f'.. include:: {celllistfile.name}\n' + + if not tableinc in old: + f.write(tableinc) + + +# --- Sphinx extension wrapper --- + +class CellList(Directive): + + def run(self): + env = self.state.document.settings.env + dirname = env.docname.rpartition('/')[0] + libname, cells = collect(dirname) + paragraph = generate_rst(dirname, libname, cells) + # parse rst string to docutils nodes + rst = ViewList() + for i,line in enumerate(paragraph.split('\n')): + rst.append(line, libname+"-cell-list.rst", i+1) + node = nodes.section() + node.document = self.state.document + nested_parse_with_titles(self.state, rst, node) + return node.children + + +def setup(app): + app.add_directive("cell_list", CellList) + + return { + 'version': '0.1', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } + +# --- stand alone, command line operation --- + +def main(): + + alllibpath = '../../../libraries/*/latest' + parser = argparse.ArgumentParser() + parser.add_argument( + "--all_libs", + help="process all libs in "+alllibpath, + action="store_true") + parser.add_argument( + "library_dir", + help="Path to the library.", + type=pathlib.Path, + nargs='*') + + args = parser.parse_args() + + if args.all_libs: + path = pathlib.Path(alllibpath).expanduser() + parts = path.parts[1:] if path.is_absolute() else path.parts + paths = pathlib.Path(path.root).glob(str(pathlib.Path("").joinpath(*parts))) + args.library_dir = list(paths) + + + libs = [pathlib.Path(d) for d in args.library_dir] + libs = [d for d in libs if d.is_dir()] + + for l in libs: print (str(l)) + + for lib in libs: + + print(f'\nAnalysing {lib}') + try: + libname, cells = collect(lib) + print(f"Library name: {libname}, found {len(cells)} cells") + paragraph = generate_rst(lib, libname, cells) + library_dir = pathlib.Path(lib) + cell_list_file = pathlib.Path(library_dir, "cell-list.rst") + except: + print(f' ERROR: failed to fetch cell list') + continue + try: + with(open(str(cell_list_file), "w")) as c: + c.write(paragraph) + print(f'Generated {cell_list_file}') + AppendToReadme(cell_list_file) + print(f'Appended to README') + except FileNotFoundError: + print(f" ERROR: Failed to create {str(cell_list_file)}", file=sys.stderr) + raise + + +if __name__ == "__main__": + sys.exit(main()) +