Merge pull request #5 from proppy/info-yaml

add support for info.yaml
diff --git a/.github/workflows/user_project_ci.yml b/.github/workflows/user_project_ci.yml
index 0bb7f88..12d29e9 100644
--- a/.github/workflows/user_project_ci.yml
+++ b/.github/workflows/user_project_ci.yml
@@ -53,16 +53,20 @@
         find $OPENLANE_ROOT/
         find $PDK_ROOT/
 
-    - name: harden tiny_user_project
-      run: |
-        make fetch
-        make tiny_user_project
-
     - name: setup python
       uses: actions/setup-python@v4
       with:
         python-version: '3.10'
 
+    - name: fetch verilog and build config
+      run: |
+        python -m pip install requests PyYAML
+        python configure.py --create-user-config
+        
+    - name: harden tiny_user_project
+      run: |
+        make tiny_user_project
+
     - name: show tiny_user_project metrics
       run: |
         python << EOF >> $GITHUB_STEP_SUMMARY
diff --git a/Makefile b/Makefile
index 0d38dd5..7c5010f 100644
--- a/Makefile
+++ b/Makefile
@@ -318,9 +318,3 @@
 	@$(MAKE) -C $(TIMING_ROOT) -f $(TIMING_ROOT)/timing.mk caravel-timing-fast
 	@$(MAKE) -C $(TIMING_ROOT) -f $(TIMING_ROOT)/timing.mk caravel-timing-slow
 	@echo "You can find results for all corners in $(CUP_ROOT)/signoff/caravel/openlane-signoff/timing/"
-
-WOKWI_PROJECT_ID=334445762078310996
-
-.PHONY: fetch
-fetch:
-	curl https://wokwi.com/api/projects/$(WOKWI_PROJECT_ID)/verilog | sed -e "s/user_module_$(WOKWI_PROJECT_ID)/user_module/" > verilog/rtl/user_module.v
diff --git a/README.md b/README.md
index 64e0a84..db03f76 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
 
 1. [Generate](https://github.com/proppy/tiny_user_project/generate) a new project based on this template
 
-2. Create a new [Wokwi](https://wokwi.com/projects/339800239192932947) project and update [WOKWI_PROJECT_ID](Makefile#L322)
+2. Create a new [Wokwi](https://wokwi.com/projects/339800239192932947) project and update [`info.yaml`](info.yaml) with your `wokwi_id`.
 
 3. Commit, push and check the [![user_project_ci](https://github.com/proppy/tiny_caravel_user_project/actions/workflows/user_project_ci.yml/badge.svg)](https://github.com/proppy/tiny_caravel_user_project/actions/workflows/user_project_ci.yml) workflow summary (if successful a new commit including the hardened files will be automatically created).
 
diff --git a/configure.py b/configure.py
new file mode 100755
index 0000000..ff5d954
--- /dev/null
+++ b/configure.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python3
+import requests
+import argparse
+import os
+import glob
+import json
+import yaml
+import logging
+import sys
+import csv
+import re
+
+
+def load_yaml(yaml_file):
+    with open(yaml_file, "r") as stream:
+        return (yaml.safe_load(stream))
+
+tiny_user_project_v = '''// generated by configure.py
+`default_nettype none
+
+module tiny_user_project(
+  input [`MPRJ_IO_PADS-1:0] io_in,
+  output [`MPRJ_IO_PADS-1:0] io_out,
+  output [`MPRJ_IO_PADS-1:0] io_oeb
+);
+
+// pass input and output pins defined in user_defines.v
+{module_name} mod (
+    io_in[19:12],
+    io_out[27:20]
+);
+// all output enabled
+assign io_oeb[27:20] = 8'b0;
+
+endmodule	// tiny_user_project
+
+`default_nettype wire'''
+    
+def write_user_config(module_name, sources):
+    with open('verilog/rtl/tiny_user_project.v', 'w') as fh:
+        fh.write(tiny_user_project_v.format(module_name=module_name))
+    with open('openlane/tiny_user_project/config.json', 'r') as fh:
+        config_json = json.load(fh)
+    sources.append('verilog/rtl/defines.v')
+    sources.append('verilog/rtl/tiny_user_project.v')
+    config_json['VERILOG_FILES'] = [f'dir::../../{s}' for s in sources]
+    with open('openlane/tiny_user_project/config.json', 'w') as fh:
+        json.dump(config_json, fh, indent=4)
+
+def get_project_source(yaml):
+    # wokwi_id must be an int or 0
+    try:
+        wokwi_id = int(yaml['project']['wokwi_id'])
+    except ValueError:
+        logging.error("wokwi id must be an integer")
+        exit(1)
+
+    # it's a wokwi project
+    if wokwi_id != 0:
+        url = "https://wokwi.com/api/projects/{}/verilog".format(wokwi_id)
+        logging.info("trying to download {}".format(url))
+        r = requests.get(url)
+        if r.status_code != 200:
+            logging.warning("couldn't download {}".format(url))
+            exit(1)
+
+        filename = "user_module_{}.v".format(wokwi_id)
+        with open(os.path.join('verilog/rtl', filename), 'wb') as fh:
+            fh.write(r.content)
+
+        # also fetch the wokwi diagram
+        url = "https://wokwi.com/api/projects/{}/diagram.json".format(wokwi_id)
+        logging.info("trying to download {}".format(url))
+        r = requests.get(url)
+        if r.status_code != 200:
+            logging.warning("couldn't download {}".format(url))
+            exit(1)
+
+        with open(os.path.join('verilog/rtl', "wokwi_diagram.json"), 'wb') as fh:
+            fh.write(r.content)
+
+        return [f'verilog/rtl/{filename}', 'verilog/rtl/cells.v']
+
+    # else it's HDL, so check source files
+    else:
+        if 'source_files' not in yaml['project']:
+            logging.error("source files must be provided if wokwi_id is set to 0")
+            exit(1)
+
+        source_files = yaml['project']['source_files']
+        if source_files is None:
+            logging.error("must be more than 1 source file")
+            exit(1)
+
+        if len(source_files) == 0:
+            logging.error("must be more than 1 source file")
+            exit(1)
+
+        if 'top_module' not in yaml['project']:
+            logging.error("must provide a top module name")
+            exit(1)
+
+        return source_files
+
+
+# documentation
+def check_docs(yaml):
+    for key in ['author', 'title', 'description', 'how_it_works', 'how_to_test', 'language']:
+        if key not in yaml['documentation']:
+            logging.error("missing key {} in documentation".format(key))
+            exit(1)
+        if yaml['documentation'][key] == "":
+            logging.error("missing value for {} in documentation".format(key))
+            exit(1)
+
+    # if provided, check discord handle is valid
+    if len(yaml['documentation']['discord']):
+        parts = yaml['documentation']['discord'].split('#')
+        if len(parts) != 2 or len(parts[0]) == 0 or not re.match('^[0-9]{4}$', parts[1]):
+            logging.error(f'Invalid format for discord username')
+            exit(1)
+
+
+def get_top_module(yaml):
+    wokwi_id = int(yaml['project']['wokwi_id'])
+    if wokwi_id != 0:
+        return "user_module_{}".format(wokwi_id)
+    else:
+        return yaml['project']['top_module']
+
+
+def get_stats():
+    with open('runs/wokwi/reports/metrics.csv') as f:
+        report = list(csv.DictReader(f))[0]
+
+    print('# Routing stats')
+    print()
+    print('| Utilisation | Wire length (um) |')
+    print('|-------------|------------------|')
+    print('| {} | {} |'.format(report['OpenDP_Util'], report['wire_length']))
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser(description="TT setup")
+
+    parser.add_argument('--check-docs', help="check the documentation part of the yaml", action="store_const", const=True)
+    parser.add_argument('--get-stats', help="print some stats from the run", action="store_const", const=True)
+    parser.add_argument('--create-user-config', help="create the user_config.tcl file with top module and source files", action="store_const", const=True)
+    parser.add_argument('--debug', help="debug logging", action="store_const", dest="loglevel", const=logging.DEBUG, default=logging.INFO)
+    parser.add_argument('--yaml', help="yaml file to load", default='info.yaml')
+
+    args = parser.parse_args()
+    # setup log
+    log_format = logging.Formatter('%(asctime)s - %(module)-10s - %(levelname)-8s - %(message)s')
+    # configure the client logging
+    log = logging.getLogger('')
+    # has to be set to debug as is the root logger
+    log.setLevel(args.loglevel)
+
+    # create console handler and set level to info
+    ch = logging.StreamHandler(sys.stdout)
+    # create formatter for console
+    ch.setFormatter(log_format)
+    log.addHandler(ch)
+
+    if args.get_stats:
+        get_stats()
+
+    elif args.check_docs:
+        logging.info("checking docs")
+        config = load_yaml(args.yaml)
+        check_docs(config)
+
+    elif args.create_user_config:
+        logging.info("creating include file")
+        config = load_yaml(args.yaml)
+        source_files = get_project_source(config)
+        top_module = get_top_module(config)
+        assert top_module != 'top'
+        write_user_config(top_module, source_files)
diff --git a/info.yaml b/info.yaml
new file mode 100644
index 0000000..a597d9e
--- /dev/null
+++ b/info.yaml
@@ -0,0 +1,46 @@
+--- 
+# TinyTapeout project information
+project:
+  wokwi_id:    334445762078310996        # If using wokwi, set this to your project's ID
+#  source_files:        # If using an HDL, set wokwi_id as 0 and uncomment and list your source files here. Source files must be in ./src
+#    - counter.v
+#    - decoder.v
+#  top_module:  "seven_segment_seconds"      # put the name of your top module here, make it unique by prepending your github username
+
+# As everyone will have access to all designs, try to make it easy for someone new to your design to know what
+# it does and how to operate it.
+#
+# Here is an example: https://github.com/mattvenn/tinytapeout_m_segments/blob/main/info.yaml
+#
+# This info will be automatically collected and used to make a datasheet for the chip.
+documentation: 
+  author:       ""      # Your name
+  discord:      ""      # Your discord handle - make sure to include the # part as well
+  title:        ""      # Project title
+  description:  ""      # Short description of what your project does
+  how_it_works: ""      # Longer description of how the project works
+  how_to_test:  ""      # Instructions on how someone could test your project, include things like what buttons do what and how to set the clock if needed
+  external_hw:  ""      # Describe any external hardware needed
+  language:     "wokwi" # other examples include Verilog, Amaranth, VHDL, etc
+  doc_link:     ""      # URL to longer form documentation, eg the README.md in your repository
+  clock_hz:     0       # Clock frequency in Hz (if required) we are expecting max clock frequency to be ~6khz. Provided on input 0.
+  picture:      ""      # relative path to a picture in your repository
+  inputs:               # a description of what the inputs do
+    - clock
+    - reset
+    - none
+    - none
+    - none
+    - none
+    - none
+    - none
+  outputs:
+    - segment a         # a description of what the outputs do
+    - segment b
+    - segment c
+    - segment d
+    - segment e
+    - segment f
+    - segment g
+    - none
+
diff --git a/verilog/rtl/tiny_user_project.v b/verilog/rtl/tiny_user_project.v
deleted file mode 100644
index cfea228..0000000
--- a/verilog/rtl/tiny_user_project.v
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2022 Google LLC
-//
-// 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
-//
-//      http://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.
-
-`default_nettype none
-
-module tiny_user_project(
-  input [`MPRJ_IO_PADS-1:0] io_in,
-  output [`MPRJ_IO_PADS-1:0] io_out,
-  output [`MPRJ_IO_PADS-1:0] io_oeb
-);
-
-// pass input and output pins defined in user_defines.v
-user_module mod (
-    .io_in (io_in[19:12]),
-    .io_out(io_out[27:20])
-);
-// all output enabled
-assign io_oeb[27:20] = 8'b0;
-
-endmodule	// tiny_user_project
-
-`default_nettype wire
\ No newline at end of file
diff --git a/verilog/rtl/user_module.v b/verilog/rtl/user_module.v
deleted file mode 100644
index 8b0b35c..0000000
--- a/verilog/rtl/user_module.v
+++ /dev/null
@@ -1,88 +0,0 @@
-/* Automatically generated from https://wokwi.com/projects/334445762078310996 */
-
-`default_nettype none
-
-module user_module(
-  input [7:0] io_in,
-  output [7:0] io_out
-);
-  wire net1 = 1'b1;
-  wire net2 = 1'b0;
-  wire net3;
-  wire net4;
-  wire net5;
-  wire net6;
-  wire net7;
-  wire net8 = 1'b1;
-  wire net9 = 1'b0;
-  wire net10;
-  wire net11;
-  wire net12 = 1'b1;
-  wire net13 = 1'b0;
-  wire net14;
-  wire net15 = 1'b1;
-  wire net16 = 1'b0;
-  wire net17;
-  wire net18 = 1'b0;
-  wire net19 = 1'b1;
-  wire net20;
-  wire net21 = 1'b1;
-  wire net22;
-  wire net23;
-  wire net24 = 1'b0;
-  wire net25 = 1'b0;
-
-  and_cell gate1 (
-    .a (net3)
-  );
-  or_cell gate2 (
-
-  );
-  xor_cell gate3 (
-
-  );
-  nand_cell gate4 (
-    .a (net4),
-    .b (net5),
-    .out (net6)
-  );
-  not_cell gate5 (
-    .in (net7),
-    .out (net5)
-  );
-  buffer_cell gate6 (
-
-  );
-  mux_cell mux1 (
-    .a (net8),
-    .b (net9),
-    .sel (net10),
-    .out (net11)
-  );
-  dff_cell flipflop1 (
-
-  );
-  mux_cell mux2 (
-    .a (net12),
-    .b (net13),
-    .sel (net10),
-    .out (net14)
-  );
-  mux_cell mux3 (
-    .a (net15),
-    .b (net16),
-    .sel (net10),
-    .out (net17)
-  );
-  mux_cell mux4 (
-    .a (net18),
-    .b (net19),
-    .sel (net10),
-    .out (net20)
-  );
-  and_cell gate7 (
-    .a (net22),
-    .b (net23),
-    .out (net4)
-  );
-endmodule