wip configure for tt02
diff --git a/configure.py b/configure.py
index a85740c..563c1b6 100755
--- a/configure.py
+++ b/configure.py
@@ -3,239 +3,202 @@
 import yaml
 from typing import List
 from urllib.parse import urlparse
-import argparse, requests, base64, zipfile, io, logging, pickle, shutil, sys, os, collections, subprocess
+import argparse, requests, base64, io, logging, pickle, shutil, sys, os, collections, subprocess
+from git_utils import fetch_file_from_git, install_artifacts
+import git
+
 
 # pipe handling
 from signal import signal, SIGPIPE, SIG_DFL
 signal(SIGPIPE, SIG_DFL)
 
 tmp_dir = '/tmp/tt'
+project_dir = 'projects'
 DEFAULT_NUM_PROJECTS = 473
 
 
 class Projects():
 
-    def __init__(self, update_cache=False, update_single=None, test=False):
-        self.default_project = 0
-        self.test = test
-        if update_cache:
-            logging.info("updating cache, this could take a while")
-            self.update_cache(update_single)
+    def __init__(self, args):
+        self.args = args
+        self.project_urls = self.get_project_urls()
+        assert len(self.project_urls) == DEFAULT_NUM_PROJECTS
+        logging.info(f"loaded {len(self.project_urls)} projects")
+
+        self.projects = []
+        for index, git_url in enumerate(self.project_urls):
+            if git_url == self.get_filler_project():
+                self.projects.append(Project(index, git_url, fill=True))
+            else:
+                self.projects.append(Project(index, git_url, fill=False))
+           
+        if args.clone_single is not None:
+            project = self.projects[args.clone_single]
+            logging.info(f"cloning {project}")
+            project.clone()
+
+        if args.clone_all:
+            first_fill = False
+            for project in self.projects:
+                if not first_fill:
+                    logging.info(f"cloning {project}")
+                    project.clone()
+                    first_fill = project.fill
+
+        if args.fetch_gds:
+            first_fill = False
+            for project in self.projects:
+                if not first_fill:
+                    logging.info(f"installing gds for {project}")
+                    project.fetch_gds()
+                    first_fill = project.fill
+
+        # projects should now be installed
+        first_fill = False
+        logging.info(f"loading project yaml")
+        for project in self.projects:
+            if not first_fill:
+                project.load_yaml()
+                first_fill = project.fill
+
+        logging.info(f"copying files to caravel")
+        first_fill = False
+        for project in self.projects:
+            if not first_fill:
+                project.copy_files_to_caravel()
+                first_fill = project.fill
+
+    def get_filler_project(self):
+        if self.args.test:
+            from project_urls_test import filler_project_url
         else:
-            # load projects from cache
-            try:
-                self.wokwi_ids = pickle.load(open(self.get_projects_db(), 'rb'))
-                logging.info("loaded {} projects".format(len(self.wokwi_ids)))
-            except FileNotFoundError:
-                logging.error("project cache {} not found, use --update-cache to build it".format(self.get_projects_db()))
-
-        # all ids must be unique
-        assert len(set(self.wokwi_ids)) == len(self.wokwi_ids)
-
-    def update_cache(self, update_single=None):
-        self.wokwi_ids = []
-        for url in self.get_project_urls():
-            if update_single is not None:
-                if url != update_single:
-                    continue
-            wokwi_id = self.install_artifacts(url)
-            if wokwi_id in self.wokwi_ids:
-                logging.error("wokwi id already exists!")
-                exit(1)
-            self.wokwi_ids.append(wokwi_id)
-
-        # cache it
-        with open(self.get_projects_db(), 'wb') as fh:
-            pickle.dump(self.wokwi_ids, fh)
-
-    def get_projects_db(self):
-        if self.test:
-            return "projects_test.pkl"
-        else:
-            return "projects.pkl"
-
-    # filling the space with default projects is handled by returning the default on exception
-    def get_macro_instance(self, id):
-        try:
-            return "user_module_{}_{}".format(self.wokwi_ids[id], id)
-        except IndexError:
-            return "user_module_{}_{}".format(self.wokwi_ids[self.default_project], id)
-
-    def get_scanchain_instance(self, id):
-        try:
-            return "scanchain_{}".format(id)
-        except IndexError:
-            return "scanchain_{}".format(id)
-
-    def get_wokwi_id(self, id):
-        try:
-            return self.wokwi_ids[id]
-        except IndexError:
-            return self.wokwi_ids[self.default_project]
-
-    def get_macro_gds_name(self, id):
-        try:
-            return "user_module_{}.gds".format(self.wokwi_ids[id])
-        except IndexError:
-            return "user_module_{}.gds".format(self.wokwi_ids[self.default_project])
-
-    def get_macro_lef_name(self, id):
-        try:
-            return "user_module_{}.lef".format(self.wokwi_ids[id])
-        except IndexError:
-            return "user_module_{}.lef".format(self.wokwi_ids[self.default_project])
-
-    def get_macro_name(self, id):
-        try:
-            return "user_module_{}".format(self.wokwi_ids[id])
-        except IndexError:
-            return "user_module_{}".format(self.wokwi_ids[self.default_project])
-
-    def get_verilog_include(self, id):
-        try:
-            return '`include "user_module_{}.v"\n'.format(self.wokwi_ids[id])
-        except IndexError:
-            return ''
-
-    def get_gl_verilog_names(self, id):
-        try:
-            return ['user_module_{}.v'.format(self.wokwi_ids[id])]
-        except IndexError:
-            return []
-
-    def get_verilog_names(self, id):
-        try:
-            return ["user_module_{}.v".format(self.wokwi_ids[id])]
-        except IndexError:
-            return []
-
-    def get_wokwi_ids(self):
-        return self.wokwi_ids
-
-    def get_giturl(self, id):
-        try:
-            return self.get_project_urls()[id]
-        except IndexError:
-            return self.get_project_urls()[self.default_project]
-
-    @classmethod
-    def build_wokwi_url(Project, wokwi_id):
-        return "https://wokwi.com/projects/{}".format(wokwi_id)
+            from project_urls import filler_project_url
+        return filler_project_url
 
     def get_project_urls(self):
-        if self.test:
+        if self.args.test:
             from project_urls_test import project_urls, filler_project_url
         else:
             from project_urls import project_urls, filler_project_url
 
-        return [filler_project_url] + project_urls
+        filler_projects = DEFAULT_NUM_PROJECTS - len(project_urls)
+        return project_urls + filler_projects * [filler_project_url]
 
     def check_dupes(self):
-        project_urls = self.get_project_urls()
+        from project_urls import project_urls
         duplicates = [item for item, count in collections.Counter(project_urls).items() if count > 1]
         if duplicates:
             logging.error("duplicate projects: {}".format(duplicates))
             exit(1)
 
-    @classmethod
-    def load_yaml(project, yaml_file):
-        with open(yaml_file, "r") as stream:
-            return (yaml.safe_load(stream))
+class Project():
 
-    @classmethod
-    def get_wokwi_id_from_yaml(project, yaml):
-        # wokwi_id must be an int or 0
+    def __init__(self, index, git_url, fill):
+        self.git_url = git_url
+        self.index = index
+        self.fill = fill
+        self.local_dir = os.path.join(os.path.join(project_dir, str(self.index)))
+
+    def load_yaml(self):
+        with open(os.path.join(self.local_dir, 'info.yaml')) as fh:
+            self.yaml = yaml.safe_load(fh)
+        self.wokwi_id = self.yaml['project']['wokwi_id']
+        if self.wokwi_id == 0:
+            # HDL project
+            # HDL project
+            self.top_module = self.yaml['project']['top_module']
+            self.src_files = self.yaml['project']['source_files']
+
+    def clone(self):
         try:
-            wokwi_id = int(yaml['project']['wokwi_id'])
-            return wokwi_id
-        except ValueError:
-            logging.error("wokwi id must be an integer")
-            exit(1)
+            git.Repo.clone_from(self.git_url, self.local_dir)
+        except git.exc.GitCommandError as e:
+            if 'already exists' in str(e):
+                logging.info("already exists")
 
-    # the latest artifact isn't necessarily the one related to the latest commit, as github
-    # could have taken longer to process an older commit than a newer one.
-    # so iterate through commits and return the artifact that matches
-    @classmethod
-    def get_most_recent_action_url(project, commits, artifacts):
-        release_sha_to_download_url = {artifact['workflow_run']['head_sha']: artifact['archive_download_url'] for artifact in artifacts}
-        for commit in commits:
-            if commit['sha'] in release_sha_to_download_url:
-                return release_sha_to_download_url[commit['sha']]
+    def update(self):
+        # do a pull
+        pass
 
-    @classmethod
-    def split_git_url(Project, url):
-        res = urlparse(url)
-        try:
-            _, user_name, repo = res.path.split('/')
-        except ValueError:
-            logging.error("couldn't split repo from {}".format(url))
-            exit(1)
-        repo = repo.replace('.git', '')
-        return user_name, repo
+    def __str__(self):
+        return f"{self.index} {self.git_url}"
 
-    # download the artifact for each project to get the gds & lef
-    def install_artifacts(self, url):
-        logging.debug(url)
-        user_name, repo = Projects.split_git_url(url)
+    def fetch_gds(self):
+        install_artifacts(self.git_url, self.local_dir)
 
-        # authenticate for rate limiting
-        auth_string = os.environ['GH_USERNAME'] + ':' + os.environ['GH_TOKEN']
-        encoded = base64.b64encode(auth_string.encode('ascii'))
-        headers = {
-            "authorization" : 'Basic ' + encoded.decode('ascii'),
-            "Accept"        : "application/vnd.github+json",
-            }
-
-        # first fetch the git commit history
-        api_url = 'https://api.github.com/repos/{}/{}/commits'.format(user_name, repo)
-        r = requests.get(api_url, headers=headers)
-        requests_remaining = int(r.headers['X-RateLimit-Remaining'])
-        if requests_remaining == 0:
-            logging.error("no API requests remaining")
-            exit(1)
-
-        commits = r.json()
-
-        # now get the artifacts
-        api_url = 'https://api.github.com/repos/{}/{}/actions/artifacts'.format(user_name, repo)
-        r = requests.get(api_url, headers=headers)
-        data = r.json()
-
-        # check there are some artifacts
-        if 'artifacts' not in data:
-            logging.error("no artifact found for {}".format(self))
-            exit(1)
+    def get_macro_instance(self):
+        print(self)
+        if self.wokwi_id == 0:
+            return self.top_module
         else:
-            # only get artifacts called GDS
-            artifacts = [a for a in data['artifacts'] if a['name'] == 'GDS']
-            logging.debug("found {} artifacts".format(len(artifacts)))
+            return f"user_module_{self.wokwi_id}"
 
-        if len(artifacts) == 0:
-            logging.error("no artifacts for this project")
-            exit(1)
+    def get_scanchain_instance(self):
+        return f"scanchain_{self.index}"
 
-        download_url = Projects.get_most_recent_action_url(commits, artifacts)
-        logging.debug("download url {}".format(download_url))
+    def get_index(self):
+        return self.index
 
-        # need actions access on the token to get the artifact
-        # won't work on a pull request because they won't have the token
-        r = requests.get(download_url, headers=headers)
-        z = zipfile.ZipFile(io.BytesIO(r.content))
-        z.extractall(tmp_dir)
+    def get_wokwi_id(self):
+        return self.wokwi_id
 
-        # get the wokwi id
-        info_yaml = Projects.load_yaml(os.path.join(tmp_dir, 'src/info.yaml'))
-        wokwi_id = Projects.get_wokwi_id_from_yaml(info_yaml)
+    def get_macro_gds_name(self):
+        if self.wokwi_id == 0:
+            return f"{self.top_module}.gds"
+        else:
+            return f"user_module_{self.wokwi_id}.gds"
 
-        logging.info("wokwi id {} github url {}".format(wokwi_id, url))
+    def get_macro_lef_name(self):
+        if self.wokwi_id == 0:
+            return f"{self.top_module}.lef"
+        else:
+            return f"user_module_{self.wokwi_id}.lef"
 
-        # copy all important files to the correct places. Everything is dependent on the id
-        files = [
-            ("/tmp/tt/runs/wokwi/results/final/gds/user_module_{}.gds".format(wokwi_id), "gds/user_module_{}.gds".format(wokwi_id)),
-            ("/tmp/tt/runs/wokwi/results/final/lef/user_module_{}.lef".format(wokwi_id), "lef/user_module_{}.lef".format(wokwi_id)),
-            ("/tmp/tt/runs/wokwi/results/final/verilog/gl/user_module_{}.v".format(wokwi_id), "verilog/gl/user_module_{}.v".format(wokwi_id)),
-            ("/tmp/tt/src/user_module_{}.v".format(wokwi_id), "verilog/rtl/user_module_{}.v".format(wokwi_id)),
-            ]
+    def get_verilog_include(self):
+        if self.wokwi_id == 0:
+            # wrong
+            return f'`include "{self.top_module}.v"\n'
+        else:
+            return f'`include "user_module_{self.wokwi_id}.v"\n'
+
+    def get_gl_verilog_names(self):
+        if self.wokwi_id == 0:
+            return [f"{self.top_module}.v"]
+        else:
+            return [f'user_module_{self.index}.v']
+
+    def get_verilog_names(self):
+        if self.wokwi_id == 0:
+            files = []
+            for src in self.src_files:
+                files.append(src)
+            return files
+        else:
+            return [f'user_module_{self.wokwi_id}.v']
+
+    def get_giturl(self):
+        return self.git_url
+
+    # todo, do the source and GL files as well
+    def copy_files_to_caravel(self):
+        if self.wokwi_id == 0:
+            files = [
+                (f"projects/{self.index}/runs/wokwi/results/final/gds/{self.top_module}.gds", f"gds/{self.top_module}.gds"),
+                (f"projects/{self.index}/runs/wokwi/results/final/lef/{self.top_module}.lef", f"lef/{self.top_module}.lef"),
+                (f"projects/{self.index}/runs/wokwi/results/final/verilog/gl/{self.top_module}.v", f"verilog/gl/{self.top_module}.v"),
+                ]
+# Tholin has used * in src
+            for src in self.src_files:
+                print(src)
+                files.append((f"projects/{self.index}/src/{src}", f"verilog/rtl/{src}"))
+        else:
+            # copy all important files to the correct places. Everything is dependent on the id
+            files = [
+                (f"projects/{self.index}/runs/wokwi/results/final/gds/user_module_{self.wokwi_id}.gds", f"gds/user_module_{self.wokwi_id}.gds"),
+                (f"projects/{self.index}/runs/wokwi/results/final/lef/user_module_{self.wokwi_id}.lef", f"lef/user_module_{self.wokwi_id}.lef"),
+                (f"projects/{self.index}/runs/wokwi/results/final/verilog/gl/user_module_{self.wokwi_id}.v", f"verilog/gl/user_module_{self.wokwi_id}.v"),
+                (f"projects/{self.index}/src/user_module_{self.wokwi_id}.v", f"verilog/rtl/user_module_{self.wokwi_id}.v"),
+                ]
 
         logging.debug("copying files into position")
         for from_path, to_path in files:
@@ -243,13 +206,10 @@
             shutil.copyfile(from_path, to_path)
 
         # Uniquify the Verilog for this project
-        self.uniquify_project(wokwi_id, [
-            f"verilog/rtl/user_module_{wokwi_id}.v",
-        ])
+#        self.uniquify_project(wokwi_id, [
+#            f"verilog/rtl/user_module_{wokwi_id}.v",
+#        ])
 
-        # unlink temp directory
-        shutil.rmtree(tmp_dir)
-        return wokwi_id
 
     def uniquify_project(self, wokwi_id : str, rtl_files : List[str]) -> None:
         """
@@ -310,7 +270,6 @@
                 with open(path, "w", encoding="utf-8") as fh:
                     fh.writelines(new_txt)
 
-
 class CaravelConfig():
 
     def __init__(self, projects, num_projects):
@@ -338,7 +297,7 @@
         scanchain_w = 36
 
         num_macros_placed = 0
-
+        logging.info(self.num_projects)
         # macro.cfg: where macros are placed
         logging.info("creating macro.cfg")
         with open("openlane/user_project_wrapper/macro.cfg", 'w') as fh:
@@ -359,26 +318,26 @@
                     if num_macros_placed < self.num_projects:
                         if orientation == 'N':
                             # scanchain first
-                            macro_instance = self.projects.get_scanchain_instance(num_macros_placed)
+                            macro_instance = self.projects[num_macros_placed].get_scanchain_instance()
                             instance = "{} {:<4} {:<4} {}\n".format(
                                 macro_instance, start_x + col * step_x, start_y + row * step_y, orientation
                             )
                             fh.write(instance)
 
-                            macro_instance = self.projects.get_macro_instance(num_macros_placed)
+                            macro_instance = self.projects[num_macros_placed].get_macro_instance()
                             instance = "{} {:<4} {:<4} {}\n".format(
                                 macro_instance, start_x + scanchain_w + col * step_x, start_y + row * step_y, orientation
                             )
                             fh.write(instance)
                         else:
                             # macro first
-                            macro_instance = self.projects.get_macro_instance(num_macros_placed)
+                            macro_instance = self.projects[num_macros_placed].get_macro_instance()
                             instance = "{} {:<4} {:<4} {}\n".format(
                                 macro_instance, start_x + col * step_x, start_y + row * step_y, orientation
                             )
                             fh.write(instance)
 
-                            macro_instance = self.projects.get_scanchain_instance(num_macros_placed)
+                            macro_instance = self.projects[num_macros_placed].get_scanchain_instance()
                             instance = "{} {:<4} {:<4} {}\n".format(
                                 macro_instance, start_x + (step_x - scanchain_w) + col * step_x, start_y + row * step_y, orientation
                             )
@@ -386,7 +345,7 @@
 
                     num_macros_placed += 1
 
-        logging.info("total user macros placed: {}".format(num_macros_placed))
+        logging.info(f"total user macros placed: {num_macros_placed}")
 
         # macro_power.tcl: extra file for macro power hooks
         logging.info("creating macro_power.tcl")
@@ -398,10 +357,10 @@
             fh.write(", \\\n")
             for i in range(self.num_projects):
                 fh.write("	")
-                fh.write(self.projects.get_scanchain_instance(i))
+                fh.write(self.projects[i].get_scanchain_instance())
                 fh.write(" vccd1 vssd1 vccd1 vssd1, \\\n")
                 fh.write("	")
-                fh.write(self.projects.get_macro_instance(i))
+                fh.write(self.projects[i].get_macro_instance())
                 fh.write(" vccd1 vssd1 vccd1 vssd1")
                 if i != self.num_projects - 1:
                     fh.write(", \\\n")
@@ -412,8 +371,8 @@
         lefs = []
         gdss = []
         for i in range(self.num_projects):
-            lefs.append(self.projects.get_macro_lef_name(i))
-            gdss.append(self.projects.get_macro_gds_name(i))
+            lefs.append(self.projects[i].get_macro_lef_name())
+            gdss.append(self.projects[i].get_macro_gds_name())
 
         # can't have duplicates or OpenLane crashes at PDN
         lefs = CaravelConfig.unique(lefs)
@@ -492,10 +451,9 @@
             pfx      = f"sw_{idx:03d}"
             prev_pfx = f"sw_{idx-1:03d}" if idx > 0 else "sc"
             # Pickup the Wokwi design ID and github URL for the project
-            wk_id  = self.projects.get_wokwi_id(idx)
-            giturl = self.projects.get_giturl(idx)
-            logging.debug("instance %(idx)d user_module_%(wk_id)s", { "idx"  : idx,
-                                                                       "wk_id": wk_id })
+            index  = self.projects[idx].get_index()
+            giturl = self.projects[idx].get_giturl()
+
             # Append the instance to the body
             body += [
                 "",
@@ -503,7 +461,7 @@
                 f"wire {pfx}_clk_out, {pfx}_data_out, {pfx}_scan_out, {pfx}_latch_out;",
                 f"wire [7:0] {pfx}_module_data_in;",
                 f"wire [7:0] {pfx}_module_data_out;",
-                f"scanchain #(.NUM_IOS(8)) {self.projects.get_scanchain_instance(idx)} (",
+                f"scanchain #(.NUM_IOS(8)) {self.projects[idx].get_scanchain_instance()} (",
                 f"    .clk_in          ({prev_pfx}_clk_out),",
                 f"    .data_in         ({prev_pfx}_data_out),",
                 f"    .scan_select_in  ({prev_pfx}_scan_out),",
@@ -520,7 +478,7 @@
             # Append the user module to the body
             body += [
                 "",
-                f"user_module_{wk_id} {self.projects.get_macro_instance(idx)} (",
+                f"user_module_{index} {self.projects[idx].get_macro_instance()} (",
                 f"    .io_in  ({pfx}_module_data_in),",
                 f"    .io_out ({pfx}_module_data_out)",
                 ");"
@@ -551,7 +509,7 @@
         # build the user_project_includes.v file - used for blackboxing when building the GDS
         verilogs = []
         for i in range(self.num_projects):
-            verilogs.append(self.projects.get_verilog_include(i))
+            verilogs.append(self.projects[i].get_verilog_include())
         verilogs = CaravelConfig.unique(verilogs)
 
         with open('verilog/rtl/user_project_includes.v', 'w') as fh:
@@ -563,7 +521,7 @@
         # build complete list of filenames for sim
         verilog_files = []
         for i in range(self.num_projects):
-            verilog_files += self.projects.get_verilog_names(i)
+            verilog_files += self.projects[i].get_verilog_names()
         verilog_files = CaravelConfig.unique(verilog_files)
         with open('verilog/includes/includes.rtl.caravel_user_project', 'w') as fh:
             fh.write('-v $(USER_PROJECT_VERILOG)/rtl/user_project_wrapper.v\n')
@@ -576,7 +534,7 @@
         # build GL includes
         verilog_files = []
         for i in range(self.num_projects):
-            verilog_files += self.projects.get_gl_verilog_names(i)
+            verilog_files += self.projects[i].get_gl_verilog_names()
         verilog_files = CaravelConfig.unique(verilog_files)
         with open('verilog/includes/includes.gl.caravel_user_project', 'w') as fh:
             fh.write('-v $(USER_PROJECT_VERILOG)/gl/user_project_wrapper.v\n')
@@ -586,6 +544,8 @@
                 fh.write('-v $(USER_PROJECT_VERILOG)/gl/{}\n'.format(verilog))
 
     def build_docs(self):
+        pass
+        """
         logging.info("building doc index")
         with open("README_init.md") as fh:
             readme = fh.read()
@@ -593,21 +553,7 @@
             fh.write(readme)
             for wokwi_id, project_url in zip(self.projects.get_wokwi_ids(), self.projects.get_project_urls()):
                 fh.write("* [{}]({}) {}\n".format(wokwi_id, Projects.build_wokwi_url(wokwi_id), project_url))
-
-    # requires tinytapeout_scan repo to be installed - use --recursive when cloning this repo
-    # also needs a mod to sby, so probably ignore this unless you're Matt
-    def formal_scan(self):
-        cwd = os.getcwd()
-        gl_dir = 'verilog/gl/'
-        formal_dir = 'tinytapeout_scan'
-        for i in range(self.num_projects):
-            gl_filename = self.projects.get_gl_verilog_names(i)[0]
-            shutil.copyfile(os.path.join(gl_dir, gl_filename), os.path.join(formal_dir, gl_filename))
-            os.chdir(formal_dir)
-            commands = ['sby', '-f', 'tinytapeout_scan.sby', gl_filename.rstrip('.v')]
-            logging.info(commands)
-            subprocess.run(commands, check=True)
-            os.chdir(cwd)
+        """
 
     def list(self):
         count = 0
@@ -615,22 +561,18 @@
             logging.info("{:3} {:20} {}".format(count, wokwi_id, project_url))
             count += 1
 
-
 if __name__ == '__main__':
     parser = argparse.ArgumentParser(description="TinyTapeout")
 
     parser.add_argument('--list', help="list projects", action='store_const', const=True)
-    parser.add_argument('--update-projects', help='fetch the project data', action='store_const', const=True)
-    parser.add_argument('--update-single', help='only fetch a single repo for debug')
+    parser.add_argument('--clone-all', help="clone all projects", action="store_const", const=True)
+    parser.add_argument('--clone-single', help='only fetch a single repo for debug', type=int)
+    parser.add_argument('--fetch-gds', help='fetch gds', action='store_const', const=True)
     parser.add_argument('--update-caravel', help='configure caravel for build', action='store_const', const=True)
     parser.add_argument('--limit-num-projects', help='only configure for the first n projects', type=int, default=DEFAULT_NUM_PROJECTS)
     parser.add_argument('--test', help='use test projects', action='store_const', const=True)
     parser.add_argument('--debug', help="debug logging", action="store_const", dest="loglevel", const=logging.DEBUG, default=logging.INFO)
 
-    # stuff for help with applying patches to all designs and re-hardening
-    # parser.add_argument('--clone-all', help="clone all projects", action="store_const", const=True)
-    parser.add_argument('--formal', help="formal scan proof", action="store_const", const=True)
-    # parser.add_argument('--summary', help="summary", action="store_const", const=True)
 
     args = parser.parse_args()
 
@@ -650,13 +592,11 @@
     ch.setFormatter(log_format)
     log.addHandler(ch)
 
-    projects = Projects(update_cache=args.update_projects, test=args.test, update_single=args.update_single)
+    projects = Projects(args)
     projects.check_dupes()
 
-    caravel = CaravelConfig(projects, num_projects=args.limit_num_projects)
+    caravel = CaravelConfig(projects.projects, num_projects=args.limit_num_projects)
 
-    if args.formal:
-        caravel.formal_scan()
     if args.list:
         caravel.list()
 
diff --git a/git_utils.py b/git_utils.py
new file mode 100644
index 0000000..00c8c0d
--- /dev/null
+++ b/git_utils.py
@@ -0,0 +1,112 @@
+import base64
+from urllib.parse import urlparse
+import logging
+import requests
+import os
+import zipfile, io
+
+
+def fetch_file_from_git(git_url, path):
+    # get the basics
+    user_name, repo = split_git_url(git_url)
+
+    # authenticate for rate limiting
+    auth_string = os.environ['GH_USERNAME'] + ':' + os.environ['GH_TOKEN']
+    encoded = base64.b64encode(auth_string.encode('ascii'))
+    headers = {
+        "authorization" : 'Basic ' + encoded.decode('ascii'),
+        "Accept"        : "application/vnd.github+json",
+        }
+    encoded = base64.b64encode(auth_string.encode('ascii'))
+
+    api_url = 'https://api.github.com/repos/%s/%s/contents/%s' % (user_name, repo, path)
+
+    logging.debug(api_url)
+    r = requests.get(api_url, headers=headers)
+    requests_remaining = int(r.headers['X-RateLimit-Remaining'])
+    if requests_remaining == 0:
+        logging.error("no API requests remaining")
+        exit(1)
+
+    logging.debug("API requests remaining %d" % requests_remaining)
+
+    data = r.json()
+    if 'content' not in data:
+        return None
+
+    file_content = data['content']
+
+    file_content_encoding = data.get('encoding')
+    if file_content_encoding == 'base64':
+        file_content = base64.b64decode(file_content)
+
+    return file_content
+
+# the latest artifact isn't necessarily the one related to the latest commit, as github
+# could have taken longer to process an older commit than a newer one.
+# so iterate through commits and return the artifact that matches
+def get_most_recent_action_url(commits, artifacts):
+    release_sha_to_download_url = {artifact['workflow_run']['head_sha']: artifact['archive_download_url'] for artifact in artifacts}
+    for commit in commits:
+        if commit['sha'] in release_sha_to_download_url:
+            return release_sha_to_download_url[commit['sha']]
+
+def split_git_url(url):
+    res = urlparse(url)
+    try:
+        _, user_name, repo = res.path.split('/')
+    except ValueError:
+        logging.error(f"couldn't split repo from {url}")
+        exit(1)
+    repo = repo.replace('.git', '')
+    return user_name, repo
+
+# download the artifact for each project to get the gds & lef
+def install_artifacts(url, directory):
+    logging.debug(url)
+    user_name, repo = split_git_url(url)
+
+    # authenticate for rate limiting
+    auth_string = os.environ['GH_USERNAME'] + ':' + os.environ['GH_TOKEN']
+    encoded = base64.b64encode(auth_string.encode('ascii'))
+    headers = {
+        "authorization" : 'Basic ' + encoded.decode('ascii'),
+        "Accept"        : "application/vnd.github+json",
+        }
+
+    # first fetch the git commit history
+    api_url = f'https://api.github.com/repos/{user_name}/{repo}/commits'
+    r = requests.get(api_url, headers=headers)
+    requests_remaining = int(r.headers['X-RateLimit-Remaining'])
+    if requests_remaining == 0:
+        logging.error("no API requests remaining")
+        exit(1)
+
+    commits = r.json()
+
+    # now get the artifacts
+    api_url = f'https://api.github.com/repos/{user_name}/{repo}/actions/artifacts'
+    r = requests.get(api_url, headers=headers)
+    data = r.json()
+
+    # check there are some artifacts
+    if 'artifacts' not in data:
+        logging.error(f"no artifact found for {url}")
+        exit(1)
+    else:
+        # only get artifacts called GDS
+        artifacts = [a for a in data['artifacts'] if a['name'] == 'GDS']
+        logging.debug(f"found {len(artifacts)} artifacts")
+
+    if len(artifacts) == 0:
+        logging.error("no artifacts for this project")
+        exit(1)
+
+    download_url = get_most_recent_action_url(commits, artifacts)
+    logging.debug(f"download url {download_url}")
+
+    # need actions access on the token to get the artifact
+    # won't work on a pull request because they won't have the token
+    r = requests.get(download_url, headers=headers)
+    z = zipfile.ZipFile(io.BytesIO(r.content))
+    z.extractall(directory)
diff --git a/project_urls.py b/project_urls.py
index 03a0f11..5cc2e5f 100644
--- a/project_urls.py
+++ b/project_urls.py
@@ -1,156 +1,25 @@
-filler_project_url = 'https://github.com/mattvenn/wokwi_filler'
+filler_project_url = 'https://github.com/TinyTapeout/tt02-test-straight'
 project_urls = [
-	'https://github.com/mattvenn/tinytapeout_m_segments', # already patched
-	'https://github.com/gregdavill/tinytapeout_spin0',
-	'https://github.com/mole99/wokwi-1bit-alu',
-	'https://github.com/ericsmi/tinytapeout_popcnt.git',
-	'https://github.com/krasin/wokwi-guess-my-number',
-	'https://github.com/johshoff/barrelshifter-wokwi-gds',
-	'https://github.com/pretentious7/tinytapeout',
-	'https://github.com/GuzTech/wokwi-ripple-carry-adder',
-	'https://github.com/kbeckmann/tinytapeout_kbeckmann1',
-	'https://github.com/H-S-S-11/tinytapeout-verilog-test',
-	'https://github.com/skerr92/tinytapeout_frequency_div',
-	'https://github.com/argunda/tinytapeout_dualedgedetector',
-	'https://github.com/libokuohai/tinytapeout-2022-08',
-	'https://github.com/jglim/tinytapeout_bcd-dec',
-	'https://github.com/jglim/tinytapeout_bcd-7seg',
-	'https://github.com/tkuester/wokwi-directghost',
-	'https://github.com/shahzaibk23/tinytapeout-barrel-shifter',
-	'https://github.com/tcptomato/tinytapeout',
-	'https://github.com/DaveyPocket/chaser',
-	'https://github.com/GuzTech/tinytapeout-4x4-multiplier',
-	'https://github.com/derhexenmeister/tinytapeout_nco',
-	'https://github.com/mbalestrini/tinytapeout_rgb_lut_test',
-	'https://github.com/derhexenmeister/tinytapeout_updwnbcd',
-	'https://github.com/bradysalz/pll_tiny_tapeout_demo',
-	'https://github.com/pramitpal/tinytapeout_pramit',
-	'https://github.com/gregdavill/tinytapeout-verilog-fifo',
-	'https://github.com/gregdavill/tinytapeout-wokwi-74x1G00',
-	'https://github.com/gregdavill/tinytapeout-wokwi-74x1G02',
-	'https://github.com/gregdavill/tinytapeout-wokwi-74xG198',
-	'https://github.com/gregdavill/tinytapeout-verilog-7seg-clock',
-	'https://github.com/alanvgreen/tinytapeout4bitadder',
-	'https://github.com/benlaurie/twistedringcounter',
-	'https://github.com/sureshsugumar/tinytapeout_counter',
-	'https://github.com/daniestevez/tinytapeout-verilog', 
-	'https://github.com/pkuligowski/tinytapeout_tmr',
-	'https://github.com/chiplet/tinytapeout-snake',
-	'https://github.com/derhexenmeister/tinytapeout_pwm',
-	'https://github.com/raha96/tinycharacters-locked',
-	'https://github.com/nathancheek/tinytapeout-loop',
-	'https://github.com/andars/universal-turing-machine-w5s8', # dup id
-	'https://github.com/vmunoz82/tinytapeout_euler1',
-	'https://github.com/mikenet213/mikenet213-tt1-verilog',
-	'https://github.com/veremenko-y/tinytapeout-ue14500',
-	'https://github.com/mikenet213/mikenet213-tt2-verilog',
-	'https://github.com/aiunderstand/tinytapeout_asyncbinterconvcomp.git',
-	'https://github.com/smunaut/tinytapeout-fifo', # already patched
-	'https://github.com/nwtechguy/tinytapeout_BCD_counter',
-	'https://github.com/kambadur/bcd_to_7seg',
-	'https://github.com/bieganski/tinytapeout_bieganski',
-	'https://github.com/TomKeddie/tinytapeout-2022-1',
-	'https://github.com/r-a-hoggarth/tinytapeGaloisLFSR',
-	'https://github.com/adamgreig/tinytapeout-prn',
-	'https://github.com/ianloic/tinytapeout-1',
-	'https://github.com/sad-electronics/tinytapeout-clock-divider-asic',
-	'https://github.com/gatecat/tinytapeout-lutram-test',
-	'https://github.com/tommythorn/tinytapeout-4-bit-cpu',
-	'https://github.com/wokwi/tt-game-of-life-cell-popcnt',
-	'https://github.com/gatecat/tinytapeout-srlut-test',
-	'https://github.com/AdDraw/tinytapeout_demo',
-	'https://github.com/cpldcpu/tinydice',
-	'https://github.com/cpldcpu/tinytapeout_mcpu6bit',
-	'https://github.com/azzeloof/tinytapeout-counter',
-	#'https://github.com/georgerennie/tinytapeout-verilog-async-arb', # can't build - replace
-    'https://github.com/mattvenn/tinytapeout-341802655228625490', 
-	'https://github.com/mwelling/led-blaster',
-	'https://github.com/mwelling/figure-8',
-	'https://github.com/gatecat/tinytapeout-fpga-test', # already patched
-	'https://github.com/cfib/trafficlight-fsm',
-	'https://github.com/clj/tinytapeout-verilog-7seg-figure-eight',
-	'https://github.com/smunaut/tinytapeout-misc-1', # already patched
-	'https://github.com/regymm/tinytapeout-funnyblinky', # fails openlane doesn't fit, has messed with config.tcl
-	'https://github.com/Sirawit7205/tinytapeout-2G57-2G58',
-	'https://github.com/Sirawit7205/tinytapeout-2G97-2G98',
-	'https://github.com/hosein-mokarian/tinytapeout_counter_3to8_decoder',
-	'https://github.com/burtyb/srld',
-	'https://github.com/Mahnoor-ismail01/tinytapeout-chromatic-3-to-8-Decoder',
-	'https://github.com/Shahzaib2028/tinytapeout-4to2Encoder-2to4Decoder',
-	'https://github.com/sfmth/tinytapeout-tinycordic',
-	'https://github.com/mm21/tinytapeout-led-matrix',
-	'https://github.com/jeanthom/tinytapout-lock',
-	'https://github.com/AidanMedcalf/tinytapeout-tinyio',
-	'https://github.com/ElectricPotato/tinytapeout-hello-world-uart',
-	'https://github.com/abdullahkhalids/TinyTapeout-hamming-code',
-	'https://github.com/hossein1387/tinytapeout-verilog-test',
-	'https://github.com/ChrisPVille/tinytapeout-FROG4bitCPU', # fails patch, easy fix
-	'https://github.com/Talha-Ahmed-1/tinytapeout_flop_regfile',
-	'https://github.com/skylersaleh/tinytapeout-hello',
-	'https://github.com/proppy/tinytapeout-xls-popcount',
-	'https://github.com/proppy/tinytapeout-xls-popcount-bithacks', 
-	'https://github.com/proppy/tinytapeout-xls-inverter', 
-	'https://github.com/mark64/tinytapeout',
-	'https://github.com/dave-roo/ddcomparatorandro',
-	'https://github.com/splinedrive/tinytapeout-verilog-4x4-multiplier',
-	'https://github.com/ThorKn/tinytapeout_shiftregister_8bit',
-	'https://github.com/UDXS/tinytapeout-sqrt',
-	'https://github.com/coralmw/tinytapeout-css-feedback', 
-	'https://github.com/ericsmi/tinytapeout-verilog-div3', # fails openlane hangs forever, works with pdk7 and updated area
-	'https://github.com/fluxfocus/jdtt-logic1.git',
-	'https://github.com/anm/nyasic',
-	'https://github.com/aiunderstand/tinytapeout_bintristateloadablecounter',
-	'https://github.com/ThorKn/tinytapeout_shiftregister_challenge',
-	'https://github.com/regymm/tinytapeout-mcpi', # openlane fail density error, has messed with config.tcl
-	'https://github.com/todd1251/tinytapeout-figure8',
-	'https://github.com/CyberGai/tinytapeout-bcd-counter',
-	'https://github.com/georgeyhere/tinytapeout-dice-roller',
-	'https://github.com/nayanesh-reddy/2-Bit_Add_Mul_Comp',
-	'https://github.com/ryancor/half_addr_asic',
-	'https://github.com/hovind/tinytapeout-verilog-test',
-	'https://github.com/siriusm46/tinytapeout_bcd_decimal',
-	'https://github.com/cpldcpu/tinytapeout_mcpu5',
-	'https://github.com/goran-mahovlic/tinytapeout-verilog-piano',
-	'https://github.com/andars/universal-turing-machine-aw7s8',
-	'https://github.com/marcusmueller/hamming74-tapeout',
-	'https://github.com/13arn/tinytapeout_counter_steamdeck',
-	'https://github.com/johshoff/tinytapeout-verilog',
-	'https://github.com/cy384/seven-segment-with-adder',
-	'https://github.com/georgerennie/tinytapeout-wokwi-cd4518',
-	'https://github.com/ElectricPotato/tinytapeout-picture-printer-b',
-	'https://github.com/theFestest/tinytapeout-simple-invert8',
-	'https://github.com/ArsenioDev/CustomSiliconTest',
-	'https://github.com/mgargano/tinytapeout_alu_with_4bit_7segmetdisplay_decoder',
-	'https://github.com/theFestest/tinytapeout-4x4-ram',
-	'https://github.com/michael-christen/wokwi-verilog-asic-experiment',
-	'https://github.com/craigcc-frii/tinytapeout_craig',
-	'https://github.com/youngpines/r2rdac_tinytapeout_demo',
-	'https://github.com/toybuilder/learn-tinytapeout',
-	'https://github.com/eggsactly/tinytapeout_demo',
-	'https://github.com/gsegura96/tinytapeout-chisel', 
-	'https://github.com/abf149/fbna_like_verilog_abf149',
-	'https://github.com/MC-SecPat/tinytapeout_chi2shares',
-	'https://github.com/MC-SecPat/tinytapeout_chi3shares',
-	'https://github.com/Adil8442/tiny_tapeout_test',
-	#'https://github.com/MC-SecPat/tinytapeout_chiDOM', # remove, - empty project
-    'https://github.com/mattvenn/tinytapeout-341802448429515346',  # filler
-	'https://github.com/r4d10n/tinytapeout-HELLo-3orLd-7seg',
-	'https://github.com/proppy/tinytapeout-xls-graydec',
-	'https://github.com/prabaldutta/tinytapeout_adi',
-	'https://github.com/maehw/wokwi-verilog-gds-wolf-goat-cabbage',
-	'https://github.com/ThorKn/tinytapeout_pattern_player',
-	'https://github.com/rigobertoruiz98/cts_fsm',
-	'https://github.com/rajarshiroy/tinytapout0_rajarshi',
-	'https://github.com/BarsMonster/MicroASIC',
-#	'https://github.com/kammoh/tinytapeout-chisel', # wrong id, hasn't merged PR
-	'https://github.com/cpldcpu/TinyTapeout_TrainLED',
-	'https://github.com/malkam03/tinytapeout-game-of-life',
-	'https://github.com/BarsMonster/MicroAsicV',
-	'https://github.com/maehw/wokwi-verilog-gds-lowspeed-tiny-uart',
-	'https://github.com/smunaut/tinytapeout-smolram', # already patched
-	'https://github.com/sirejdua/6bit-cellular-automata-tinytapeout',
-	'https://github.com/DuaneSand/TinyTapeout-Hello',
-	'https://github.com/tzachari/tinytapeout-lab11',
-	'https://github.com/mattvenn/tinytapeout-marc',
-    'https://github.com/mattvenn/tinytapeout-laura', # already patched
+	"https://github.com/maehw/tt02-bcd-7segment-encoder",
+	"https://github.com/ekliptik/tt02-chase-the-beat",
+	"https://github.com/leardilap/tt02-LUTRAM",
+	"https://github.com/steieio/tt02-submission-universal-sr",
+	"https://github.com/Tschucker/tt02-submission-tiny-fir",
+	"https://github.com/moyesw/tt02-moyesw-StreamIntegrator",
+	"https://github.com/RiceShelley/tiny-fft",
+	#"https://github.com/89Mods/tt2-AvalonSemi-5401",
+    'https://github.com/TinyTapeout/tt02-test-straight',
+	"https://github.com/svd321/tt02-Ising",
+	"https://github.com/JensIMS/tt02-trafficlight",
+	"https://github.com/jar/tt02_sram",
+	"https://github.com/justinP-wrk/tt02-TinySensor",
+	"https://github.com/azdle/binary-clock-asic",
+	"https://github.com/AidanGood/tt02-McCoy",
+	"https://github.com/ryancor/tt02-submission-template",
+	"https://github.com/grayresearch/tt02-s4ga",
+	"https://github.com/migcorre/tt02-dc",
+	"https://github.com/loxodes/tt02-submission-loxodes",
+	"https://github.com/chrisruk/matrixchip",
+	"https://github.com/TomKeddie/tinytapeout-2022-2",
+	"https://github.com/Fraserbc/tt02-simon",
     ]