Merge pull request #14 from donn/main

Create check-drcs-for-cell-gds-using-magic
diff --git a/.github/workflows/build-docker-image-run-drc-for-cell-gds-using-magic.yml b/.github/workflows/build-docker-image-run-drc-for-cell-gds-using-magic.yml
new file mode 100644
index 0000000..c1d7b9c
--- /dev/null
+++ b/.github/workflows/build-docker-image-run-drc-for-cell-gds-using-magic.yml
@@ -0,0 +1,52 @@
+# Copyright 2021 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
+
+name: Build Docker Image for Run DRC for cell GDS (using Magic) Action
+
+on:
+  workflow_dispatch:
+  push:
+
+jobs:
+  prebuild-magic-gds-drc:
+    runs-on: ubuntu-latest
+    steps:
+    - name: Checkout code
+      uses: actions/checkout@v2
+
+    - name: Set up Docker Buildx
+      uses: docker/setup-buildx-action@v1
+
+    - name: Set Action Name
+      run: echo "ACTION_NAME=run-drc-for-cell-gds-using-magic" >> $GITHUB_ENV
+
+    - name: Login to GitHub Container Registry
+      uses: docker/login-action@v1
+      with:
+        registry: ghcr.io
+        username: ${{ github.repository_owner }}
+        password: ${{ github.token }}
+
+    - name: Build and push
+      uses: docker/build-push-action@v2
+      with:
+        context: ${{ env.ACTION_NAME }}
+        file: ${{ env.ACTION_NAME }}/Dockerfile
+        push: true
+        tags: ghcr.io/${{ github.repository }}-${{ env.ACTION_NAME }}:latest
+
+    - name: Image digest
+      run: echo ${{ steps.docker_build.outputs.digest }}
diff --git a/.gitignore b/.gitignore
index a81c8ee..b530fea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -136,3 +136,6 @@
 
 # Cython debug symbols
 cython_debug/
+
+# IDEs
+.vscode/
diff --git a/AUTHORS b/AUTHORS
index ea70201..de14df6 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -7,7 +7,7 @@
 
 # Companies
 Google LLC
-efabless corporation
+Efabless Corporation
 The American University in Cairo
 
 # Individuals
diff --git a/environment.yml b/environment.yml
index 2a7b5da..b740766 100644
--- a/environment.yml
+++ b/environment.yml
@@ -23,3 +23,4 @@
 # Packages installed from PyPI
 - pip:
   - -r file:requirements.txt
+  - -r file:run-drc-for-cell-gds-using-magic/requirements.txt
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..4735410
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,2 @@
+[pytest]
+addopts = --doctest-modules --ignore-glob=env --ignore-glob=third_party
diff --git a/requirements.txt b/requirements.txt
index 58d13b0..5dc7f8b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,6 @@
+# Python code testing + linting
 flake8
+pytest
 
 # rst_include tool as GitHub doesn't support `.. include::` when rendering
 # previews.
diff --git a/run-drc-for-cell-gds-using-magic/Dockerfile b/run-drc-for-cell-gds-using-magic/Dockerfile
new file mode 100644
index 0000000..ab321e8
--- /dev/null
+++ b/run-drc-for-cell-gds-using-magic/Dockerfile
@@ -0,0 +1,70 @@
+# Copyright 2021 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
+
+FROM ubuntu:20.04
+
+ENV DEBIAN_FRONTEND noninteractive
+
+RUN apt-get update
+RUN apt-get install -y build-essential git curl python3 python3-pip tcl-dev tk-dev csh libcairo2-dev
+
+WORKDIR /build
+
+# Setup Magic
+ENV MAGIC_TAG 8.3.160
+RUN \
+    curl -L https://github.com/RTimothyEdwards/magic/archive/refs/tags/${MAGIC_TAG}.tar.gz -o /build/magic.tar.gz \
+    && sha256sum /build/magic.tar.gz \
+    && mkdir /build/magic \
+    && tar -xzC /build/magic --strip-components=1 -f /build/magic.tar.gz \
+    && cd /build/magic \
+    && ./configure \
+    && make -j$(nproc) \
+    && make install \
+    && rm -rf /build/magic*
+
+# Setup Python Dependencies
+WORKDIR /build
+COPY ./requirements.txt /build/requirements.txt
+RUN \
+	cat /build/requirements.txt \
+	&& python3 -m pip install -r /build/requirements.txt --progress-bar off \
+	&& rm -rf ~/.cache/pip
+
+# OpenPDKs
+## Tag must exist on both https://github.com/efabless/open_pdk_techfiles
+## and https://github.com/RTimothyEdwards/open_pdks
+ENV OPEN_PDKS_TAG 1.0.159
+
+## Download run_standard_drc
+RUN \
+    curl -L https://raw.githubusercontent.com/RTimothyEdwards/open_pdks/${OPEN_PDKS_TAG}/sky130/custom/scripts/run_standard_drc.py -o /usr/bin/run_standard_drc.py \
+    && sha256sum /usr/bin/run_standard_drc.py
+
+## Download Precompiled OpenPDKs Magic Tech Files
+ENV PDK_ROOT /share/pdk/sky130A
+RUN \
+    curl -L https://github.com/efabless/open_pdk_techfiles/releases/download/${OPEN_PDKS_TAG}/sky130A_tech_magic.tar.xz -o /build/sky130A_tech_magic.tar.xz \
+    && sha256sum /build/sky130A_tech_magic.tar.xz \
+    && mkdir -p ${PDK_ROOT}/libs.tech/magic \
+    && tar -xC ${PDK_ROOT}/libs.tech/magic -f /build/sky130A_tech_magic.tar.xz \
+    && rm -rf /build/*
+
+# Copy Entry Point
+COPY ./run_all_drc.py /usr/bin/run_all_drc.py
+RUN chmod +x /usr/bin/run_all_drc.py
+
+ENTRYPOINT ["/usr/bin/run_all_drc.py"]
diff --git a/run-drc-for-cell-gds-using-magic/Makefile b/run-drc-for-cell-gds-using-magic/Makefile
new file mode 100644
index 0000000..fb408e8
--- /dev/null
+++ b/run-drc-for-cell-gds-using-magic/Makefile
@@ -0,0 +1,24 @@
+# Copyright 2021 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
+
+TOP_DIR := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))/..)
+
+README.rst: README.src.rst $(TOP_DIR)/docs/*.rst $(TOP_DIR)/Makefile
+	make -C $(TOP_DIR) run-drc-for-cell-gds-using-magic/README.rst
+
+# Redirect everything to the top directory by default.
+%:
+	make -C $(TOP_DIR) $@
diff --git a/run-drc-for-cell-gds-using-magic/README.rst b/run-drc-for-cell-gds-using-magic/README.rst
new file mode 100644
index 0000000..dd4d8e9
--- /dev/null
+++ b/run-drc-for-cell-gds-using-magic/README.rst
@@ -0,0 +1,145 @@
+``skywater-pdk-actions`` - ``run-drc-for-cell-gds-using-magic``
+=============================================================
+
+This GitHub action runs Design Rule Checks on all GDS files inside the /cells
+directory.
+
+Usage
+=====
+
+Add this to any push, PR or manual dispatch workflow:
+
+.. code:: yml
+
+       steps:
+       - uses: actions/checkout@v2
+
+       - name: Run Magic DRC
+         uses: docker://ghcr.io/google/skywater-pdk-actions-run-drc-for-cell-gds-using-magic:latest
+         with:
+           args: --acceptable-errors-file /dev/null --match-directories . --known-bad ''
+
+Check the Python file for more documentation on arguments.
+
+How to Contribute
+=================
+
+We'd love to accept your patches and contributions to this project.
+There are just a few small guidelines you need to follow.
+
+Contributor License Agreement
+-----------------------------
+
+Contributions to this project must be accompanied by a Contributor
+License Agreement. You (or your employer) retain the copyright to your
+contribution; this simply gives us permission to use and redistribute
+your contributions as part of the project. Head over to
+https://cla.developers.google.com/ to see your current agreements on
+file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already
+submitted one (even if it was for a different project), you probably
+don't need to do it again.
+
+Code reviews
+------------
+
+All submissions, including submissions by project members, require
+review. We use GitHub pull requests for this purpose. Consult `GitHub
+Help <https://help.github.com/articles/about-pull-requests/>`__ for more
+information on using pull requests.
+
+Community Guidelines
+--------------------
+
+This project follows `Google's Open Source Community
+Guidelines <https://opensource.google/conduct/>`__.
+
+At Google, we recognize and celebrate the creativity and collaboration
+of open source contributors and the diversity of skills, experiences,
+cultures, and opinions they bring to the projects and communities they
+participate in.
+
+Every one of Google's open source projects and communities are inclusive
+environments, based on treating all individuals respectfully, regardless
+of gender identity and expression, sexual orientation, disabilities,
+neurodiversity, physical appearance, body size, ethnicity, nationality,
+race, age, religion, or similar personal characteristic.
+
+We value diverse opinions, but we value respectful behavior more.
+
+Respectful behavior includes:
+
+-  Being considerate, kind, constructive, and helpful.
+-  Not engaging in demeaning, discriminatory, harassing, hateful,
+   sexualized, or physically threatening behavior, speech, and imagery.
+-  Not engaging in unwanted physical contact.
+
+Some Google open source projects
+`may adopt <https://opensource.google/docs/releasing/preparing/#conduct>`__
+an explicit project code of conduct, which may have additional detailed
+expectations for participants. Most of those projects will use our
+`modified Contributor Covenant <https://opensource.google/docs/releasing/template/CODE_OF_CONDUCT/>`__.
+
+Resolve peacefully
+~~~~~~~~~~~~~~~~~~
+
+We do not believe that all conflict is necessarily bad; healthy debate
+and disagreement often yields positive results. However, it is never
+okay to be disrespectful.
+
+If you see someone behaving disrespectfully, you are encouraged to
+address the behavior directly with those involved. Many issues can be
+resolved quickly and easily, and this gives people more control over the
+outcome of their dispute. If you are unable to resolve the matter for
+any reason, or if the behavior is threatening or harassing, report it.
+We are dedicated to providing an environment where participants feel
+welcome and safe.
+
+Reporting problems
+~~~~~~~~~~~~~~~~~~
+
+Some Google open source projects may adopt a project-specific code of
+conduct. In those cases, a Google employee will be identified as the
+Project Steward, who will receive and handle reports of code of conduct
+violations. In the event that a project hasn’t identified a Project
+Steward, you can report problems by emailing opensource@google.com.
+
+We will investigate every complaint, but you may not receive a direct
+response. We will use our discretion in determining when and how to
+follow up on reported incidents, which may range from not taking action
+to permanent expulsion from the project and project-sponsored spaces. We
+will notify the accused of the report and provide them an opportunity to
+discuss it before any action is taken. The identity of the reporter will
+be omitted from the details of the report supplied to the accused. In
+potentially harmful situations, such as ongoing harassment or threats to
+anyone's safety, we may take action without notice.
+
+*This document was adapted from the*
+`IndieWeb Code of Conduct <https://indieweb.org/code-of-conduct>`_
+*and can also be found at* <https://opensource.google/conduct/>.
+
+License
+=======
+
+The SkyWater Open Source PDK GitHub actions are released under the
+`Apache 2.0 license <https://github.com/google/skywater-pdk/blob/master/LICENSE>`_.
+
+The copyright details (which should also be found at the top of every file) are;
+
+::
+
+   Copyright 2021 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
+
+       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.
+
diff --git a/run-drc-for-cell-gds-using-magic/README.src.rst b/run-drc-for-cell-gds-using-magic/README.src.rst
new file mode 100644
index 0000000..50057f9
--- /dev/null
+++ b/run-drc-for-cell-gds-using-magic/README.src.rst
@@ -0,0 +1,26 @@
+``skywater-pdk-actions`` - ``run-drc-for-cell-gds-using-magic``
+=============================================================
+
+This GitHub action runs Design Rule Checks on all GDS files inside the /cells
+directory.
+
+Usage
+=====
+
+Add this to any push, PR or manual dispatch workflow:
+
+.. code:: yml
+
+       steps:
+       - uses: actions/checkout@v2
+
+       - name: Run Magic DRC
+         uses: docker://ghcr.io/google/skywater-pdk-actions-run-drc-for-cell-gds-using-magic:latest
+         with:
+           args: --acceptable-errors-file /dev/null --match-directories . --known-bad ''
+
+Check the Python file for more documentation on arguments.
+
+.. include:: ../docs/contributing.rst
+
+.. include:: ../docs/license.rst
diff --git a/run-drc-for-cell-gds-using-magic/requirements.txt b/run-drc-for-cell-gds-using-magic/requirements.txt
new file mode 100644
index 0000000..b98f660
--- /dev/null
+++ b/run-drc-for-cell-gds-using-magic/requirements.txt
@@ -0,0 +1 @@
+click
\ No newline at end of file
diff --git a/run-drc-for-cell-gds-using-magic/run_all_drc.py b/run-drc-for-cell-gds-using-magic/run_all_drc.py
new file mode 100644
index 0000000..5adcbcd
--- /dev/null
+++ b/run-drc-for-cell-gds-using-magic/run_all_drc.py
@@ -0,0 +1,231 @@
+#!/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
+
+"""
+run_all_drc.py --- A script that will run run_standard_drc for all .gds files
+under the cells/ folder.
+
+Must be run from repository root.
+
+Usage: python3 run_all_drc.py --help
+
+Results:
+
+  Prints a report to standard output.
+"""
+
+import os
+import re
+import subprocess
+import traceback
+
+from concurrent import futures
+from typing import List, Tuple
+
+import click
+
+acceptable_errors = []
+
+SCRIPT_DIR = os.path.realpath(os.path.dirname(__file__))
+STANDARD_DRC_SCRIPT = os.path.join(SCRIPT_DIR, "run_standard_drc.py")
+PDK_SUBSET = os.getenv("PDK_ROOT") or os.path.join(SCRIPT_DIR, "sky130A")
+
+DRCError = Tuple[str, List[str]]
+
+
+PARSE_DRC_REPORT_EXAMPLE = """
+This first set of lines is the 'header':
+DRC errors for a cell that doesn't exist
+It's skipped over by this function.
+--------------------------------------------
+
+This is an acceptable error.
+These lines are details for the acceptable error.
+There are usually a couple of lines.
+
+This is an unacceptable error.
+These lines are details for the unacceptable error.
+There are usually a couple of lines.
+
+This is another unacceptable error.
+It has less lines of detail.
+"""
+
+
+def parse_drc_report(
+        report: str, acceptable_errors: List[str]) -> List[DRCError]:
+    """
+    Takes a magic report in the format as seen in PARSE_DRC_REPORT_EXAMPLE
+    above, and returns all errors as a list of tuples, where the first element
+    of the tuple is the name of the error and the other lines are the details.
+
+    >>> from pprint import pprint as p
+    >>> p(parse_drc_report(
+    ...     PARSE_DRC_REPORT_EXAMPLE.strip(),
+    ...     ["This is an acceptable error."]))
+    [('This is an unacceptable error.',
+      ['These lines are details for the unacceptable error.',
+       'There are usually a couple of lines.']),
+     ('This is another unacceptable error.', ['It has less lines of detail.'])]
+    """
+    components = [x.split("\n") for x in report.split("\n\n")]
+    errors = []
+
+    header = components.pop(0)  # noqa: F841
+
+    for error in components:
+        error_name = error[0]
+        if error_name in acceptable_errors:
+            continue
+        errors.append((error[0], error[1:]))
+
+    return errors
+
+
+def drc_gds(path: str) -> Tuple[str, List[DRCError]]:
+    """
+    Takes a GDS path. Returns the name of the cell and returns a list of
+    DRC errors.
+    """
+    cell_name = os.path.basename(path)[:-4]
+
+    env = os.environ.copy()
+    env["PDKPATH"] = PDK_SUBSET
+
+    res = subprocess.run([
+        "python3",
+        STANDARD_DRC_SCRIPT,
+        path
+    ], env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+    report_path = path[:-4] + "_drc.txt"
+    try:
+        report = open(report_path).read()
+
+        if os.getenv("ACTIONS_STEP_DEBUG") or False:
+            print("::group::%s" % report_path)
+            print(report)
+            print("::endgroup::")
+
+        return cell_name, parse_drc_report(report, acceptable_errors)
+    except FileNotFoundError:
+        return cell_name, [
+            (
+                "Magic did not produce a report.",
+                [res.stdout.decode("utf8"), res.stderr.decode("utf8")]
+            )
+        ]
+
+
+@click.command()
+@click.option(
+    "-a",
+    "--acceptable-errors-file",
+    default="/dev/null",
+    help="A file containing a list of newline-delimited acceptable DRC errors."
+         " Default: No file will be read and all errors deemed unacceptable."
+)
+@click.option(
+    "-m",
+    "--match-directories",
+    default=".",
+    help="A regex that will match subdirectories under cells/."
+         " Default: . (matches everything.)"
+)
+@click.option(
+    "-b",
+    "--known-bad",
+    default="",
+    help="A comma,delimited list of cells that are known bad and"
+         " thus do not cause a non-zero exit upon failure."
+         " Default: empty string (None of them.)"
+)
+def run_all_drc(acceptable_errors_file, match_directories, known_bad):
+    print("Testing cells in directories matching /%s/…" % match_directories)
+
+    global acceptable_errors
+    acceptable_errors_str = open(acceptable_errors_file).read()
+    acceptable_errors = acceptable_errors_str.split("\n")
+
+    known_bad_list = known_bad.split(",")
+
+    nproc = os.cpu_count()
+    with futures.ThreadPoolExecutor(max_workers=nproc) as executor:
+        future_list = []
+
+        cells_dir = "./cells"
+        cells = os.listdir(cells_dir)
+
+        for cell in cells:
+            if not re.match(match_directories, cell):
+                print("Skipping directory %s…" % cell)
+                continue
+
+            cell_dir = os.path.join(cells_dir, cell)
+
+            gds_list = list(
+                filter(lambda x: x.endswith(".gds"), os.listdir(cell_dir))
+            )
+
+            for gds_name in gds_list:
+                gds_path = os.path.join(cell_dir, gds_name)
+
+                future_list.append(executor.submit(drc_gds, gds_path))
+
+        successes = 0
+        total = 0
+        exit_code = 0
+        for future in future_list:
+            total += 1
+            cell_name, errors = future.result()
+
+            symbol = "❌"
+            message = "ERROR"
+            if len(errors) == 0:
+                successes += 1
+                # This tick is rendered black on all major platforms except for
+                # Microsoft.
+                symbol = "✔\ufe0f"
+                message = "CLEAN"
+            print("%-64s %s %s" % (cell_name, symbol, message))
+
+            if len(errors) != 0:
+                if cell_name not in known_bad_list:
+                    exit_code = 65
+                for error in errors:
+                    print("* %s" % error[0])
+                    for line in error[1]:
+                        print("  %s" % line)
+
+        success_rate = (successes / total * 100)
+        print("%i/%i successes (%0.1f%%)" % (successes, total, success_rate))
+
+        exit(exit_code)
+
+
+def main():
+    try:
+        run_all_drc()
+    except Exception:
+        print("An unhandled exception has occurred.", traceback.format_exc())
+        exit(69)
+
+
+if __name__ == '__main__':
+    main()