Repository: ChrisCummins/ProGraML (language: python)

Issue:
Inst2Vec Node Embedding DevMap
## ❓ Questions and Help

I tried to embed the IR Statements from the Devmap (amd) dataset using the following command:
`bazel-bin/bin/inst2vec --directory /Users/<User>/Projects/programlmodel/devicemap/graphs_amd`
As taken from the paper ProGraML: For the Case Study "Device Mapping", the pre-trained inst2vec embeddings were used. 

The IR seems to be correctly preprocessed and the embedding index was successfully added to the graphs. However, from 490103 IR Statements, only three distinct indices were generated and attached to the node features: _8564, 8565, 8566_  (!UNK, !IDENTIFIER, !IMMEDIATE).

Am I doing something wrong here or is this a bug?

Thanks in advance and regards 



Discussion / hints:

Fix patch:
diff --git a/.bazelrc b/.bazelrc
index a9347765b..695ca8166 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -1,29 +1,21 @@
 # See: https://bazel.build/versions/master/docs/bazel-user-manual.html#flag--compilation_mode
 
-# This flag is set by the bazel IntelliJ plugin. Set it here so that alternating
-# between command line and IntelliJ invocations does not reset the analysis cache.
-build --runs_per_test=1
-test --runs_per_test=1
-
-# This project uses C++14.
+# This project requires c++14.
+#
+# NOTE(cummins): This flag has to be pushed out to the copts argument of a
+# handful of cxx targets, grep for them when changing the standard.
+#
+# NOTE(cummins): LLVM 3.8 is not compatible with C++17 or later.
 build --cxxopt='-std=c++14'
 test --cxxopt='-std=c++14'
 
-# Promote unused variable warnings to errors.
-build --cxxopt='-Werror=unused-variable'
-test --cxxopt='-Werror=unused-variable'
-
-# Promote unused result warnings to errors.
-build --cxxopt='-Werror=unused-result'
-test --cxxopt='-Werror=unused-result'
-
-# Promote warnings about missing braces on if/else blocks to errors.
-build --cxxopt='-Werror=dangling-else'
-test --cxxopt='-Werror=dangling-else'
+# Workaround for broken grpc build on macOS.
+# See: https://github.com/bazelbuild/bazel/issues/4341
+build --copt -DGRPC_BAZEL_BUILD
+test --copt -DGRPC_BAZEL_BUILD
 
-# Default to continuing on build error. Override using --keep_going=false.
-build --keep_going
-test --keep_going
+# Show test error output. Override using --test_output={summary,full}.
+test --test_output=errors
 
 # Show test error output. Override using --test_output={summary,full}.
 test --test_output=errors
diff --git a/.bazelversion b/.bazelversion
index 47b322c97..fcdb2e109 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1,1 +1,1 @@
-3.4.1
+4.0.0
diff --git a/.bettercodehub.yml b/.bettercodehub.yml
index c35cbd8e0..e8d9e57f7 100644
--- a/.bettercodehub.yml
+++ b/.bettercodehub.yml
@@ -2,7 +2,10 @@
 # Configuration for Better Code analysis.
 # See https://bettercodehub.com/docs/configuration-manual
 exclude:
+    - /benchmarks/.*
     - /bin/.*
+    - /models/.*
+    - /programl/third_party/.*
     - /tasks/.*
     - /tests/.*
     - /third_party/.*
diff --git a/.clang-format b/.clang-format
index cc9b1ec27..f533d6850 100644
--- a/.clang-format
+++ b/.clang-format
@@ -1,5 +1,5 @@
 ---
-Language:        Cpp
+Language: Cpp
 # BasedOnStyle:  Google
 AccessModifierOffset: -1
 AlignAfterOpenBracket: Align
@@ -7,7 +7,7 @@ AlignConsecutiveMacros: false
 AlignConsecutiveAssignments: false
 AlignConsecutiveDeclarations: false
 AlignEscapedNewlines: Left
-AlignOperands:   true
+AlignOperands: true
 AlignTrailingComments: true
 AllowAllArgumentsOnNextLine: true
 AllowAllConstructorInitializersOnNextLine: true
@@ -25,22 +25,22 @@ AlwaysBreakTemplateDeclarations: Yes
 BinPackArguments: true
 BinPackParameters: true
 BraceWrapping:
-  AfterCaseLabel:  false
-  AfterClass:      false
-  AfterControlStatement: false
-  AfterEnum:       false
-  AfterFunction:   false
-  AfterNamespace:  false
-  AfterObjCDeclaration: false
-  AfterStruct:     false
-  AfterUnion:      false
-  AfterExternBlock: false
-  BeforeCatch:     false
-  BeforeElse:      false
-  IndentBraces:    false
-  SplitEmptyFunction: true
-  SplitEmptyRecord: true
-  SplitEmptyNamespace: true
+    AfterCaseLabel: false
+    AfterClass: false
+    AfterControlStatement: false
+    AfterEnum: false
+    AfterFunction: false
+    AfterNamespace: false
+    AfterObjCDeclaration: false
+    AfterStruct: false
+    AfterUnion: false
+    AfterExternBlock: false
+    BeforeCatch: false
+    BeforeElse: false
+    IndentBraces: false
+    SplitEmptyFunction: true
+    SplitEmptyRecord: true
+    SplitEmptyNamespace: true
 BreakBeforeBinaryOperators: None
 BreakBeforeBraces: Attach
 BreakBeforeInheritanceComma: false
@@ -50,8 +50,8 @@ BreakConstructorInitializersBeforeComma: false
 BreakConstructorInitializers: BeforeColon
 BreakAfterJavaFieldAnnotations: false
 BreakStringLiterals: true
-ColumnLimit:     100
-CommentPragmas:  '^ IWYU pragma:'
+ColumnLimit: 100
+CommentPragmas: '^ IWYU pragma:'
 CompactNamespaces: false
 ConstructorInitializerAllOnOneLineOrOnePerLine: true
 ConstructorInitializerIndentWidth: 4
@@ -59,39 +59,39 @@ ContinuationIndentWidth: 4
 Cpp11BracedListStyle: true
 DeriveLineEnding: true
 DerivePointerAlignment: false
-DisableFormat:   false
+DisableFormat: false
 ExperimentalAutoDetectBinPacking: false
 FixNamespaceComments: true
 ForEachMacros:
-  - foreach
-  - Q_FOREACH
-  - BOOST_FOREACH
-IncludeBlocks:   Regroup
+    - foreach
+    - Q_FOREACH
+    - BOOST_FOREACH
+IncludeBlocks: Regroup
 IncludeCategories:
-  - Regex:           '^<ext/.*\.h>'
-    Priority:        2
-    SortPriority:    0
-  - Regex:           '^<.*\.h>'
-    Priority:        1
-    SortPriority:    0
-  - Regex:           '^<.*'
-    Priority:        2
-    SortPriority:    0
-  - Regex:           '.*'
-    Priority:        3
-    SortPriority:    0
-IncludeIsMainRegex: '([-_](test|unittest))?$'
+    - Regex: ^<ext/.*\.h>
+      Priority: 2
+      SortPriority: 0
+    - Regex: ^<.*\.h>
+      Priority: 1
+      SortPriority: 0
+    - Regex: ^<.*
+      Priority: 2
+      SortPriority: 0
+    - Regex: .*
+      Priority: 3
+      SortPriority: 0
+IncludeIsMainRegex: ([-_](test|unittest))?$
 IncludeIsMainSourceRegex: ''
 IndentCaseLabels: true
 IndentGotoLabels: true
 IndentPPDirectives: None
-IndentWidth:     2
+IndentWidth: 2
 IndentWrappedFunctionNames: false
 JavaScriptQuotes: Leave
 JavaScriptWrapImports: true
 KeepEmptyLinesAtTheStartOfBlocks: false
 MacroBlockBegin: ''
-MacroBlockEnd:   ''
+MacroBlockEnd: ''
 MaxEmptyLinesToKeep: 1
 NamespaceIndentation: None
 ObjCBinPackProtocolList: Never
@@ -108,35 +108,35 @@ PenaltyExcessCharacter: 1000000
 PenaltyReturnTypeOnItsOwnLine: 200
 PointerAlignment: Left
 RawStringFormats:
-  - Language:        Cpp
-    Delimiters:
-      - cc
-      - CC
-      - cpp
-      - Cpp
-      - CPP
-      - 'c++'
-      - 'C++'
-    CanonicalDelimiter: ''
-    BasedOnStyle:    google
-  - Language:        TextProto
-    Delimiters:
-      - pb
-      - PB
-      - proto
-      - PROTO
-    EnclosingFunctions:
-      - EqualsProto
-      - EquivToProto
-      - PARSE_PARTIAL_TEXT_PROTO
-      - PARSE_TEST_PROTO
-      - PARSE_TEXT_PROTO
-      - ParseTextOrDie
-      - ParseTextProtoOrDie
-    CanonicalDelimiter: ''
-    BasedOnStyle:    google
-ReflowComments:  true
-SortIncludes:    true
+    - Language: Cpp
+      Delimiters:
+          - cc
+          - CC
+          - cpp
+          - Cpp
+          - CPP
+          - c++
+          - C++
+      CanonicalDelimiter: ''
+      BasedOnStyle: google
+    - Language: TextProto
+      Delimiters:
+          - pb
+          - PB
+          - proto
+          - PROTO
+      EnclosingFunctions:
+          - EqualsProto
+          - EquivToProto
+          - PARSE_PARTIAL_TEXT_PROTO
+          - PARSE_TEST_PROTO
+          - PARSE_TEXT_PROTO
+          - ParseTextOrDie
+          - ParseTextProtoOrDie
+      CanonicalDelimiter: ''
+      BasedOnStyle: google
+ReflowComments: true
+SortIncludes: true
 SortUsingDeclarations: true
 SpaceAfterCStyleCast: false
 SpaceAfterLogicalNot: false
@@ -150,18 +150,17 @@ SpaceBeforeRangeBasedForLoopColon: true
 SpaceInEmptyBlock: false
 SpaceInEmptyParentheses: false
 SpacesBeforeTrailingComments: 2
-SpacesInAngles:  false
+SpacesInAngles: false
 SpacesInConditionalStatement: false
 SpacesInContainerLiterals: true
 SpacesInCStyleCastParentheses: false
 SpacesInParentheses: false
 SpacesInSquareBrackets: false
 SpaceBeforeSquareBrackets: false
-Standard:        Auto
+Standard: Auto
 StatementMacros:
-  - Q_UNUSED
-  - QT_REQUIRE_VERSION
-TabWidth:        8
-UseCRLF:         false
-UseTab:          Never
-...
+    - Q_UNUSED
+    - QT_REQUIRE_VERSION
+TabWidth: 8
+UseCRLF: false
+UseTab: Never
diff --git a/.github/ISSUE_TEMPLATE/--bug-report.md b/.github/ISSUE_TEMPLATE/--bug-report.md
new file mode 100644
index 000000000..d0f42cff7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/--bug-report.md
@@ -0,0 +1,44 @@
+---
+name: "\U0001F41B Bug Report"
+about: Create a report to help us improve ProGraML
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+## 🐛 Bug
+
+<!-- A clear and concise description of what the bug is. -->
+
+## To Reproduce
+
+Steps to reproduce the behavior:
+
+1.
+1.
+1.
+
+<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->
+
+## Expected behavior
+
+<!-- A clear and concise description of what you expected to happen. -->
+
+## Environment
+
+<!-- Please fill in this checklist: -->
+
+- ProGraML version (if building from source, use `git rev-parse HEAD`):
+- How you installed ProGraML (source, pip):
+- OS:
+- Python version:
+- Build command you used (if building from source):
+- GCC/clang version (if building from source):
+- Bazel version (if building from source):
+- Versions of any other relevant libraries:
+
+
+## Additional context
+
+<!-- Add any other context about the problem here. -->
diff --git a/.github/ISSUE_TEMPLATE/--documentation.md b/.github/ISSUE_TEMPLATE/--documentation.md
new file mode 100644
index 000000000..5aa369cef
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/--documentation.md
@@ -0,0 +1,12 @@
+---
+name: "\U0001F4DA Documentation"
+about: Report a documentation issue
+title: ''
+labels: 'Documentation'
+assignees: ''
+
+---
+
+## 📚 Documentation
+
+<!-- A clear and concise description of the issue. -->
diff --git a/.github/ISSUE_TEMPLATE/--feature-request.md b/.github/ISSUE_TEMPLATE/--feature-request.md
new file mode 100644
index 000000000..c9db71047
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/--feature-request.md
@@ -0,0 +1,28 @@
+---
+name: "\U0001F680Feature Request"
+about: Submit a proposal/request for a new ProGraML feature
+title: ''
+labels: 'Enhancement'
+assignees: ''
+
+---
+
+## 🚀 Feature
+
+<!-- A clear and concise description of the feature proposal. -->
+
+## Motivation
+
+<!-- Please outline the motivation for the proposal. Is your feature request related to a problem? e.g., I'm always frustrated when [...]. If this is related to another GitHub issue, please link here too. -->
+
+## Pitch
+
+<!-- A clear and concise description of what you want to happen. -->
+
+## Alternatives
+
+<!-- A clear and concise description of any alternative solutions or features you've considered, if any. -->
+
+## Additional context
+
+<!-- Add any other context or screenshots about the feature request here. -->
diff --git a/.github/ISSUE_TEMPLATE/--questions-help-support.md b/.github/ISSUE_TEMPLATE/--questions-help-support.md
new file mode 100644
index 000000000..2416c904b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/--questions-help-support.md
@@ -0,0 +1,16 @@
+---
+name: "❓Questions/Help/Support"
+about: Do you need support? We have resources.
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+## ❓ Questions and Help
+
+<!-- A clear and concise description of the question. -->
+
+## Additional Context
+
+<!-- Add any other context about the problem here. -->
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000..676b18c8d
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,1 @@
+Fixes #{issue number}
diff --git a/.github/actions/install-build-dependencies/action.yaml b/.github/actions/install-build-dependencies/action.yaml
new file mode 100644
index 000000000..7ffdac8e4
--- /dev/null
+++ b/.github/actions/install-build-dependencies/action.yaml
@@ -0,0 +1,35 @@
+---
+name: Install build dependencies
+runs:
+    using: composite
+    steps:
+        - name: Install dependencies (linux)
+          run: |
+              if [ "$(uname)" != "Darwin" ]; then
+                  curl -L "https://github.com/bazelbuild/bazelisk/releases/download/v1.6.1/bazelisk-linux-amd64" > bazel
+                  chmod +x bazel
+                  sudo mv bazel /usr/local/bin/bazel
+                  sudo apt-get install clang-9 patchelf
+              fi
+          shell: bash
+
+        - name: Install dependencies (macos)
+          run: |
+              if [ "$(uname)" = "Darwin" ]; then
+                  brew install bazelisk zlib
+              fi
+          shell: bash
+          env:
+              LDFLAGS: -L/usr/local/opt/zlib/lib
+              CPPFLAGS: -I/usr/local/opt/zlib/include
+              PKG_CONFIG_PATH: /usr/local/opt/zlib/lib/pkgconfig
+
+        - name: Install Python dependencies
+          run: |
+              python -m pip install -r requirements.txt
+              echo /home/runner/.local/bin >> $GITHUB_PATH
+          shell: bash
+
+        - name: whoami
+          run: ./tools/whoami.sh
+          shell: bash
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
deleted file mode 100644
index 890462338..000000000
--- a/.github/workflows/ci.yaml
+++ /dev/null
@@ -1,82 +0,0 @@
----
-name: CI
-
-on:
-    push:
-    pull_request:
-    schedule:
-        - cron: 0 0 * * 0 # weekly
-
-jobs:
-    test:
-        runs-on: ${{ matrix.os }}
-
-        strategy:
-            fail-fast: false
-            matrix:
-                os: [ubuntu-latest, macos-latest]
-                python: [3.6, 3.7, 3.8]
-                exclude:
-                    # Only test recent python versions on macOS.
-                    - os: macos-latest
-                      python: 3.6
-                    - os: macos-latest
-                      python: 3.7
-        steps:
-            - uses: actions/checkout@v2
-
-            - name: 'Linux: Mount bazel cache'
-              uses: pat-s/always-upload-cache@v2.1.0
-              with:
-                  path: /home/runner/.cache/bazel
-                  key: ${{ matrix.os }}-${{ matrix.python }}-bazel
-              if: matrix.os == 'ubuntu-latest'
-
-            - name: 'Linux: Install system dependencies'
-              run: |
-                  curl -L "https://github.com/bazelbuild/bazelisk/releases/download/v1.6.1/bazelisk-linux-amd64" > bazel.tmp
-                  sudo mv bazel.tmp /usr/local/bin/bazel
-                  chmod +x /usr/local/bin/bazel
-              if: matrix.os == 'ubuntu-latest'
-
-            - name: 'macOS: Mount bazel cache'
-              uses: pat-s/always-upload-cache@v2.1.0
-              with:
-                  path: /private/var/tmp/_bazel_runner
-                  key: ${{ matrix.os }}-${{ matrix.python }}-bazel
-              if: matrix.os == 'macos-latest'
-
-            - name: 'macOS: Install system dependencies'
-              run: |
-                  brew install freetype pkg-config bazelisk coreutils findutils gnu-sed zlib
-              if: matrix.os == 'macos-latest'
-
-            - name: Set up Python ${{ matrix.python }}
-              uses: actions/setup-python@v2
-              with:
-                  python-version: ${{ matrix.python }}
-
-            - name: Install python dependencies
-              run: |
-                  python3 -m pip install --upgrade pip
-                  # Install python dependencies that fail with bazel pip import.
-                  grep -E '(numpy|torch)' programl/requirements.txt | xargs python3 -m pip install
-                  python3 -m pip install setuptools
-                  echo /home/runner/.local/bin >> $GITHUB_PATH
-
-            - name: whoami
-              run: ./tools/whoami.sh
-
-            - name: Test
-              run: xargs bazel test --config=ci //...
-
-            - name: Install
-              run: |
-                  mkdir "${GITHUB_WORKSPACE}/install"
-                  bazel run --config=ci //:install "${GITHUB_WORKSPACE}/install"
-                  echo "${GITHUB_WORKSPACE}/install/bin" >> $GITHUB_PATH
-                  echo "LD_LIBRARY_PATH=${GITHUB_WORKSPACE}/install/lib" >> $GITHUB_ENV
-
-            - name: Test installed binaries
-              run: |
-                  llvm2graph --version
diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml
new file mode 100644
index 000000000..63ce791ea
--- /dev/null
+++ b/.github/workflows/coverage.yaml
@@ -0,0 +1,46 @@
+---
+name: Test Coverage
+
+on:
+    push:
+        branches:
+            - development
+            - stable
+    pull_request:
+
+jobs:
+    pytest-cov:
+        runs-on: ubuntu-latest
+
+        steps:
+            - uses: actions/checkout@v2
+
+            - name: Setup python
+              uses: actions/setup-python@v2
+              with:
+                  python-version: 3.9
+
+            - name: Install build dependencies
+              uses: ./.github/actions/install-build-dependencies
+
+            - name: Install
+              run: make install
+              env:
+                  BAZEL_OPTS: --batch
+                  BAZEL_BUILD_OPTS: --config=ci
+                  CC: clang
+                  CXX: clang++
+
+            - name: Test
+              # Note the `|| true` as we don't care about failing tests in this
+              # job.
+              run: make install-test-cov || true
+              env:
+                  CC: clang
+                  CXX: clang++
+
+            - name: Upload coverage to Codecov
+              uses: codecov/codecov-action@v1
+              with:
+                  files: ./coverage.xml
+              if: ${{ always() }}
diff --git a/.github/workflows/lint.yaml b/.github/workflows/pre_commit.yaml
similarity index 96%
rename from .github/workflows/lint.yaml
rename to .github/workflows/pre_commit.yaml
index 8e517950b..4829ba985 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/pre_commit.yaml
@@ -1,7 +1,11 @@
 ---
 name: Pre-commit
 
-on: [push, pull_request]
+on:
+    push:
+        branches:
+            - development
+    pull_request:
 
 jobs:
     all:
diff --git a/.gitignore b/.gitignore
index 7078df1c0..3d24ca5d2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,13 @@
+__pycache__
 .DS_Store
+.env
+/.act
+/.clwb
+/.vscode
+/*.egg-info
 /bazel-*
-/bootstrap.sh
-/env
-/.clwb/
-/.idea/
-
-__pycache__/
+/build
+/coverage.xml
+/dist
+/node_modules
+/package-lock.json
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e306dca13..1070715da 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -7,7 +7,15 @@
 # The pre-commit hooks rely on third party linters being in your $PATH.
 # On macOS, install these using:
 #
-#     $ brew install pre-commit buildifier llvm prototool python
+#     $ brew install pre-commit buildifier llvm prototool python hadolint
+#     $ python -m pip install -r requirements_pre_commit.txt
+#
+# On linux:
+#
+#     $ sudo apt install golang clang-format
+#     $ go get github.com/bazelbuild/buildtools/buildifier
+#     $ GO111MODULE=on go get github.com/uber/prototool/cmd/prototool@dev
+#     $ wget -O hadolint https://github.com/hadolint/hadolint/releases/download/v1.19.0/hadolint-Linux-x86_64 && chmod +x hadolint && sudo mv hadolint /usr/local/bin/hadolint
 #     $ python -m pip install -r requirements.pre_commit.txt
 #
 # Usage
@@ -23,9 +31,10 @@
 #
 repos:
     - repo: https://github.com/pre-commit/pre-commit-hooks
-      rev: v2.3.0
+      rev: v3.4.0
       hooks:
           - id: check-added-large-files
+            args: [--maxkb=10000]
           - id: check-case-conflict
           - id: check-docstring-first
           - id: check-executables-have-shebangs
@@ -34,38 +43,47 @@ repos:
           - id: check-symlinks
           - id: end-of-file-fixer
           - id: pretty-format-json
+            args: [--autofix, --indent=2, --no-sort-keys]
           - id: requirements-txt-fixer
           - id: trailing-whitespace
     - repo: https://github.com/jumanjihouse/pre-commit-hook-yamlfmt
-      rev: master
+      rev: 0.1.0
       hooks:
           - id: yamlfmt
     - repo: https://github.com/timothycrosley/isort
-      rev: master
+      rev: 5.7.0
       hooks:
           - id: isort
     - repo: https://github.com/psf/black
-      rev: stable
+      rev: 20.8b1
       hooks:
           - id: black
             language_version: python3.8
+    - repo: https://gitlab.com/pycqa/flake8
+      rev: 3.8.4
+      hooks:
+          - id: flake8
     - repo: https://github.com/kynan/nbstripout
-      rev: master
+      rev: 0.3.9
       hooks:
           - id: nbstripout
             files: .ipynb
     - repo: https://github.com/pocc/pre-commit-hooks
-      rev: python
+      rev: v1.1.1
       hooks:
           - id: clang-format
             args: [-i]
     - repo: https://github.com/nametake/pre-commit-prototool
-      rev: master
+      rev: v0.1.0
       hooks:
           # Temporarily disabled:
           # - id: prototool-lint
           - id: prototool-format-fix
     - repo: https://github.com/FelixSeptem/pre-commit-golang
-      rev: master
+      rev: 12f5442f51377b10b26651ad745206bbe1500ad6
       hooks:
           - id: bazel-buildifier
+    - repo: https://github.com/petalmd/dockerfile-pre-commit
+      rev: v1.0
+      hooks:
+          - id: dockerlint
diff --git a/BUILD b/BUILD.bazel
similarity index 90%
rename from BUILD
rename to BUILD.bazel
index c27ef02bf..7bbfb8fad 100644
--- a/BUILD
+++ b/BUILD.bazel
@@ -16,6 +16,7 @@
 # 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.
+load("@rules_python//python:defs.bzl", "py_binary", "py_library")
 
 exports_files([
     ".bettercodehub.yml",
@@ -32,23 +33,37 @@ config_setting(
     visibility = ["//visibility:public"],
 )
 
+py_binary(
+    name = "py_package",
+    srcs = ["empty.py"],
+    main = "empty.py",
+    deps = ["//programl"],
+)
+
+genrule(
+    name = "make_empty_py_file",
+    outs = ["empty.py"],
+    cmd = "touch $@",
+)
+
 sh_binary(
     name = "install",
     srcs = ["install.sh"],
     data = [
-        ":package",
+        ":tar_package",
         "@bazel_tools//tools/bash/runfiles",
     ],
 )
 
 genrule(
-    name = "package",
+    name = "tar_package",
     srcs = [
         "//bin:analyze",
         "//bin:graph2cdfg",
         "//bin:clang2graph-10",
         "//bin:graph2dot",
         "//bin:graph2json",
+        "//bin:llvm2graph-3.8",
         "//bin:llvm2graph-10",
         "//bin:pbq",
         "//bin:xla2graph",
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..57bb13a04
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,8 @@
+## v0.2.0 (2021-06-06)
+
+Move model definitions out of the `programl` package and provide a prebuilt
+wheel for the Python package.
+
+## v0.1.0 (2020-12-17)
+
+Initial release.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 524cdf682..011e16927 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -21,9 +21,38 @@ exposes the issue and submit it as a PR. If you can do this, continue reading.
 
 ## Contributing Code
 
-Please see the [Development](/Documentation/Development.md) guide.
-To summarize:
+To contribute to this project:
 
-1. Always write tests to cover new code.
-2. Keep commits small and atomic.
-3. Use a code style consistent with the rest of the project.
+1. Fork the repo and create your branch from development.
+2. Follow the instructions for building from source to set up your environment.
+3. If you've added code that should be tested, add tests.
+4. If you've changed APIs, update the documentation.
+5. Ensure the make test suite passes.
+6. Make sure your code lints (see Code Style below).
+7. If you haven't already, complete the Contributor License Agreement ("CLA").
+
+
+## Code Style
+
+Our code style is simple:
+
+* Python:
+  [black](https://github.com/psf/black/blob/master/docs/the_black_code_style.md)
+  and [isort](https://pypi.org/project/isort/).
+* C++: [Google C++
+  style](https://google.github.io/styleguide/cppguide.html) with 100
+  character line length and `camelCaseFunctionNames()`.
+
+We use [pre-commit](https://pre-commit.com/) to ensure that code is formatted
+prior to committing. Before submitting pull requests, please run pre-commit. See
+the [config file](/.pre-commit-config.yaml) for installation and usage
+instructions.
+
+Other common sense rules we encourage are:
+
+* Prefer descriptive names over short ones.
+* Split complex code into small units.
+* When writing new features, add tests.
+* Make tests deterministic.
+* Prefer easy-to-use code over easy-to-read, and easy-to-read code over
+  easy-to-write.
diff --git a/DEPS.md b/DEPS.md
deleted file mode 100644
index 1d1c63a7a..000000000
--- a/DEPS.md
+++ /dev/null
@@ -1,94 +0,0 @@
-# macOS
-
-## step 1. Install homebrew:
-```
-test -f /usr/local/bin/brew || yes | /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
-```
-
-## step 2.
-```
-brew install bazel
-brew install coreutils
-brew install exempi
-brew install findutils
-brew install gnu-indent
-brew install gnu-sed
-brew install gnu-tar
-brew install gnu-time
-brew install gnu-which
-brew install libomp
-brew install python
-brew install cmake
-brew install boost
-brew install sdl2
-brew install wget
-brew install ffmpeg
-```
-
-# Linux env
-
-## step 1. Install bazel:
-```
-curl -L -o /tmp/bazel.sh https://github.com/bazelbuild/bazel/releases/download/2.0.0/bazel-2.0.0-installer-linux-x86_64.sh && sudo bash /tmp/bazel.sh && rm /tmp/bazel.sh
-```
-
-## step 2. Ubuntu only
-```
-sudo apt-get update  # Update package index:
-sudo apt install -y --no-install-recommends ca-certificates
-sudo apt install -y --no-install-recommends curl
-sudo apt install -y --no-install-recommends wget
-sudo apt install -y --no-install-recommends g++
-sudo apt install -y --no-install-recommends git
-sudo apt install -y --no-install-recommends ocl-icd-opencl-dev  # Needed for -lOpenCL
-sudo apt install -y --no-install-recommends opencl-c-headers  # Needed for #include <CL/cl.h>
-sudo apt install -y --no-install-recommends pkg-config
-sudo apt install -y --no-install-recommends python
-sudo apt install -y --no-install-recommends python-dev  # Python headers needed for pybind
-sudo apt install -y --no-install-recommends python3.6
-sudo apt install -y --no-install-recommends python3.6-dev
-sudo apt install -y --no-install-recommends python3-distutils
-sudo apt install -y --no-install-recommends unzip
-sudo apt install -y --no-install-recommends zip
-sudo apt install -y --no-install-recommends zlib1g-dev
-sudo apt install -y --no-install-recommends openjdk-11-jdk
-sudo apt install -y --no-install-recommends m4
-sudo apt install -y --no-install-recommends libexempi-dev
-sudo apt install -y --no-install-recommends rsync
-sudo apt install -y --no-install-recommends python3-numpy
-sudo apt install -y --no-install-recommends build-essential
-sudo apt install -y --no-install-recommends libsdl2-dev
-sudo apt install -y --no-install-recommends libjpeg-dev
-sudo apt install -y --no-install-recommends nasm
-sudo apt install -y --no-install-recommends tar
-sudo apt install -y --no-install-recommends libbz2-dev
-sudo apt install -y --no-install-recommends libgtk2.0-dev
-sudo apt install -y --no-install-recommends cmake
-sudo apt install -y --no-install-recommends libfluidsynth-dev
-sudo apt install -y --no-install-recommends libgme-dev
-sudo apt install -y --no-install-recommends libopenal-dev
-sudo apt install -y --no-install-recommends timidity
-sudo apt install -y --no-install-recommends libwildmidi-dev
-sudo apt install -y --no-install-recommends libboost-all-dev
-sudo apt install -y --no-install-recommends libsdl2-dev
-sudo apt install -y --no-install-recommends patch # Required by bazel workspace rules:
-sudo apt-get install -y --no-install-recommends libmysqlclient-dev
-sudo apt-get install -y --no-install-recommends libtinfo-dev
-sudo apt-get install -y --no-install-recommends python3-tk
-```
-in one line:
-
-```bash
-sudo apt-get update && sudo apt install -y --no-install-recommends ca-certificates curl wget g++ git ocl-icd-opencl-dev opencl-c-headers  pkg-config python3.6 python3.6-dev python3-pip python3-distutils unzip zip zlib1g-dev openjdk-11-jdk m4 libexempi-dev rsync python3-numpy build-essential libsdl2-dev libjpeg-dev nasm tar libbz2-dev libgtk2.0-dev cmake libfluidsynth-dev libgme-dev libopenal-dev timidity libwildmidi-dev libboost-all-dev libsdl2-dev patch libmysqlclient-dev libtinfo-dev
-```
-
-## step 3. python packages manually installed
-```
-python -m pip install 'pybind11==2.4.3' # Required for building pyopencl:
-python3 -m pip install 'setuptools==49.2.0' # Required for installing tensorflow
-python3 -m pip install 'tensorflow==1.14.0'
-```
-in one line
-```bash
-python3 -m pip install 'pybind11==2.4.3'  'setuptools==49.2.0' && python3 -m pip install 'tensorflow==1.14.0'
-```
diff --git a/Documentation/Development.md b/Documentation/Development.md
deleted file mode 100644
index cf82dd808..000000000
--- a/Documentation/Development.md
+++ /dev/null
@@ -1,107 +0,0 @@
-# Development
-
-This document describes the development workflow for ProGraML.
-
-## Branching Model
-
-This project uses the
-[git flow](https://danielkummer.github.io/git-flow-cheatsheet/) branching model
-to ensure that changes to the graph data format which break compatibility are
-recognized by different release versions.
-
-Please write new features by branching off of `development` and using the
-branch name format: `feature/<name>_<issue>`, where `<name>` is a short
-snake_case summary of the feature, and `<issue>` is an issue number on the
-[issue tracker](https://github.com/ChrisCummins/ProGraML/issues) (all new
-features should start with a tracking issue).
-
-To use git flow:
-
-1. [Install git-flow](https://github.com/nvie/gitflow/wiki/Installation).
-2. Initialize it in this repository by running:
-
-```
-$ git flow init
-
-Which branch should be used for bringing forth production releases?
-Branch name for production releases: stable
-
-Which branch should be used for integration of the "next release"?
-Branch name for "next release" development: development
-
-How to name your supporting branch prefixes?
-Feature branches? [feature/]
-Release branches? [release/]
-Hotfix branches? [hotfix/]
-Support branches? [support/]
-Version tag prefix? []
-```
-
-## Commits
-
-Try to ensure that commits are small and atomic. Commit messages should
-describe:
-
-1. The goal of the change.
-2. Example usage of the new code (if appropriate).
-3. What you did to test the change (if not covered by existing tests).
-4. The URL of the GitHUb tracking issue,
-   e.g., `github.com/ChrisCummins/ProGraML/issues/76`.
-
-Example:
-
-```
-From 6aa9b5bbec0525b27c950d59b773e1585cedf4a3 Mon Sep 17 00:00:00 2001
-From: Chris Cummins <chrisc.101@gmail.com>
-Date: Fri, 21 Aug 2020 22:45:06 +0100
-Subject: [PATCH] Update dependencies: networkx, scipy, scikit-learn.
-
-Python 3.8 introduced some failures in the python pypi dependencies:
-
-scipy:
-
-    $ bazel test //programl/models:rolling_results_test
-
-    ImportError: dlopen(${bazelroot}/bin/programl/test/benchmarks/benchmark_dataflow_analyses.runfiles/programl_requirements_pypi__scipy_1_2_1/scipy/linalg/cython_lapack.cpython-38-darwin.so, 2): Symbol not found: _cbbcsd_
-      Referenced from: ${bazelroot}/bin/programl/test/benchmarks/benchmark_dataflow_analyses.runfiles/programl_requirements_pypi__scipy_1_2_1/scipy/linalg/cython_lapack.cpython-38-darwin.so
-      Expected in: flat namespace
-
-scikit-learn:
-
-    $ bazel test //programl/test/benchmarks:benchmark_dataflow_analyses
-
-      File "${bazelroot}/bin/programl/models/rolling_results_test.runfiles/programl_requirements_pypi__scikit_learn_0_20_3/sklearn/externals/joblib/externals/cloudpickle/cloudpickle.py", line 148, in _make_cell_set_template_code
-        return types.CodeType(
-    TypeError: an integer is required (got type bytes)
-
-networkx:
-
-    $ bazel test //programl/ir/llvm:inst2vec_test
-
-    ${bazelroot}/bin/bin/inst2vec.runfiles/programl_requirements_pypi__networkx_2_2/networkx/drawing/nx_pydot.py:210: SyntaxWarning: "is" with a literal. Did you mean "=="?
-
-This patch updates those packages to the versions to maintain support on
-Python 3.8. Tested on macOS 10.15, python 3.8.
-
-github.com//issues/76
----
- requirements.txt                  | 7 ++++---
- third_party/py/scikit_learn/BUILD | 1 +
- 2 files changed, 5 insertions(+), 3 deletions(-)
-```
-
-## Tests
-
-Please, please, test new code.
-
-To paraphrase Kanye West, "one good test is worth a thousand patches". Use
-bazel's excellent testing infrastructure to run the existing tests, and the new
-ones that you write). Although bazel caches test results and will perform only
-the minimum amount of testing on incremental builds, this is a large project
-with a lot of tests, you may not want to run all of them as many will not be
-relevant to what you are working on. Use the CI script provided by bazel to run
-the tests on only those targets which have been modified on your current branch:
-
-```sh
-$ ./third_party/bazel/ci.sh
-```
diff --git a/Documentation/ProjectStructure.md b/Documentation/ProjectStructure.md
index ede0ba459..cfdb777ca 100644
--- a/Documentation/ProjectStructure.md
+++ b/Documentation/ProjectStructure.md
@@ -3,12 +3,16 @@
 The key directories of this project are:
 
 * [`/bin`](/bin): Command line tools. All executable binaries are stored here.
-* [`/Documentation`](/Documentation): Additional (possibly out-of-date 😢) documentation.
-* [`/programl/graph`](/programl/graph): Libraries for creating and manipulating the ProGraML graph representation.
+* [`/Documentation`](/Documentation): Additional (possibly out-of-date 😢)
+  documentation.
+* [`/models`](/model): Model implementations and helper code.
+* [`/programl/graph`](/programl/graph): Libraries for creating and manipulating
+  the ProGraML graph representation.
 * [`/programl/ir`](/programl/ir): Support for specific compiler IRs, e.g. LLVM.
-* [`/programl/proto`](/programl/proto): Protocol message definitions. These define the core data structures for this project.
-  This is a good starting point for understanding the code.
-* [`/programl/test`](/programl/test): Test data and helper libraries.
+* [`/programl/proto`](/programl/proto): Protocol message definitions. These
+  define the core data structures for this project. This is a good starting
+  point for understanding the code.
 * [`/programl/util`](/programl/util): Utility code.
-* [`/tasks`](/tasks): Experimental tasks. This contains the code for producing the results of our published
-  experiments.
+* [`/tasks`](/tasks): Experimental tasks. This contains the code for producing
+  the results of our published experiments.
+* [`/tests`](/tests): Tests.
diff --git a/Documentation/bin/clang2graph.txt b/Documentation/bin/clang2graph.txt
index 217ef2c1a..8c949413f 100644
--- a/Documentation/bin/clang2graph.txt
+++ b/Documentation/bin/clang2graph.txt
@@ -51,7 +51,7 @@ OPTIONS:
   -emit-ast               Emit Clang AST files for source inputs
   -emit-llvm              Use the LLVM representation for assembler and object files
   -E                      Only run the preprocessor
-  -faligned-allocation    Enable C++17 aligned allocation functions
+  -faligned-allocation    Enable c++14 aligned allocation functions
   -fallow-editor-placeholders
                           Treat editor placeholders as valid source code
   -fansi-escape-codes     Use ANSI escape codes for diagnostics
@@ -303,7 +303,7 @@ OPTIONS:
   -freciprocal-math       Allow division operations to be reassociated
   -freg-struct-return     Override the default ABI to return small structs in registers
   -frelaxed-template-template-args
-                          Enable C++17 relaxed template template argument matching
+                          Enable c++14 relaxed template template argument matching
   -freroll-loops          Turn on loop reroller
   -frtlib-add-rpath       Add -rpath with architecture-specific resource directory to the linker flags
   -fsanitize-address-field-padding=<value>
diff --git a/INSTALL.md b/INSTALL.md
new file mode 100644
index 000000000..baf1733c1
--- /dev/null
+++ b/INSTALL.md
@@ -0,0 +1,152 @@
+# Installation
+
+## Installing the Python package
+
+Install the ProGraML python package release using:
+
+    pip install -U programl
+
+ProGraML requires Python >= 3.6. The binary works on macOS and Linux (on Ubuntu
+18.04, Fedora 28, Debian 10 or newer equivalents).
+
+## Installing the prebuilt binaries
+
+Download the latest macOS or Linux release archive from the [releases
+page](https://github.com/ChrisCummins/ProGraML/releases).
+
+Unpack the release archive to `~/.local/opt/programl` (or a directory of your
+choice) using:
+```sh
+mkdir -p ~/.local/opt/programl
+tar xjvf ~/Downloads/programl-*.tar.bz2 -C ~/.local/opt/programl
+```
+
+Add the installed binaries to your PATH, and libraries to your LD_LIBRARY_PATH.
+You may want to add this to your `~/.bashrc`:
+```sh
+export PATH=$HOME/.local/opt/programl/bin:$PATH
+export LD_LIBRARY_PATH=$HOME/.local/opt/programl/lib:$LD_LIBRARY_PATH
+```
+
+
+## Building from Source
+
+Building from source requires a modern C++ toolchain and bazel.
+
+### macOS
+
+On macOS the required dependencies can be installed using
+[homebrew](https://docs.brew.sh/Installation):
+
+```sh
+brew install bazelisk zlib
+export LDFLAGS="-L/usr/local/opt/zlib/lib"
+export CPPFLAGS="-I/usr/local/opt/zlib/include"
+export PKG_CONFIG_PATH="/usr/local/opt/zlib/lib/pkgconfig"
+```
+
+Now proceed to [All platforms](#all-platforms) below.
+
+### Linux
+
+On debian-based linux systems, install the required toolchain using:
+
+```sh
+sudo apt install clang-9 libtinfo5 libjpeg-dev zlib1g-dev
+wget https://github.com/bazelbuild/bazelisk/releases/download/v1.7.5/bazelisk-linux-amd64 -O bazel
+chmod +x bazel && mkdir -p ~/.local/bin && mv -v bazel ~/.local/bin
+export PATH="$HOME/.local/bin:$PATH"
+export CC=clang
+export CXX=clang++
+```
+
+### All platforms
+
+We recommend using
+[conda](https://docs.conda.io/projects/conda/en/latest/user-guide/install/) to
+manage the remaining build dependencies. First create a conda environment with
+the required dependencies:
+
+    conda create -n programl python=3.8 cmake pandoc patchelf
+    conda activate programl
+    conda install -c conda-forge doxygen
+
+Then clone the ProGraML source code using:
+
+    git clone https://github.com/facebookresearch/ProGraML.git
+    cd ProGraML
+
+There are two primary git branches: `stable` tracks the latest release;
+`development` is for bleeding edge features that may not yet be mature. Checkout
+your preferred branch and install the python development dependencies using:
+
+    git checkout stable
+    make init
+
+The `make init` target only needs to be run once on initial setup, or when
+pulling remote changes to the ProGraML repository.
+
+Run the test suite to confirm that everything is working:
+
+    make test
+
+To build and install the python package, run:
+
+    make install
+
+**NOTE:** To use the `programl` package that is installed by `make install` you
+must leave the root directory of this repository. Attempting to import
+`programl` while in the root of this repository will cause import errors.
+
+When you are finished, you can deactivate and delete the conda environment
+using:
+
+    conda deactivate
+    conda env remove -n programl
+
+
+## Using ProGraML as a dependency
+
+For Python packages you can simply add `pip freeze | grep programl >> requirements.txt`.
+
+If you are using bazel you can add ProGraML as an external dependency. Add to
+your WORKSPACE file:
+
+```py
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+http_archive(
+    name="programl",
+    strip_prefix="ProGraML-<stable-commit>",
+    urls=["https://github.com/ChrisCummins/ProGraML/archive/<stable-commit>.tar.gz"],
+)
+
+# ----------------- Begin ProGraML dependencies -----------------
+<WORKSPACE dependencies>
+# ----------------- End ProGraML dependencies -----------------
+```
+
+Where `<WORKSPACE dependencies>` is the block of delimited code in
+[@programl//:WORKSPACE](https://github.com/ChrisCummins/ProGraML/blob/development/WORKSPACE)
+(this is an unfortunately clumsy workaround for [recursive
+workspaces](https://github.com/bazelbuild/bazel/issues/1943)).
+
+Then in your BUILD file:
+
+```py
+cc_library(
+    name = "mylib",
+    srcs = ["mylib.cc"],
+    deps = [
+        "@programl//programl/ir/llvm",
+    ],
+)
+
+py_binary(
+    name = "myscript",
+    srcs = ["myscript.py"],
+    deps = [
+        "@programl//programl/ir/llvm/py:llvm",
+    ],
+)
+```
diff --git a/Makefile b/Makefile
new file mode 100644
index 000000000..1003e0370
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,281 @@
+# This Makefile is adapted from CompilerGym's Makefile, available at:
+#   https://github.com/facebookresearch/CompilerGym
+#
+# CompilerGym is is licensed under the MIT license.
+# Copyright (c) Facebook, Inc. and its affiliates.
+define HELP
+ProGraML $(VERSION). Available targets:
+
+Setting up
+----------
+
+    make init
+        Install the build and runtime python dependencies. This should be run
+        once before any other targets.
+
+
+Testing
+-------
+
+    make test
+        Run the test suite. Test results are cached so that incremental test
+        runs are minimal and fast. Use this as your go-to target for testing
+        modifications to the codebase.
+
+    make itest
+        Run the test suite continuously on change. This is equivalent to
+        manually running `make test` when a source file is modified. Note that
+        `make install-test` tests are not run. This requires bazel-watcher.
+        See: https://github.com/bazelbuild/bazel-watcher#installation
+
+
+Post-installation Tests
+-----------------------
+
+    make install-test
+        Run the full test suite against an installed ProGraML package. This
+        requires that the ProGraML package has been installed (`make
+        install`). This is useful for checking the package contents but is
+        usually not needed for interactive development since `make test` runs
+        the same tests without having to install anything.
+
+    make install-test-cov
+        The same as `make install-test`, but with python test coverage
+        reporting. A summary of test coverage is printed at the end of execution
+        and the full details are recorded in a coverage.xml file in the project
+        root directory.
+
+    make install-fuzz
+        Run the fuzz testing suite against an installed ProGraML package.
+        Fuzz tests are tests that generate their own inputs and run in a loop
+        until an error has been found, or until a minimum number of seconds have
+        elapsed. This minimum time is controlled using a FUZZ_SECONDS variable.
+        The default is 300 seconds (5 minutes). Override this value at the
+        command line, for example `FUZZ_SECONDS=60 make install-fuzz` will run
+        the fuzz tests for a minimum of one minute. This requires that the
+        ProGraML package has been installed (`make install`).
+
+
+Documentation
+-------------
+
+    make docs
+        Build the HTML documentation using Sphinx. This is the documentation
+        site that is hosted at <https://facebookresearch.github.io/ProGraML>.
+        The generated HTML files are in docs/build/html.
+
+    make livedocs
+        Build the HTML documentation and serve them on localhost:8000. Changes
+        to the documentation will automatically trigger incremental rebuilds
+        and reload the changes.
+
+
+Deployment
+----------
+
+    make bdist_wheel
+        Build an optimized python wheel. The generated file is in
+        dist/programl-<version>-<platform_tags>.whl
+
+    make install
+        Build and install the python wheel.
+
+    make bdist_wheel-linux
+        Use a docker container to build a python wheel for linux. This is only
+        used for making release builds. This requires docker.
+
+    bdist_wheel-linux-shell
+        Drop into a bash terminal in the docker container that is used for
+        linux builds. This may be useful for debugging bdist_wheel-linux
+        builds.
+
+    make bdist_wheel-linux-test
+        Run the `make install-test` suite against the build artifact generated
+        by `make bdist_wheel-linux`.
+
+
+Tidying up
+-----------
+
+    make clean
+        Remove build artifacts.
+
+    make distclean
+        Clean up all build artifacts, including the build cache.
+
+    make uninstall
+        Uninstall the python package.
+
+    make purge
+        Uninstall the python package and completely remove all datasets, logs,
+        and cached files. Any experimental data or generated logs will be
+        irreversibly deleted!
+endef
+export HELP
+
+# Configurable paths to binaries.
+CC ?= clang
+CXX ?= clang++
+BAZEL ?= bazel
+IBAZEL ?= ibazel
+PYTHON ?= python3
+RSYNC ?= rsync
+
+# Bazel build options.
+BAZEL_OPTS ?=
+BAZEL_BUILD_OPTS ?= -c opt
+BAZEL_TEST_OPTS ?=
+
+# The path of the repository reoot.
+ROOT := $(dir $(realpath $(firstword $(MAKEFILE_LIST))))
+
+VERSION := $(shell cat version.txt)
+OS := $(shell uname)
+
+
+##############
+# Setting up #
+##############
+
+.DEFAULT_GOAL := help
+
+.PHONY: help init
+
+help:
+	@echo "$$HELP"
+
+init:
+	$(PYTHON) -m pip install -r requirements.txt
+	pre-commit install
+
+
+############
+# Building #
+############
+
+# Files and directories generated by python disttools.
+DISTTOOLS_OUTS := dist build programl.egg-info
+
+BUILD_TARGET ?= //:py_package
+
+bazel-build:
+	$(BAZEL) $(BAZEL_OPTS) build $(BAZEL_BUILD_OPTS) $(BUILD_TARGET)
+
+install-test-data:
+	$(BAZEL) $(BAZEL_OPTS) build $(BAZEL_BUILD_OPTS) //tests/data 2>/dev/null
+
+bdist_wheel: bazel-build
+	$(PYTHON) setup.py bdist_wheel
+
+bdist_wheel-linux-rename:
+	mv dist/programl-$(VERSION)-py3-none-linux_x86_64.whl dist/programl-$(VERSION)-py3-none-manylinux2014_x86_64.whl
+
+bdist_wheel-linux:
+	rm -rf build
+	docker pull chriscummins/compiler_gym-linux-build:latest
+	docker run -v $(ROOT):/ProGraML --workdir /ProGraML --rm --shm-size=8g chriscummins/programl-linux-build:latest /bin/sh -c './packaging/container_init.sh && make bdist_wheel'
+	mv dist/programl-$(VERSION)-py3-none-linux_x86_64.whl dist/programl-$(VERSION)-py3-none-manylinux2014_x86_64.whl
+	rm -rf build
+
+bdist_wheel-linux-shell:
+	docker run -v $(ROOT):/ProGraML --workdir /ProGraML --rm --shm-size=8g -it --entrypoint "/bin/bash" chriscummins/programl-linux-build:latest
+
+bdist_wheel-linux-test:
+	docker run -v $(ROOT):/ProGraML --workdir /ProGraML --rm --shm-size=8g chriscummins/programl-linux-build:latest /bin/sh -c 'cd /ProGraML && pip3 install -U pip && pip3 install dist/programl-$(VERSION)-py3-none-manylinux2014_x86_64.whl && pip install -r tests/requirements.txt && make install-test'
+
+all: docs bdist_wheel bdist_wheel-linux
+
+.PHONY: bazel-build bdist_wheel bdist_wheel-linux bdist_wheel-linux-shell bdist_wheel-linux-test
+
+
+###########
+# Testing #
+###########
+
+programl_SITE_DATA ?= "/tmp/programl/tests/site_data"
+programl_CACHE ?= "/tmp/programl/tests/cache"
+
+# A directory that is used as the working directory for running pytest tests
+# by symlinking the tests directory into it.
+INSTALL_TEST_ROOT ?= "/tmp/programl/install_tests"
+
+# The target to use. If not provided, all tests will be run. For `make test` and
+# related, this is a bazel target pattern, with default value '//...'. For `make
+# install-test` and related, this is a relative file path of the directory or
+# file to test, with default value 'tests'.
+TEST_TARGET ?=
+
+# Extra command line arguments for pytest.
+PYTEST_ARGS ?=
+
+test:
+	$(BAZEL) $(BAZEL_OPTS) test $(BAZEL_TEST_OPTS) $(if $(TEST_TARGET),$(TEST_TARGET),//...)
+
+itest:
+	$(IBAZEL) $(BAZEL_OPTS) test $(BAZEL_TEST_OPTS) $(if $(TEST_TARGET),$(TEST_TARGET),//...)
+
+# Since we can't run ProGraML from the project root we need to jump through some
+# hoops to run pytest "out of tree" by creating an empty directory and
+# symlinking the test directory into it so that pytest can be invoked.
+install-test-setup: install-test-data
+	mkdir -p "$(INSTALL_TEST_ROOT)"
+	rm -rf "$(INSTALL_TEST_ROOT)/tests" "$(INSTALL_TEST_ROOT)/tox.ini"
+	mkdir "$(INSTALL_TEST_ROOT)/tests/"
+	"$(RSYNC)" -a "$(ROOT)/bazel-bin/tests/data/" "$(INSTALL_TEST_ROOT)/tests/data/"
+	"$(RSYNC)" -a "$(ROOT)/tests/" "$(INSTALL_TEST_ROOT)/tests/"
+	ln -s "$(ROOT)/tox.ini" "$(INSTALL_TEST_ROOT)"
+
+define pytest
+	cd "$(INSTALL_TEST_ROOT)" && pytest $(if $(TEST_TARGET),$(TEST_TARGET),tests) $(1) $(PYTEST_ARGS)
+endef
+
+install-test: install-test-setup
+	$(call pytest,--benchmark-disable -n auto -k "not fuzz" --durations=5)
+
+# Note we export $CI=1 so that the tests always run as if within the CI
+# environement. This is to ensure that the reported coverage matches that of
+# the value on: https://codecov.io/gh/facebookresearch/ProGraML
+install-test-cov: install-test-setup
+	export CI=1; $(call pytest,--benchmark-disable -n auto -k "not fuzz" --durations=5 --cov=programl --cov-report=xml --cov-report=term)
+	@mv "$(INSTALL_TEST_ROOT)/coverage.xml" .
+
+# The minimum number of seconds to run the fuzz tests in a loop for. Override
+# this at the commandline, e.g. `FUZZ_SECONDS=1800 make fuzz`.
+FUZZ_SECONDS ?= 300
+
+install-fuzz: install-test-setup
+	$(call pytest,-p no:sugar -x -vv -k fuzz --seconds=$(FUZZ_SECONDS))
+
+post-install-test:
+	$(MAKE) -C examples/makefile_integration clean
+	SEARCH_TIME=3 $(MAKE) -C examples/makefile_integration test
+
+.PHONY: test post-install-test
+
+
+################
+# Installation #
+################
+
+pip-install:
+	$(PYTHON) setup.py install
+
+install: | bazel-build pip-install
+
+.PHONY: pip-install install
+
+
+##############
+# Tidying up #
+##############
+
+.PHONY: clean distclean uninstall purge
+
+clean:
+	rm -rf $(DISTTOOLS_OUTS)
+
+distclean: clean
+	bazel clean --expunge
+
+uninstall:
+	$(PYTHON) -m pip uninstall -y programl
diff --git a/README.md b/README.md
index 81fcf759b..77391ca3b 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
     </tr>
     <tr>
       <td>Python Versions</td>
-      <td>3.6, 3.7, 3.8</td>
+      <td>3.6, 3.7, 3.8, 3.9</td>
     </tr>
     <tr>
       <td>
@@ -64,23 +64,20 @@
 
 <!-- MarkdownTOC autolink="true" -->
 
-- [Overview](#overview)
-- [Getting Started](#getting-started)
-- [Constructing the ProGraML Representation](#constructing-the-programl-representation)
-    - [Step 1: Compiler IR](#step-1-compiler-ir)
-    - [Step 2: Control-flow](#step-2-control-flow)
-    - [Step 3: Data-flow](#step-3-data-flow)
-    - [Step 4: Call graph](#step-4-call-graph)
-- [Datasets](#datasets)
-- [Installation](#installation)
-  - [Requirements](#requirements)
-  - [Command-line tools](#command-line-tools)
-  - [Dataflow experiments](#dataflow-experiments)
-  - [Using this project as a dependency](#using-this-project-as-a-dependency)
-- [Usage](#usage)
-  - [End-to-end C++ example](#end-to-end-c-example)
-- [Contributing](#contributing)
-- [Acknowledgements](#acknowledgements)
+1. [Overview](#overview)
+1. [Getting Started](#getting-started)
+1. [Installation](#installation)
+  1. [Datasets](#datasets)
+1. [Constructing the ProGraML Representation](#constructing-the-programl-representation)
+  1. [Step 1: Compiler IR](#step-1-compiler-ir)
+  1. [Step 2: Control-flow](#step-2-control-flow)
+  1. [Step 3: Data-flow](#step-3-data-flow)
+  1. [Step 4: Call graph](#step-4-call-graph)
+1. [Usage](#usage)
+  1. [End-to-end C++ flow](#end-to-end-c-flow)
+  1. [Dataflow experiments](#dataflow-experiments)
+1. [Contributing](#contributing)
+1. [Acknowledgements](#acknowledgements)
 
 <!-- /MarkdownTOC -->
 
@@ -116,31 +113,54 @@ Or if papers are more your ☕, have a read of ours:
 [![Preprint](/Documentation/arXiv.2003.10536/paper.png)](https://arxiv.org/abs/2003.10536)
 
 
+## Installation
+
+Install the latest release of the Python package using:
+
+```
+pip install -U programl
+```
+
+See [INSTALL.md](INSTALL.md) for alternative installation options.
+
+
+#### Datasets
+
+[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.4122437.svg)](https://doi.org/10.5281/zenodo.4122437)
+
+Please see [this doc](/Documentation/DataflowDataset.md) for download
+links for our publicly available datasets of LLVM-IRs, ProGraML graphs, and data
+flow analysis labels.
+
+
 ## Constructing the ProGraML Representation
 
 The ProGraML representation is constructed in multiple stages. Here we describe
 the process for a simple recursive Fibonacci implementation in C. For
 instructions on how to run this process, see [Usage](#usage) below.
 
+
 #### Step 1: Compiler IR
 
-<img src="/Documentation/assets/llvm2graph-1-ir.png" width=300>
+<img src="https://github.com/ChrisCummins/ProGraML/raw/development/Documentation/assets/llvm2graph-1-ir.png" width=300>
 
 We start by lowering the program to a compiler IR. In this case, we'll use
 LLVM-IR. This can be done using: `clang -emit-llvm -S -O3 fib.c`.
 
+
 #### Step 2: Control-flow
 
-<img src="/Documentation/assets/llvm2graph-2-cfg.png" width=300>
+<img src="https://github.com/ChrisCummins/ProGraML/raw/development/Documentation/assets/llvm2graph-2-cfg.png" width=300>
 
 We begin building a graph by constructing a full-flow graph of the program. In a
 full-flow graph, every instruction is a node and the edges are control-flow.
 Note that edges are positional so that we can differentiate the branching
 control flow in that `switch` instruction.
 
+
 #### Step 3: Data-flow
 
-<img src="/Documentation/assets/llvm2graph-3-dfg.png" width=300>
+<img src="https://github.com/ChrisCummins/ProGraML/raw/development/Documentation/assets/llvm2graph-3-dfg.png" width=300>
 
 Then we add a graph node for every variable and constant. In the drawing above,
 the diamonds are constants and the variables are ovals. We add data-flow edges
@@ -149,9 +169,10 @@ and variables and the constants which define/use them. Like control edges, data
 edges have positions. In the case of data edges, the position encodes the order
 of a data element in the list of instruction operands.
 
+
 #### Step 4: Call graph
 
-<img src="/Documentation/assets/llvm2graph-4-cg.png" width=300>
+<img src="https://github.com/ChrisCummins/ProGraML/raw/development/Documentation/assets/llvm2graph-4-cg.png" width=300>
 
 Finally, we add call edges (green) from callsites to the function entry
 instruction, and return edges from function exits to the callsite. Since this is
@@ -165,35 +186,10 @@ The process described above can be run locally using our
 clang2graph -O3 fib.c | graph2dot`
 
 
-## Datasets
-
-[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.4122437.svg)](https://doi.org/10.5281/zenodo.4122437)
-
-Please see [this doc](/Documentation/DataflowDataset.md) for download
-links for our publicly available datasets of LLVM-IRs, ProGraML graphs, and data
-flow analysis labels.
-
-
-## Installation
-
-
-### Requirements
-
-* macOS ≥ 10.15 or GNU / Linux (we recommend Ubuntu Linux ≥ 18.04).
-* bazel ≥ 2.0 (we recommend using
-  [bazelisk](https://github.com/bazelbuild/bazelisk) to automatically
-  download and use the correct bazel version).
-* Python ≥ 3.6
-
-Once you have the above requirements installed, test that everything is working
-by building and running full test suite:
-
-```sh
-$ bazel test //...
-```
+## Usage
 
 
-### Command-line tools
+#### End-to-end C++ flow
 
 In the manner of Unix Zen, creating and manipulating ProGraML graphs is done
 using [command-line tools](/Documentation/bin) which act as filters,
@@ -201,101 +197,8 @@ reading in graphs from stdin and emitting graphs to stdout. The structure for
 graphs is described through a series of [protocol
 buffers](/Documentation/ProtocolBuffers.md).
 
-Build and install the command line tools to `~/.local/opt/programl` (or a
-directory of your choice) using:
-
-```sh
-$ bazel run -c opt //:install -- ~/.local/opt/programl
-```
-
-Then to use them, append the following to your `~/.bashrc`:
-
-```sh
-export PATH=~/.local/opt/programl/bin:$PATH
-export LD_LIBRARY_PATH=~/.local/opt/programl/lib:$LD_LIBRARY_PATH
-```
-
-
-### Dataflow experiments
-
-1. Download and unpack our [dataflow
-dataset](/Documentation/DataflowDataset.md)
-2. Install the python requirements:
-```sh
-python -m pip install -r requirements.txt
-```
-3. Train and evaluate a graph neural network model using:
-
-```sh
-bazel run -c opt //tasks/dataflow:train_ggnn -- \
-    --analysis reachability \
-    --path=$HOME/programl
-```
-
-where `--analysis` is the name of the analysis you want to evaluate, and
-`--path` is the root of the unpacked dataset. There are a lot of options that
-you can use to control the behavior of the experiment, see `--helpfull` for a
-full list. Some useful ones include:
-
-* `--batch_size` controls the number of nodes in each batch of graphs.
-* `--layer_timesteps` defines the layers of the GGNN model, and the number of timesteps used for
-  each.
-* `--learning_rate` sets the initial learning rate of the optimizer.
-* `--lr_decay_rate` the rate at which learning rate decays.
-* `--lr_decay_steps` number of gradient steps until the lr is decayed.
-* `--train_graph_counts` lists the number of graphs to train on between runs of the validation set.
-
-
-### Using this project as a dependency
-
-If you are using bazel you can add ProGraML as an external dependency. Add to
-your WORKSPACE file:
-
-```py
-load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
-
-http_archive(
-    name="programl",
-    strip_prefix="ProGraML-<stable-commit>",
-    urls=["https://github.com/ChrisCummins/ProGraML/archive/<stable-commit>.tar.gz"],
-)
-
-# ----------------- Begin ProGraML dependencies -----------------
-<WORKSPACE dependencies>
-# ----------------- End ProGraML dependencies -----------------
-```
-
-Where `<WORKSPACE dependencies>` is the block of delimited code in
-[@programl//:WORKSPACE](https://github.com/ChrisCummins/ProGraML/blob/development/WORKSPACE)
-(this is an unfortunately clumsy workaround for [recursive
-workspaces](https://github.com/bazelbuild/bazel/issues/1943)).
-
-Then in your BUILD file:
-
-```py
-cc_library(
-    name = "mylib",
-    srcs = ["mylib.cc"],
-    deps = [
-        "@programl//programl/ir/llvm",
-    ],
-)
-
-py_binary(
-    name = "myscript",
-    srcs = ["myscript.py"],
-    deps = [
-        "@programl//programl/ir/llvm/py:llvm",
-    ],
-)
-```
-
-## Usage
-
-### End-to-end C++ example
-
 This section provides an example step-by-step guide for generating a program
-graph from a C++ application.
+graph for a C++ application.
 
 1. Install LLVM-10 and the ProGraML [command line tools](#command-line-tools).
 2. Compile your C++ code to LLVM-IR. The way to do this to modify your build
@@ -333,6 +236,38 @@ $ dot -Tpng my_app.dot -o my_app.png
 ```
 
 
+#### Dataflow experiments
+
+1. Follow the instructions for [building from source](#building-from-source)
+2. Download and unpack our [dataflow
+dataset](/Documentation/DataflowDataset.md)
+3. Train and evaluate a graph neural network model using:
+
+```sh
+bazel run -c opt //tasks/dataflow:train_ggnn -- \
+    --analysis reachability \
+    --path=$HOME/programl
+```
+
+where `--analysis` is the name of the analysis you want to evaluate, and
+`--path` is the root of the unpacked dataset. There are a lot of options that
+you can use to control the behavior of the experiment, see `--helpfull` for a
+full list. Some useful ones include:
+
+* `--batch_size` controls the number of nodes in each batch of graphs.
+* `--layer_timesteps` defines the layers of the GGNN model, and the number of timesteps used for
+  each.
+* `--learning_rate` sets the initial learning rate of the optimizer.
+* `--lr_decay_rate` the rate at which learning rate decays.
+* `--lr_decay_steps` number of gradient steps until the lr is decayed.
+* `--train_graph_counts` lists the number of graphs to train on between runs of the validation set.
+
+🏗️ **Under construction** We are in the process of refactoring the dataflow
+experiments with a revamped API.  There are currently bugs in the data loader
+which may affect training jobs, see
+[#147](https://github.com/ChrisCummins/ProGraML/issues/147).
+
+
 ## Contributing
 
 Patches, bug reports, feature requests are welcome! Please use the
diff --git a/WORKSPACE b/WORKSPACE
index 21aa34e2f..fdc6e00d2 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -29,31 +29,6 @@ load("@com_github_chriscummins_rules_bats//:bats.bzl", "bats_deps")
 
 bats_deps()
 
-# === Python requirements ===
-
-load(
-    "@rules_python//python:pip.bzl",
-    "pip3_import",
-    "pip_repositories",
-)
-
-pip_repositories()
-
-# //programl:requirements.txt
-
-pip3_import(
-    name = "programl_requirements",
-    timeout = 3600,
-    requirements = "@programl//:programl/requirements.txt",
-)
-
-load(
-    "@programl_requirements//:requirements.bzl",
-    programl_pip_install = "pip_install",
-)
-
-programl_pip_install()
-
 # === Protocol buffers ===
 
 load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies", "rules_proto_toolchains")
@@ -73,33 +48,3 @@ load("@com_github_grpc_grpc//bazel:grpc_extra_deps.bzl", "grpc_extra_deps")
 grpc_extra_deps()
 
 # ----------------- End ProGraML dependencies -----------------
-
-# //tasks:requirements.txt
-
-pip3_import(
-    name = "tasks_requirements",
-    timeout = 3600,
-    requirements = "@programl//:tasks/requirements.txt",
-)
-
-load(
-    "@tasks_requirements//:requirements.bzl",
-    tasks_pip_install = "pip_install",
-)
-
-tasks_pip_install()
-
-# //tests:requirements.txt
-
-pip3_import(
-    name = "tests_requirements",
-    timeout = 3600,
-    requirements = "@programl//:tests/requirements.txt",
-)
-
-load(
-    "@tests_requirements//:requirements.bzl",
-    tests_pip_install = "pip_install",
-)
-
-tests_pip_install()
diff --git a/tests/benchmarks/BUILD b/benchmarks/BUILD
similarity index 81%
rename from tests/benchmarks/BUILD
rename to benchmarks/BUILD
index 633c66454..0f15dc232 100644
--- a/tests/benchmarks/BUILD
+++ b/benchmarks/BUILD
@@ -18,7 +18,7 @@
 
 py_test(
     name = "benchmark_dataflow_analyses",
-    timeout = "long",
+    timeout = "eternal",
     srcs = ["benchmark_dataflow_analyses.py"],
     data = [
         "//bin:analyze",
@@ -26,17 +26,15 @@ py_test(
     ],
     deps = [
         "//programl/util/py:runfiles_path",
-        "//third_party/py/absl",
-        "//third_party/py/numpy",
     ],
 )
 
 py_test(
     name = "benchmark_dataflow_ggnn",
-    timeout = "long",
+    timeout = "eternal",
     srcs = ["benchmark_dataflow_ggnn.py"],
     deps = [
-        "//programl/models/ggnn",
+        "//models/ggnn",
         "//programl/proto:programl_py",
         "//programl/util/py:humanize",
         "//programl/util/py:progress",
@@ -45,17 +43,15 @@ py_test(
         "//tasks/dataflow:graph_loader",
         "//tests/plugins:llvm_program_graph",
         "//tests/plugins:llvm_reachability_features",
-        "//third_party/py/absl",
-        "//third_party/py/tqdm",
     ],
 )
 
 #py_test(
 #    name = "benchmark_dataflow_lstm",
-#    timeout = "long",
+#    timeout = "eternal",
 #    srcs = ["benchmark_dataflow_lstm.py"],
 #    deps = [
-#        "//programl/models/lstm",
+#        "//models/lstm",
 #        "//programl/proto:programl_py",
 #        "//programl/util/py:progress",
 #        "//programl/util/py:threaded_iterator",
@@ -63,15 +59,13 @@ py_test(
 #        "//tests/plugins:llvm_reachability_features",
 #        "//tasks/dataflow:graph_loader",
 #        "//tasks/dataflow:lstm_batch_builder",
-#        "//third_party/py/absl",
-#        "//third_party/py/ncc:vocabulary",
-#        "//third_party/py/tqdm",
+#        "//programl/third_party/ncc:vocabulary",
 #    ],
 #)
 
 py_test(
     name = "benchmark_llvm2graph",
-    timeout = "long",
+    timeout = "eternal",
     srcs = ["benchmark_llvm2graph.py"],
     data = [
         "//bin:llvm2graph",
@@ -79,7 +73,5 @@ py_test(
     ],
     deps = [
         "//programl/util/py:runfiles_path",
-        "//third_party/py/absl",
-        "//third_party/py/numpy",
     ],
 )
diff --git a/tests/benchmarks/benchmark_dataflow_analyses.py b/benchmarks/benchmark_dataflow_analyses.py
similarity index 76%
rename from tests/benchmarks/benchmark_dataflow_analyses.py
rename to benchmarks/benchmark_dataflow_analyses.py
index 45b9d0fe4..864cf17e3 100644
--- a/tests/benchmarks/benchmark_dataflow_analyses.py
+++ b/benchmarks/benchmark_dataflow_analyses.py
@@ -15,7 +15,6 @@
 # limitations under the License.
 """Benchmark for analyze command."""
 import subprocess
-import sys
 import time
 
 import numpy as np
@@ -28,8 +27,8 @@
 )
 FLAGS = flags.FLAGS
 
-LLVM_IR_GRAPHS = runfiles_path("programl/tests/data/llvm_ir_graphs")
-ANALYZE = runfiles_path("programl/bin/analyze")
+LLVM_IR_GRAPHS = runfiles_path("tests/data/llvm_ir_graphs")
+ANALYZE = runfiles_path("bin/analyze")
 
 ANALYSES = [
     "reachability",
@@ -60,20 +59,24 @@ def BenchmarkAnalysis(analysis: str):
     paths = list(LLVM_IR_GRAPHS.iterdir())
     if FLAGS.graph_count:
         paths = paths[: FLAGS.graph_count]
+    print(f"Runtimes for {analysis} analysis on test LLVM-IR files:", flush=True)
     for path in paths:
         with open(str(path), "rb") as f:
             start_time = time.time()
-            subprocess.call(
-                [ANALYZE, analysis, "--stdin_fmt=pb", "--stdout_fmt=pb"],
-                stdin=f,
-                stdout=subprocess.DEVNULL,
-                stderr=subprocess.DEVNULL,
-            )
-            runtimes.append(time.time() - start_time)
+            try:
+                subprocess.call(
+                    [ANALYZE, analysis, "--stdin_fmt=pb", "--stdout_fmt=pb"],
+                    stdin=f,
+                    stdout=subprocess.DEVNULL,
+                    stderr=subprocess.DEVNULL,
+                    timeout=30,
+                )
+                runtimes.append(time.time() - start_time)
+            except subprocess.TimeoutExpired:
+                runtimes.append(float("inf"))
+                break
     runtimes_ms = np.array(runtimes) * 1000
-    print(f"Runtimes for {analysis} analysis on test LLVM-IR files:")
-    print(SummarizeFloats(runtimes_ms, unit="ms"))
-    sys.stdout.flush()
+    print(SummarizeFloats(runtimes_ms, unit="ms"), flush=True)
 
 
 def main(argv):
diff --git a/tests/benchmarks/benchmark_dataflow_ggnn.py b/benchmarks/benchmark_dataflow_ggnn.py
similarity index 99%
rename from tests/benchmarks/benchmark_dataflow_ggnn.py
rename to benchmarks/benchmark_dataflow_ggnn.py
index 294dc5c55..707717a95 100644
--- a/tests/benchmarks/benchmark_dataflow_ggnn.py
+++ b/benchmarks/benchmark_dataflow_ggnn.py
@@ -25,7 +25,7 @@
 from sklearn.exceptions import UndefinedMetricWarning
 from tqdm import tqdm
 
-from programl.models.ggnn.ggnn import Ggnn
+from models.ggnn.ggnn import Ggnn
 from programl.proto import epoch_pb2
 from programl.util.py import humanize, progress
 from programl.util.py.threaded_iterator import ThreadedIterator
diff --git a/tests/benchmarks/benchmark_dataflow_lstm.py b/benchmarks/benchmark_dataflow_lstm.py
similarity index 98%
rename from tests/benchmarks/benchmark_dataflow_lstm.py
rename to benchmarks/benchmark_dataflow_lstm.py
index c9be69fcc..43260fc16 100644
--- a/tests/benchmarks/benchmark_dataflow_lstm.py
+++ b/benchmarks/benchmark_dataflow_lstm.py
@@ -20,18 +20,18 @@
 import tempfile
 import warnings
 
-from absl import agg, flags
+from absl import app, flags
 from sklearn.exceptions import UndefinedMetricWarning
 from tqdm import tqdm
 
-from programl.models.lstm.lstm import Lstm
+from models.lstm.lstm import Lstm
 from programl.proto import epoch_pb2
+from programl.third_party.ncc import vocabulary
 from programl.util.py import progress
 from programl.util.py.threaded_iterator import ThreadedIterator
 from tasks.dataflow.graph_loader import DataflowGraphLoader
 from tasks.dataflow.lstm_batch_builder import DataflowLstmBatchBuilder
 from tests.plugins import llvm_program_graph, llvm_reachability_features
-from third_party.py.ncc import vocabulary
 
 flags.DEFINE_integer("graph_count", None, "The number of graphs to load.")
 flags.DEFINE_integer("batch_count", None, "The number of batches.")
diff --git a/tests/benchmarks/benchmark_llvm2graph.py b/benchmarks/benchmark_llvm2graph.py
similarity index 95%
rename from tests/benchmarks/benchmark_llvm2graph.py
rename to benchmarks/benchmark_llvm2graph.py
index 9fe38c461..8b3656c00 100644
--- a/tests/benchmarks/benchmark_llvm2graph.py
+++ b/benchmarks/benchmark_llvm2graph.py
@@ -29,8 +29,8 @@
 )
 FLAGS = flags.FLAGS
 
-LLVM_IR = runfiles_path("programl/tests/data/llvm_ir")
-LLVM2GRAPH = runfiles_path("programl/bin/llvm2graph")
+LLVM_IR = runfiles_path("tests/data/llvm_ir")
+LLVM2GRAPH = runfiles_path("bin/llvm2graph")
 
 
 def SummarizeFloats(floats, nplaces: int = 2, unit: str = "") -> str:
diff --git a/bin/BUILD b/bin/BUILD
index 55a6d775a..ca16101c2 100644
--- a/bin/BUILD
+++ b/bin/BUILD
@@ -56,11 +56,10 @@ cc_binary(
     copts = [
         "-std=c++14",
         "-fno-rtti",
-        "-DGOOGLE_PROTOBUF_NO_RTTI",
     ],
     visibility = ["//visibility:public"],
     deps = [
-        "//programl/ir/llvm",
+        "//programl/ir/llvm:llvm-10",
         "//programl/proto:programl_cc",
         "@labm8//labm8/cpp:status",
         "@llvm//10.0.0",
@@ -116,7 +115,6 @@ py_binary(
         "//programl/proto:programl_py",
         "//programl/util/py:init_app",
         "//programl/util/py:stdin_fmt",
-        "//third_party/py/absl",
     ],
 )
 
@@ -143,7 +141,6 @@ py_binary(
         "//programl/util/py:init_app",
         "//programl/util/py:stdin_fmt",
         "//programl/util/py:stdout_fmt",
-        "//third_party/py/absl",
     ],
 )
 
@@ -155,6 +152,24 @@ genrule(
     visibility = ["//visibility:public"],
 )
 
+cc_binary(
+    name = "llvm2graph-3.8",
+    srcs = ["llvm2graph.cc"],
+    copts = ["-std=c++14"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//programl:version_header",
+        "//programl/ir/llvm:llvm-3.8",
+        "//programl/proto:programl_cc",
+        "//programl/util:stdout_fmt",
+        "@labm8//labm8/cpp:app",
+        "@labm8//labm8/cpp:status",
+        "@labm8//labm8/cpp:statusor",
+        "@labm8//labm8/cpp:strutil",
+        "@llvm//3.8.0",
+    ],
+)
+
 cc_binary(
     name = "llvm2graph-10",
     srcs = ["llvm2graph.cc"],
@@ -162,7 +177,7 @@ cc_binary(
     visibility = ["//visibility:public"],
     deps = [
         "//programl:version_header",
-        "//programl/ir/llvm",
+        "//programl/ir/llvm:llvm-10",
         "//programl/proto:programl_cc",
         "//programl/util:stdout_fmt",
         "@labm8//labm8/cpp:app",
@@ -180,9 +195,9 @@ cc_binary(
     deps = [
         "//programl:version_header",
         "//programl/proto:programl_cc",
+        "//programl/third_party/tensorflow:xla_cc",
         "//programl/util:stdin_fmt",
         "//programl/util:stdout_fmt",
-        "//third_party/tensorflow:xla_cc",
         "@com_google_protobuf//:protobuf",
         "@labm8//labm8/cpp:app",
     ],
@@ -195,8 +210,8 @@ cc_binary(
     deps = [
         "//programl:version_header",
         "//programl/ir/xla:hlo_module_graph_builder",
+        "//programl/third_party/tensorflow:xla_cc",
         "//programl/util:stdin_fmt",
-        "//third_party/tensorflow:xla_cc",
         "@labm8//labm8/cpp:app",
         "@labm8//labm8/cpp:logging",
         "@labm8//labm8/cpp:statusor",
diff --git a/bin/llvm2graph.cc b/bin/llvm2graph.cc
index 4070eae33..e3d85e5c2 100644
--- a/bin/llvm2graph.cc
+++ b/bin/llvm2graph.cc
@@ -90,7 +90,7 @@ llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> GetInputAsBuffer(const string
     programl::IrList irList;
     if (!irList.ParseFromIstream(&file)) {
       LOG(ERROR) << "Failed to parse IrList protocol buffer";
-      return nullptr;
+      return std::unique_ptr<llvm::MemoryBuffer>(nullptr);
     }
     const auto& ir = irList.ir(FLAGS_ir_list_index);
     return llvm::MemoryBuffer::getMemBuffer(ir.text());
@@ -99,14 +99,14 @@ llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> GetInputAsBuffer(const string
     programl::Ir ir;
     if (!ir.ParseFromIstream(&file)) {
       LOG(ERROR) << "Failed to parse Ir protocol buffer";
-      return nullptr;
+      return std::unique_ptr<llvm::MemoryBuffer>(nullptr);
     }
     return llvm::MemoryBuffer::getMemBuffer(ir.text());
   } else {
     auto buf = llvm::MemoryBuffer::getFileOrSTDIN(filename);
     if (!buf) {
       LOG(ERROR) << "File not found: " << filename;
-      return nullptr;
+      return std::unique_ptr<llvm::MemoryBuffer>(nullptr);
     }
     return buf;
   }
diff --git a/bin/pbq.cc b/bin/pbq.cc
index 4e7394047..ded0898ca 100644
--- a/bin/pbq.cc
+++ b/bin/pbq.cc
@@ -21,10 +21,10 @@
 #include "programl/proto/program_graph.pb.h"
 #include "programl/proto/program_graph_features.pb.h"
 #include "programl/proto/src.pb.h"
+#include "programl/third_party/tensorflow/xla.pb.h"
 #include "programl/util/stdin_fmt.h"
 #include "programl/util/stdout_fmt.h"
 #include "programl/version.h"
-#include "third_party/tensorflow/xla.pb.h"
 
 const char* usage = R"(Query properties of a protocol buffer.
 
diff --git a/bin/xla2graph.cc b/bin/xla2graph.cc
index acc1cd804..fe22b691c 100644
--- a/bin/xla2graph.cc
+++ b/bin/xla2graph.cc
@@ -25,9 +25,9 @@
 #include "labm8/cpp/statusor.h"
 #include "labm8/cpp/string.h"
 #include "programl/ir/xla/hlo_module_graph_builder.h"
+#include "programl/third_party/tensorflow/xla.pb.h"
 #include "programl/util/stdin_fmt.h"
 #include "programl/version.h"
-#include "third_party/tensorflow/xla.pb.h"
 
 static const char* usage = R"(Generate program graph from a HLO module.
 
diff --git a/programl/models/BUILD b/models/BUILD
similarity index 86%
rename from programl/models/BUILD
rename to models/BUILD
index b33a19124..aa5046ce8 100644
--- a/programl/models/BUILD
+++ b/models/BUILD
@@ -23,7 +23,6 @@ py_library(
     deps = [
         ":base_batch_builder",
         "//programl/util/py:humanize",
-        "//third_party/py/absl",
     ],
 )
 
@@ -53,11 +52,6 @@ py_library(
     name = "batch_results",
     srcs = ["batch_results.py"],
     visibility = ["//visibility:public"],
-    deps = [
-        "//third_party/py/absl",
-        "//third_party/py/numpy",
-        "//third_party/py/scikit_learn",
-    ],
 )
 
 py_library(
@@ -75,12 +69,11 @@ py_library(
     srcs = ["model.py"],
     visibility = ["//visibility:public"],
     deps = [
-        "//programl/models:batch_data",
-        "//programl/models:batch_results",
-        "//programl/models:rolling_results_builder",
+        "//models:batch_data",
+        "//models:batch_results",
+        "//models:rolling_results_builder",
         "//programl/proto:programl_py",
         "//programl/util/py:progress",
-        "//third_party/py/absl",
     ],
 )
 
@@ -103,6 +96,17 @@ py_library(
         ":batch_data",
         ":batch_results",
         ":rolling_results",
-        "//third_party/py/tqdm",
+    ],
+)
+
+py_test(
+    name = "rolling_results_test",
+    srcs = ["rolling_results_test.py"],
+    deps = [
+        "//models:batch_data",
+        "//models:batch_results",
+        "//models:rolling_results",
+        "//programl/util/py:runfiles_path",
+        "//tests:test_main",
     ],
 )
diff --git a/programl/models/async_batch_builder.py b/models/async_batch_builder.py
similarity index 95%
rename from programl/models/async_batch_builder.py
rename to models/async_batch_builder.py
index ced94b702..f7cea246f 100644
--- a/programl/models/async_batch_builder.py
+++ b/models/async_batch_builder.py
@@ -20,8 +20,8 @@
 
 from absl import logging
 
-from programl.models.base_batch_builder import BaseBatchBuilder
-from programl.models.batch_data import BatchData
+from models.base_batch_builder import BaseBatchBuilder
+from models.batch_data import BatchData
 from programl.util.py import humanize
 
 
diff --git a/programl/models/base_batch_builder.py b/models/base_batch_builder.py
similarity index 97%
rename from programl/models/base_batch_builder.py
rename to models/base_batch_builder.py
index eed1ce93e..732366fbe 100644
--- a/programl/models/base_batch_builder.py
+++ b/models/base_batch_builder.py
@@ -18,8 +18,8 @@
 from threading import Thread
 from typing import Iterable, Optional
 
-from programl.models.base_graph_loader import BaseGraphLoader
-from programl.models.batch_data import BatchData
+from models.base_graph_loader import BaseGraphLoader
+from models.batch_data import BatchData
 
 
 class BaseBatchBuilder(object):
diff --git a/programl/models/base_graph_loader.py b/models/base_graph_loader.py
similarity index 100%
rename from programl/models/base_graph_loader.py
rename to models/base_graph_loader.py
diff --git a/programl/models/batch_data.py b/models/batch_data.py
similarity index 100%
rename from programl/models/batch_data.py
rename to models/batch_data.py
diff --git a/programl/models/batch_results.py b/models/batch_results.py
similarity index 100%
rename from programl/models/batch_results.py
rename to models/batch_results.py
diff --git a/programl/models/epoch_batch_iterator.py b/models/epoch_batch_iterator.py
similarity index 94%
rename from programl/models/epoch_batch_iterator.py
rename to models/epoch_batch_iterator.py
index 601e4a744..9c3583b80 100644
--- a/programl/models/epoch_batch_iterator.py
+++ b/models/epoch_batch_iterator.py
@@ -16,8 +16,8 @@
 """This module defines an iterator over batches, divided into epochs."""
 from typing import Iterable
 
-from programl.models.base_batch_builder import BaseBatchBuilder
-from programl.models.batch_data import BatchData
+from models.base_batch_builder import BaseBatchBuilder
+from models.batch_data import BatchData
 
 
 def _LimitBatchesToTargetGraphCount(
diff --git a/programl/models/ggnn/BUILD b/models/ggnn/BUILD
similarity index 78%
rename from programl/models/ggnn/BUILD
rename to models/ggnn/BUILD
index 01bd4fad1..b87a9c7a6 100644
--- a/programl/models/ggnn/BUILD
+++ b/models/ggnn/BUILD
@@ -20,7 +20,6 @@ py_library(
     name = "aux_readout",
     srcs = ["aux_readout.py"],
     deps = [
-        "//third_party/py/torch",
     ],
 )
 
@@ -36,15 +35,12 @@ py_library(
         ":loss",
         ":node_embeddings",
         ":readout",
+        "//models:batch_data",
+        "//models:batch_results",
+        "//models:model",
         "//programl/graph/format/py:graph_tuple",
-        "//programl/models:batch_data",
-        "//programl/models:batch_results",
-        "//programl/models:model",
         "//programl/proto:programl_py",
         "//programl/util/py:progress",
-        "//third_party/py/absl",
-        "//third_party/py/numpy",
-        "//third_party/py/torch",
     ],
 )
 
@@ -55,7 +51,6 @@ py_library(
     deps = [
         "//programl/graph/format/py:graph_tuple",
         "//programl/proto:programl_py",
-        "//third_party/py/numpy",
     ],
 )
 
@@ -63,7 +58,6 @@ py_library(
     name = "ggnn_layer",
     srcs = ["ggnn_layer.py"],
     deps = [
-        "//third_party/py/torch",
     ],
 )
 
@@ -76,8 +70,6 @@ py_library(
         ":loss",
         ":metrics",
         ":node_embeddings",
-        "//third_party/py/absl",
-        "//third_party/py/torch",
     ],
 )
 
@@ -88,7 +80,6 @@ py_library(
         ":ggnn_layer",
         ":messaging_layer",
         ":readout",
-        "//third_party/py/torch",
     ],
 )
 
@@ -96,7 +87,6 @@ py_library(
     name = "linear_net",
     srcs = ["linear_net.py"],
     deps = [
-        "//third_party/py/torch",
     ],
 )
 
@@ -104,7 +94,6 @@ py_library(
     name = "loss",
     srcs = ["loss.py"],
     deps = [
-        "//third_party/py/torch",
     ],
 )
 
@@ -114,7 +103,6 @@ py_library(
     deps = [
         ":linear_net",
         ":position_embeddings",
-        "//third_party/py/torch",
     ],
 )
 
@@ -122,7 +110,6 @@ py_library(
     name = "metrics",
     srcs = ["metrics.py"],
     deps = [
-        "//third_party/py/torch",
     ],
 )
 
@@ -130,8 +117,6 @@ py_library(
     name = "node_embeddings",
     srcs = ["node_embeddings.py"],
     deps = [
-        "//third_party/py/numpy",
-        "//third_party/py/torch",
     ],
 )
 
@@ -139,7 +124,6 @@ py_library(
     name = "position_embeddings",
     srcs = ["position_embeddings.py"],
     deps = [
-        "//third_party/py/torch",
     ],
 )
 
@@ -148,6 +132,5 @@ py_library(
     srcs = ["readout.py"],
     deps = [
         ":linear_net",
-        "//third_party/py/torch",
     ],
 )
diff --git a/programl/models/ggnn/aux_readout.py b/models/ggnn/aux_readout.py
similarity index 100%
rename from programl/models/ggnn/aux_readout.py
rename to models/ggnn/aux_readout.py
diff --git a/programl/models/ggnn/ggnn.py b/models/ggnn/ggnn.py
similarity index 97%
rename from programl/models/ggnn/ggnn.py
rename to models/ggnn/ggnn.py
index 6bc48b66b..617f01931 100644
--- a/programl/models/ggnn/ggnn.py
+++ b/models/ggnn/ggnn.py
@@ -22,17 +22,17 @@
 from absl import flags, logging
 from torch import nn
 
+from models.batch_data import BatchData
+from models.batch_results import BatchResults
+from models.ggnn.aux_readout import AuxiliaryReadout
+from models.ggnn.ggnn_batch import GgnnBatchData
+from models.ggnn.ggnn_model import GGNNModel
+from models.ggnn.ggnn_proper import GGNNProper
+from models.ggnn.loss import Loss
+from models.ggnn.node_embeddings import NodeEmbeddings
+from models.ggnn.readout import Readout
+from models.model import Model
 from programl.graph.format.py.graph_tuple import GraphTuple
-from programl.models.batch_data import BatchData
-from programl.models.batch_results import BatchResults
-from programl.models.ggnn.aux_readout import AuxiliaryReadout
-from programl.models.ggnn.ggnn_batch import GgnnBatchData
-from programl.models.ggnn.ggnn_model import GGNNModel
-from programl.models.ggnn.ggnn_proper import GGNNProper
-from programl.models.ggnn.loss import Loss
-from programl.models.ggnn.node_embeddings import NodeEmbeddings
-from programl.models.ggnn.readout import Readout
-from programl.models.model import Model
 from programl.proto import epoch_pb2
 from programl.util.py.progress import NullContext, ProgressContext
 
diff --git a/programl/models/ggnn/ggnn_batch.py b/models/ggnn/ggnn_batch.py
similarity index 100%
rename from programl/models/ggnn/ggnn_batch.py
rename to models/ggnn/ggnn_batch.py
diff --git a/programl/models/ggnn/ggnn_layer.py b/models/ggnn/ggnn_layer.py
similarity index 100%
rename from programl/models/ggnn/ggnn_layer.py
rename to models/ggnn/ggnn_layer.py
diff --git a/programl/models/ggnn/ggnn_model.py b/models/ggnn/ggnn_model.py
similarity index 94%
rename from programl/models/ggnn/ggnn_model.py
rename to models/ggnn/ggnn_model.py
index 5cb77694e..6b6d54214 100644
--- a/programl/models/ggnn/ggnn_model.py
+++ b/models/ggnn/ggnn_model.py
@@ -20,11 +20,11 @@
 from absl import flags
 from torch import nn, optim
 
-from programl.models.ggnn.aux_readout import AuxiliaryReadout
-from programl.models.ggnn.ggnn_proper import GGNNProper
-from programl.models.ggnn.loss import Loss
-from programl.models.ggnn.metrics import Metrics
-from programl.models.ggnn.node_embeddings import NodeEmbeddings
+from models.ggnn.aux_readout import AuxiliaryReadout
+from models.ggnn.ggnn_proper import GGNNProper
+from models.ggnn.loss import Loss
+from models.ggnn.metrics import Metrics
+from models.ggnn.node_embeddings import NodeEmbeddings
 
 FLAGS = flags.FLAGS
 
diff --git a/programl/models/ggnn/ggnn_proper.py b/models/ggnn/ggnn_proper.py
similarity index 97%
rename from programl/models/ggnn/ggnn_proper.py
rename to models/ggnn/ggnn_proper.py
index a021ea6d7..7e82641b5 100644
--- a/programl/models/ggnn/ggnn_proper.py
+++ b/models/ggnn/ggnn_proper.py
@@ -20,9 +20,9 @@
 import torch.nn.functional as F
 from torch import nn
 
-from programl.models.ggnn.ggnn_layer import GGNNLayer
-from programl.models.ggnn.messaging_layer import MessagingLayer
-from programl.models.ggnn.readout import Readout
+from models.ggnn.ggnn_layer import GGNNLayer
+from models.ggnn.messaging_layer import MessagingLayer
+from models.ggnn.readout import Readout
 
 
 class GGNNProper(nn.Module):
diff --git a/programl/models/ggnn/linear_net.py b/models/ggnn/linear_net.py
similarity index 94%
rename from programl/models/ggnn/linear_net.py
rename to models/ggnn/linear_net.py
index b4f739d15..61836768d 100644
--- a/programl/models/ggnn/linear_net.py
+++ b/models/ggnn/linear_net.py
@@ -32,9 +32,9 @@ class LinearNet(nn.Module):
 
     Shape:
     - Input: :math:`(N, *, H_{in})` where :math:`*` means any number of
-    additional dimensions and :math:`H_{in} = \text{in\_features}`
+    additional dimensions and :math:`H_{in} = \\text{in\\_features}`
     - Output: :math:`(N, *, H_{out})` where all but the last dimension
-    are the same shape as the input and :math:`H_{out} = \text{out\_features}`.
+    are the same shape as the input and :math:`H_{out} = \\text{out\\_features}`.
     """
 
     def __init__(self, in_features, out_features, bias=True, dropout=0.0):
diff --git a/programl/models/ggnn/loss.py b/models/ggnn/loss.py
similarity index 100%
rename from programl/models/ggnn/loss.py
rename to models/ggnn/loss.py
diff --git a/programl/models/ggnn/messaging_layer.py b/models/ggnn/messaging_layer.py
similarity index 96%
rename from programl/models/ggnn/messaging_layer.py
rename to models/ggnn/messaging_layer.py
index 6907ca5c9..d1dc20c5a 100644
--- a/programl/models/ggnn/messaging_layer.py
+++ b/models/ggnn/messaging_layer.py
@@ -18,8 +18,8 @@
 import torch.nn.functional as F
 from torch import nn
 
-from programl.models.ggnn.linear_net import LinearNet
-from programl.models.ggnn.position_embeddings import PositionEmbeddings
+from models.ggnn.linear_net import LinearNet
+from models.ggnn.position_embeddings import PositionEmbeddings
 
 
 class MessagingLayer(nn.Module):
diff --git a/programl/models/ggnn/metrics.py b/models/ggnn/metrics.py
similarity index 100%
rename from programl/models/ggnn/metrics.py
rename to models/ggnn/metrics.py
diff --git a/programl/models/ggnn/node_embeddings.py b/models/ggnn/node_embeddings.py
similarity index 100%
rename from programl/models/ggnn/node_embeddings.py
rename to models/ggnn/node_embeddings.py
diff --git a/programl/models/ggnn/position_embeddings.py b/models/ggnn/position_embeddings.py
similarity index 100%
rename from programl/models/ggnn/position_embeddings.py
rename to models/ggnn/position_embeddings.py
diff --git a/programl/models/ggnn/readout.py b/models/ggnn/readout.py
similarity index 97%
rename from programl/models/ggnn/readout.py
rename to models/ggnn/readout.py
index 5f4e79322..eb6285714 100644
--- a/programl/models/ggnn/readout.py
+++ b/models/ggnn/readout.py
@@ -17,7 +17,7 @@
 import torch
 from torch import nn
 
-from programl.models.ggnn.linear_net import LinearNet
+from models.ggnn.linear_net import LinearNet
 
 
 class Readout(nn.Module):
diff --git a/programl/models/lstm/BUILD b/models/lstm/BUILD
similarity index 87%
rename from programl/models/lstm/BUILD
rename to models/lstm/BUILD
index 97abb4e7d..eaa0d03df 100644
--- a/programl/models/lstm/BUILD
+++ b/models/lstm/BUILD
@@ -22,12 +22,10 @@ py_library(
     visibility = ["//visibility:public"],
     deps = [
         ":lstm_batch",
-        "//programl/models:batch_data",
-        "//programl/models:model",
+        "//models:batch_data",
+        "//models:model",
         "//programl/proto:programl_py",
         "//programl/util/py:progress",
-        "//third_party/py/absl",
-        "//third_party/py/numpy",
     ],
 )
 
diff --git a/programl/models/lstm/lstm.py b/models/lstm/lstm.py
similarity index 98%
rename from programl/models/lstm/lstm.py
rename to models/lstm/lstm.py
index 70674b612..f57d6a5dc 100644
--- a/programl/models/lstm/lstm.py
+++ b/models/lstm/lstm.py
@@ -22,10 +22,10 @@
 import tensorflow as tf
 from absl import flags
 
-from programl.models.batch_data import BatchData
-from programl.models.batch_results import BatchResults
-from programl.models.lstm.lstm_batch import LstmBatchData
-from programl.models.model import Model
+from models.batch_data import BatchData
+from models.batch_results import BatchResults
+from models.lstm.lstm_batch import LstmBatchData
+from models.model import Model
 from programl.proto import epoch_pb2
 from programl.util.py.progress import NullContext, ProgressContext
 
diff --git a/programl/models/lstm/lstm_batch.py b/models/lstm/lstm_batch.py
similarity index 100%
rename from programl/models/lstm/lstm_batch.py
rename to models/lstm/lstm_batch.py
diff --git a/programl/models/model.py b/models/model.py
similarity index 97%
rename from programl/models/model.py
rename to models/model.py
index ff484153d..034c1ec14 100644
--- a/programl/models/model.py
+++ b/models/model.py
@@ -19,9 +19,9 @@
 from threading import Thread
 from typing import Any, Dict, Iterable
 
-from programl.models.batch_data import BatchData
-from programl.models.batch_results import BatchResults
-from programl.models.rolling_results_builder import RollingResultsBuilder
+from models.batch_data import BatchData
+from models.batch_results import BatchResults
+from models.rolling_results_builder import RollingResultsBuilder
 from programl.proto import checkpoint_pb2, epoch_pb2
 from programl.util.py.progress import NullContext, ProgressContext
 
diff --git a/models/requirements.txt b/models/requirements.txt
new file mode 100644
index 000000000..d6ee4ef40
--- /dev/null
+++ b/models/requirements.txt
@@ -0,0 +1,2 @@
+scikit-learn>=0.23.2
+torch>=1.7.0
diff --git a/programl/models/rolling_results.py b/models/rolling_results.py
similarity index 96%
rename from programl/models/rolling_results.py
rename to models/rolling_results.py
index 87229c7e7..f99e7e031 100644
--- a/programl/models/rolling_results.py
+++ b/models/rolling_results.py
@@ -17,8 +17,8 @@
 import time
 from typing import Optional
 
-from programl.models.batch_data import BatchData
-from programl.models.batch_results import BatchResults
+from models.batch_data import BatchData
+from models.batch_results import BatchResults
 from programl.proto import epoch_pb2
 
 
@@ -129,7 +129,9 @@ def ToEpochResults(self) -> epoch_pb2.EpochResults:
             mean_recall=self.recall,
             mean_f1=self.f1,
             walltime_seconds=time.time() - self.start_time,
-            confusion_matrix=epoch_pb2.ConfusionMatrix(
+            confusion_matrix=None
+            if self.confusion_matrix is None
+            else epoch_pb2.ConfusionMatrix(
                 column=[
                     epoch_pb2.ConfusionMatrixRow(row=column)
                     for column in self.confusion_matrix.T.tolist()
diff --git a/programl/models/rolling_results_builder.py b/models/rolling_results_builder.py
similarity index 95%
rename from programl/models/rolling_results_builder.py
rename to models/rolling_results_builder.py
index a98720ae2..a628a2655 100644
--- a/programl/models/rolling_results_builder.py
+++ b/models/rolling_results_builder.py
@@ -20,9 +20,9 @@
 
 from tqdm import tqdm
 
-from programl.models.batch_data import BatchData
-from programl.models.batch_results import BatchResults
-from programl.models.rolling_results import RollingResults
+from models.batch_data import BatchData
+from models.batch_results import BatchResults
+from models.rolling_results import RollingResults
 
 
 class RollingResultsBuilder(object):
diff --git a/third_party/py/empty.py b/packaging/container_init.sh
similarity index 100%
rename from third_party/py/empty.py
rename to packaging/container_init.sh
diff --git a/programl/BUILD b/programl/BUILD
index 807b8ab94..2e1b79948 100644
--- a/programl/BUILD
+++ b/programl/BUILD
@@ -1,3 +1,23 @@
+py_library(
+    name = "programl",
+    srcs = [
+        "__init__.py",
+        "version.py",
+    ],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//programl/graph/analysis/py:analysis",
+        "//programl/graph/format/py:format",
+        "//programl/graph/py:program_graph_builder",
+        "//programl/ir/llvm:inst2vec_encoder",
+        "//programl/ir/llvm/py:llvm",
+        "//programl/ir/xla/py:xla",
+        "//programl/proto:programl_py",
+        "//programl/third_party/tensorflow:xla_py",
+        "//programl/util/py:util",
+    ],
+)
+
 genrule(
     name = "version_header-gen",
     srcs = ["//:version.txt"],
@@ -8,7 +28,7 @@ genrule(
 genrule(
     name = "version_py-gen",
     srcs = ["//:version.txt"],
-    outs = ["__init__.py"],
+    outs = ["version.py"],
     cmd = "echo \"PROGRAML_VERSION=\\\"$$(tr -d '\n' < $<)\\\"\" > $@",
 )
 
@@ -17,9 +37,3 @@ cc_library(
     hdrs = ["version.h"],
     visibility = ["//visibility:public"],
 )
-
-py_library(
-    name = "programl",
-    srcs = ["__init__.py"],
-    visibility = ["//visibility:public"],
-)
diff --git a/tests/models/BUILD b/programl/__init__.py
similarity index 56%
rename from tests/models/BUILD
rename to programl/__init__.py
index abeff138f..fb44495df 100644
--- a/tests/models/BUILD
+++ b/programl/__init__.py
@@ -13,17 +13,20 @@
 # 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.
+"""ProGraML is a graph-based program representation for data flow analysis and
+compiler optimizations.
+"""
+from programl.ir.llvm.py.llvm import BuildProgramGraph
+from programl.version import PROGRAML_VERSION
 
-py_test(
-    name = "rolling_results_test",
-    srcs = ["rolling_results_test.py"],
-    deps = [
-        "//programl/models:batch_data",
-        "//programl/models:batch_results",
-        "//programl/models:rolling_results",
-        "//programl/util/py:runfiles_path",
-        "//tests:test_main",
-        "//third_party/py/absl",
-        "//third_party/py/pytest",
-    ],
-)
+__version__ = PROGRAML_VERSION
+__author__ = "Chris Cummins"
+__email__ = "chrisc.101@gmail.com"
+__copyright__ = "Copyright 2019-2020 the ProGraML authors"
+__license__ = "Apache License, Version 2.0"
+
+build_llvm_graph = BuildProgramGraph
+
+__all__ = [
+    "build_llvm_graph",
+]
diff --git a/programl/graph/format/py/BUILD b/programl/graph/format/py/BUILD
index 8fbb003a6..06788e519 100644
--- a/programl/graph/format/py/BUILD
+++ b/programl/graph/format/py/BUILD
@@ -21,6 +21,18 @@ load(
     "pybind_extension",
 )
 
+py_library(
+    name = "format",
+    visibility = ["//visibility:public"],
+    deps = [
+        ":cdfg",
+        ":graph_serializer",
+        ":graph_tuple",
+        ":graph_tuple_builder",
+        ":nx_format",
+    ],
+)
+
 py_library(
     name = "cdfg",
     srcs = ["cdfg.py"],
@@ -31,7 +43,6 @@ py_library(
     deps = [
         "//programl/proto:programl_py",
         "//programl/util/py:runfiles_path",
-        "//third_party/py/absl",
     ],
 )
 
@@ -61,7 +72,6 @@ py_library(
     srcs = ["graph_tuple.py"],
     visibility = ["//visibility:public"],
     deps = [
-        "//third_party/py/numpy",
     ],
 )
 
@@ -72,7 +82,6 @@ py_library(
     deps = [
         ":graph_tuple",
         "//programl/proto:programl_py",
-        "//third_party/py/numpy",
     ],
 )
 
@@ -104,6 +113,5 @@ py_library(
     visibility = ["//visibility:public"],
     deps = [
         "//programl/proto:programl_py",
-        "//third_party/py/networkx",
     ],
 )
diff --git a/programl/graph/format/py/cdfg.py b/programl/graph/format/py/cdfg.py
index 346904af8..17951a0e8 100644
--- a/programl/graph/format/py/cdfg.py
+++ b/programl/graph/format/py/cdfg.py
@@ -22,7 +22,7 @@
 from programl.proto import program_graph_pb2
 from programl.util.py.runfiles_path import runfiles_path
 
-GRAPH2CDFG = runfiles_path("programl/bin/graph2cdfg")
+GRAPH2CDFG = runfiles_path("bin/graph2cdfg")
 
 
 def FromProgramGraphFile(path) -> Optional[program_graph_pb2.ProgramGraph]:
diff --git a/programl/graph/format/py/graph_tuple_builder.py b/programl/graph/format/py/graph_tuple_builder.py
index a350a6402..c931c77e2 100644
--- a/programl/graph/format/py/graph_tuple_builder.py
+++ b/programl/graph/format/py/graph_tuple_builder.py
@@ -106,7 +106,7 @@ def adjacencies(self) -> np.array:
                 np.array(self._adjacencies[1], dtype=np.int32).reshape((-1, 2)),
                 np.array(self._adjacencies[2], dtype=np.int32).reshape((-1, 2)),
             ],
-            dtype=np.object,
+            dtype=object,
         )
 
     @property
@@ -117,5 +117,5 @@ def edge_positions(self) -> np.array:
                 np.array(self._edge_positions[1], dtype=np.int32),
                 np.array(self._edge_positions[2], dtype=np.int32),
             ],
-            dtype=np.object,
+            dtype=object,
         )
diff --git a/programl/graph/py/program_graph_builder.py b/programl/graph/py/program_graph_builder.py
index cb42571fb..b0c60b3f3 100644
--- a/programl/graph/py/program_graph_builder.py
+++ b/programl/graph/py/program_graph_builder.py
@@ -16,10 +16,9 @@
 """This file defines a class for constructing program graphs."""
 from typing import Optional
 
-from programl.proto.program_graph_options_pb2 import ProgramGraphOptions
-
 from programl.graph.py import program_graph_builder_pybind
 from programl.proto import program_graph_pb2
+from programl.proto.program_graph_options_pb2 import ProgramGraphOptions
 
 
 class ProgramGraphBuilder(program_graph_builder_pybind.ProgramGraphBuilder):
diff --git a/programl/ir/llvm/BUILD b/programl/ir/llvm/BUILD
index 5cf0a3aa3..5402e972f 100644
--- a/programl/ir/llvm/BUILD
+++ b/programl/ir/llvm/BUILD
@@ -39,38 +39,87 @@ cc_library(
     ],
 )
 
+filegroup(
+    name = "inst2vec_augmented_dictionary",
+    srcs = ["internal/inst2vec_augmented_dictionary.pickle"],
+    visibility = ["//visibility:public"],
+)
+
+filegroup(
+    name = "inst2vec_augmented_embeddings",
+    srcs = ["internal/inst2vec_augmented_embeddings.pickle"],
+    visibility = ["//visibility:public"],
+)
+
 py_library(
     name = "inst2vec_encoder",
     srcs = ["inst2vec_encoder.py"],
     data = [
-        "//programl/ir/llvm/internal:inst2vec_augmented_dictionary",
-        "//programl/ir/llvm/internal:inst2vec_augmented_embeddings",
+        ":inst2vec_augmented_dictionary",
+        ":inst2vec_augmented_embeddings",
     ],
     visibility = ["//visibility:public"],
     deps = [
         "//programl/graph/py:program_graph_builder",
         "//programl/proto:programl_py",
+        "//programl/third_party/ncc/inst2vec:inst2vec_preprocess",
         "//programl/util/py:decorators",
         "//programl/util/py:pbutil",
         "//programl/util/py:progress",
         "//programl/util/py:runfiles_path",
-        "//third_party/py/absl",
-        "//third_party/py/ncc/inst2vec:inst2vec_preprocess",
-        "//third_party/py/numpy",
     ],
 )
 
 cc_library(
-    name = "llvm",
-    srcs = ["llvm.cc"],
-    hdrs = ["llvm.h"],
-    copts = ["-std=c++14"],
+    name = "llvm-10",
+    srcs = ["llvm.cc"] + glob(["internal/*.cc"]),
+    hdrs = ["llvm.h"] + glob(["internal/*.h"]),
+    copts = [
+        "-std=c++14",
+        "-fno-rtti",
+    ],
+    defines = [
+        "GOOGLE_PROTOBUF_NO_RTTI",
+        "PROGRAML_LLVM_VERSION_MAJOR=10",
+    ],
     visibility = ["//visibility:public"],
     deps = [
-        "//programl/ir/llvm/internal:program_graph_builder_pass",
+        "//programl/graph:features",
+        "//programl/graph:program_graph_builder",
         "//programl/proto:programl_cc",
+        "@com_google_absl//absl/container:flat_hash_map",
+        "@com_google_absl//absl/container:flat_hash_set",
         "@labm8//labm8/cpp:status",
         "@labm8//labm8/cpp:status_macros",
+        "@labm8//labm8/cpp:statusor",
+        "@labm8//labm8/cpp:string",
         "@llvm//10.0.0",
     ],
 )
+
+cc_library(
+    name = "llvm-3.8",
+    srcs = ["llvm.cc"] + glob(["internal/*.cc"]),
+    hdrs = ["llvm.h"] + glob(["internal/*.h"]),
+    copts = [
+        "-std=c++14",
+        "-fno-rtti",
+    ],
+    defines = [
+        "GOOGLE_PROTOBUF_NO_RTTI",
+        "PROGRAML_LLVM_VERSION_MAJOR=3",
+    ],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//programl/graph:features",
+        "//programl/graph:program_graph_builder",
+        "//programl/proto:programl_cc",
+        "@com_google_absl//absl/container:flat_hash_map",
+        "@com_google_absl//absl/container:flat_hash_set",
+        "@labm8//labm8/cpp:status",
+        "@labm8//labm8/cpp:status_macros",
+        "@labm8//labm8/cpp:statusor",
+        "@labm8//labm8/cpp:string",
+        "@llvm//3.8.0",
+    ],
+)
diff --git a/programl/ir/llvm/clang.cc b/programl/ir/llvm/clang.cc
index 268fd2c51..64c00a949 100644
--- a/programl/ir/llvm/clang.cc
+++ b/programl/ir/llvm/clang.cc
@@ -55,10 +55,7 @@ Status Clang::Compile(const string& src, IrList* irs) const {
 
 std::vector<string> Clang::BuildCompileCommands(const string& baseFlags, int timeout,
                                                 bool abspath) {
-  const string clangPath =
-      (abspath ? absl::StrFormat("timeout -s9 %d %s", timeout,
-                                 labm8::BazelDataPathOrDie(kClangPath).string())
-               : "clang++");
+  const string clangPath = (abspath ? labm8::BazelDataPathOrDie(kClangPath).string() : "clang++");
 
   const std::vector<string> opts{
       "-O0",
diff --git a/programl/ir/llvm/clang.h b/programl/ir/llvm/clang.h
index 4fd8cbef6..53647a274 100644
--- a/programl/ir/llvm/clang.h
+++ b/programl/ir/llvm/clang.h
@@ -41,6 +41,7 @@ namespace llvm {
 //
 class Clang {
  public:
+  // TODO(cummins): timeout is unused!
   Clang(const string& baseFlags, int timeout = 60)
       : compileCommands_(BuildCompileCommands(baseFlags, timeout, /*abspath=*/true)),
         compileCommandsWithoutAbspath_(
diff --git a/programl/ir/llvm/inst2vec_encoder.py b/programl/ir/llvm/inst2vec_encoder.py
index 65df18fc8..5ff368540 100644
--- a/programl/ir/llvm/inst2vec_encoder.py
+++ b/programl/ir/llvm/inst2vec_encoder.py
@@ -25,15 +25,15 @@
 import numpy as np
 
 from programl.proto import node_pb2, program_graph_pb2
+from programl.third_party.ncc.inst2vec import inst2vec_preprocess
 from programl.util.py import decorators, pbutil, progress
 from programl.util.py.runfiles_path import runfiles_path
-from third_party.py.ncc.inst2vec import inst2vec_preprocess
 
 DICTIONARY = runfiles_path(
-    "programl/programl/ir/llvm/internal/inst2vec_augmented_dictionary.pickle"
+    "programl/ir/llvm/internal/inst2vec_augmented_dictionary.pickle"
 )
 AUGMENTED_INST2VEC_EMBEDDINGS = runfiles_path(
-    "programl/programl/ir/llvm/internal/inst2vec_augmented_embeddings.pickle"
+    "programl/ir/llvm/internal/inst2vec_augmented_embeddings.pickle"
 )
 
 
@@ -111,11 +111,11 @@ def Encode(
         text_index = 0
         for node in proto.node:
             if node.type == node_pb2.Node.INSTRUCTION:
-                text = preprocessed_texts[text_index].encode("utf-8")
+                text = preprocessed_texts[text_index]
                 text_index += 1
                 embedding = self.dictionary.get(text, self.dictionary["!UNK"])
                 node.features.feature["inst2vec_preprocessed"].bytes_list.value.append(
-                    text
+                    text.encode("utf-8")
                 )
                 node.features.feature["inst2vec_embedding"].int64_list.value.append(
                     embedding
diff --git a/programl/ir/llvm/internal/BUILD b/programl/ir/llvm/internal/BUILD
deleted file mode 100644
index 2a5543bfd..000000000
--- a/programl/ir/llvm/internal/BUILD
+++ /dev/null
@@ -1,79 +0,0 @@
-# This package contains implementation details for LLVM-IR support.
-#
-# Copyright 2019-2020 the ProGraML authors.
-#
-# Contact Chris Cummins <chrisc.101@gmail.com>.
-#
-# 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.
-
-filegroup(
-    name = "inst2vec_augmented_dictionary",
-    srcs = ["inst2vec_augmented_dictionary.pickle"],
-    visibility = ["//programl/ir/llvm:__subpackages__"],
-)
-
-filegroup(
-    name = "inst2vec_augmented_embeddings",
-    srcs = ["inst2vec_augmented_embeddings.pickle"],
-    visibility = ["//programl/ir/llvm:__subpackages__"],
-)
-
-cc_library(
-    name = "program_graph_builder",
-    srcs = ["program_graph_builder.cc"],
-    hdrs = ["program_graph_builder.h"],
-    copts = ["-std=c++14"],
-    deps = [
-        ":text_encoder",
-        "//programl/graph:features",
-        "//programl/graph:program_graph_builder",
-        "//programl/proto:programl_cc",
-        "@com_google_absl//absl/container:flat_hash_map",
-        "@com_google_absl//absl/container:flat_hash_set",
-        "@labm8//labm8/cpp:status_macros",
-        "@labm8//labm8/cpp:statusor",
-        "@labm8//labm8/cpp:string",
-        "@llvm//10.0.0",
-    ],
-)
-
-cc_library(
-    name = "program_graph_builder_pass",
-    srcs = ["program_graph_builder_pass.cc"],
-    hdrs = ["program_graph_builder_pass.h"],
-    copts = [
-        "-std=c++14",
-        "-fno-rtti",
-        "-DGOOGLE_PROTOBUF_NO_RTTI",
-    ],
-    visibility = ["//programl/ir/llvm:__subpackages__"],
-    deps = [
-        ":program_graph_builder",
-        "//programl/proto:programl_cc",
-        "@labm8//labm8/cpp:statusor",
-        "@llvm//10.0.0",
-    ],
-)
-
-cc_library(
-    name = "text_encoder",
-    srcs = ["text_encoder.cc"],
-    hdrs = ["text_encoder.h"],
-    copts = ["-std=c++14"],
-    deps = [
-        "@com_google_absl//absl/container:flat_hash_map",
-        "@labm8//labm8/cpp:logging",
-        "@labm8//labm8/cpp:string",
-        "@llvm//10.0.0",
-    ],
-)
diff --git a/programl/ir/llvm/internal/program_graph_builder.cc b/programl/ir/llvm/internal/program_graph_builder.cc
index b826afb33..ba0a50c78 100644
--- a/programl/ir/llvm/internal/program_graph_builder.cc
+++ b/programl/ir/llvm/internal/program_graph_builder.cc
@@ -27,7 +27,9 @@
 #include "llvm/IR/Function.h"
 #include "llvm/IR/Instruction.h"
 #include "llvm/IR/Instructions.h"
+#if PROGRAML_LLVM_VERSION_MAJOR > 3
 #include "llvm/IR/ProfileSummary.h"
+#endif
 #include "programl/graph/features.h"
 #include "programl/ir/llvm/internal/text_encoder.h"
 #include "programl/proto/program_graph.pb.h"
@@ -307,6 +309,7 @@ Node* ProgramGraphBuilder::AddLlvmInstruction(const ::llvm::Instruction* instruc
   node->set_block(blockCount_);
   graph::AddScalarFeature(node, "full_text", text.text);
 
+#if PROGRAML_LLVM_VERSION_MAJOR > 3
   // Add profiling information features, if available.
   uint64_t profTotalWeight;
   if (instruction->extractProfTotalWeight(profTotalWeight)) {
@@ -318,6 +321,7 @@ Node* ProgramGraphBuilder::AddLlvmInstruction(const ::llvm::Instruction* instruc
     graph::AddScalarFeature(node, "llvm_profile_true_weight", profTrueWeight);
     graph::AddScalarFeature(node, "llvm_profile_false_weight", profFalseWeight);
   }
+#endif
 
   return node;
 }
@@ -351,11 +355,23 @@ Node* ProgramGraphBuilder::AddLlvmConstant(const ::llvm::Constant* constant) {
   return node;
 }
 
+namespace {
+
+std::string getModuleName(const ::llvm::Module& module) {
+#if PROGRAML_LLVM_VERSION_MAJOR == 3
+  return module.getModuleIdentifier();
+#else
+  return module.getSourceFileName();
+#endif
+}
+
+}  // anonymous namespace
+
 labm8::StatusOr<ProgramGraph> ProgramGraphBuilder::Build(const ::llvm::Module& module) {
   // A map from functions to their entry and exit nodes.
   absl::flat_hash_map<const ::llvm::Function*, FunctionEntryExits> functions;
 
-  Module* moduleMessage = AddModule(module.getSourceFileName());
+  Module* moduleMessage = AddModule(getModuleName(module));
 
   graph::AddScalarFeature(moduleMessage, "llvm_target_triple", module.getTargetTriple());
   graph::AddScalarFeature(moduleMessage, "llvm_data_layout", module.getDataLayoutStr());
@@ -364,6 +380,7 @@ labm8::StatusOr<ProgramGraph> ProgramGraphBuilder::Build(const ::llvm::Module& m
     // Create the function message.
     Function* functionMessage = AddFunction(function.getName(), moduleMessage);
 
+#if PROGRAML_LLVM_VERSION_MAJOR > 3
     // Add profiling information, if available.
     if (function.hasProfileData()) {
       auto profileCount = function.getEntryCount();
@@ -372,6 +389,7 @@ labm8::StatusOr<ProgramGraph> ProgramGraphBuilder::Build(const ::llvm::Module& m
       functionMessage->mutable_features()->mutable_feature()->insert(
           {"llvm_profile_entry_count", feature});
     }
+#endif
 
     FunctionEntryExits functionEntryExits;
     ASSIGN_OR_RETURN(functionEntryExits, VisitFunction(function, functionMessage));
@@ -405,6 +423,7 @@ labm8::StatusOr<ProgramGraph> ProgramGraphBuilder::Build(const ::llvm::Module& m
     }
   }
 
+#if PROGRAML_LLVM_VERSION_MAJOR > 3
   // Add profiling information, if available.
   ::llvm::Metadata* profileMetadata = module.getModuleFlag("ProfileSummary");
   if (profileMetadata) {
@@ -424,6 +443,7 @@ labm8::StatusOr<ProgramGraph> ProgramGraphBuilder::Build(const ::llvm::Module& m
     graph::AddScalarFeature(moduleMessage, "llvm_profile_max_internal_count",
                             profileSummary->getMaxInternalCount());
   }
+#endif
 
   return programl::graph::ProgramGraphBuilder::Build();
 }
diff --git a/programl/ir/llvm/internal/text_encoder.cc b/programl/ir/llvm/internal/text_encoder.cc
index 2ed9882f0..17cf4f081 100644
--- a/programl/ir/llvm/internal/text_encoder.cc
+++ b/programl/ir/llvm/internal/text_encoder.cc
@@ -19,6 +19,7 @@
 #include <utility>
 
 #include "labm8/cpp/logging.h"
+#include "llvm/IR/DerivedTypes.h"
 #include "llvm/IR/Type.h"
 #include "llvm/Support/raw_ostream.h"
 
diff --git a/programl/ir/llvm/llvm.h b/programl/ir/llvm/llvm.h
index adb7007bc..a3ba2625e 100644
--- a/programl/ir/llvm/llvm.h
+++ b/programl/ir/llvm/llvm.h
@@ -22,6 +22,7 @@
 
 #include "labm8/cpp/status.h"
 #include "llvm/IR/Module.h"
+#include "llvm/Support/MemoryBuffer.h"
 #include "programl/proto/program_graph.pb.h"
 #include "programl/proto/program_graph_options.pb.h"
 
diff --git a/programl/ir/llvm/py/BUILD b/programl/ir/llvm/py/BUILD
index d19989158..ccf61dc7c 100644
--- a/programl/ir/llvm/py/BUILD
+++ b/programl/ir/llvm/py/BUILD
@@ -24,20 +24,64 @@ load(
 py_library(
     name = "llvm",
     srcs = ["llvm.py"],
-    data = [":graph_builder_bin"],
+    data = [":graph_builder_bin-10-files"],
     visibility = ["//visibility:public"],
     deps = [
         "//programl/proto:programl_py",
         "//programl/util/py:runfiles_path",
-        "//third_party/py/absl",
     ],
 )
 
+# This target includes the service binary and its runtime dependencies and
+# libraries.
+filegroup(
+    name = "graph_builder_bin-10-files",
+    srcs = [
+        ":graph_builder_bin-10",
+    ] + select({
+        "@llvm//:darwin": [],
+        "//conditions:default": [":libLLVMPolly"],
+    }),
+    visibility = ["//visibility:public"],
+)
+
+# Post-build shenanigans to strip the absolute path to LLVMPolly.so for linux
+# builds.
+genrule(
+    name = "graph_builder_bin-10-bin",
+    srcs = [":graph_builder_bin-10-prelinked"],
+    outs = ["graph_builder_bin-10"],
+    cmd = select({
+        "@llvm//:darwin": (
+            "cp $(location :graph_builder_bin-10-prelinked) $@"
+        ),
+        "//conditions:default": (
+            "cp $(location :graph_builder_bin-10-prelinked) $@ && " +
+            "chmod 666 $@ && " +
+            "patchelf --set-rpath '$$ORIGIN' $@ && " +
+            "chmod 555 $@"
+        ),
+    }),
+)
+
+# This target copies the LLVMPolly.so file from LLVM to libLLVMPolly.so.
+genrule(
+    name = "libLLVMPolly",
+    srcs = [
+        "@clang-llvm-10.0.0-x86_64-linux-gnu-ubuntu-18.04//:all_files",
+        "@clang-llvm-10.0.0-x86_64-linux-gnu-ubuntu-18.04//:clang",
+    ],
+    outs = ["libLLVMPolly.so"],
+    cmd = "cp $$(dirname $(location @clang-llvm-10.0.0-x86_64-linux-gnu-ubuntu-18.04//:clang))/../lib/LLVMPolly.so $@",
+    visibility = ["//visibility:public"],
+)
+
 cc_binary(
-    name = "graph_builder_bin",
+    name = "graph_builder_bin-10-prelinked",
     srcs = ["graph_builder_bin.cc"],
+    copts = ["-std=c++14"],
     deps = [
-        "//programl/ir/llvm",
+        "//programl/ir/llvm:llvm-10",
         "//programl/proto:programl_cc",
         "@labm8//labm8/cpp:status",
         "@llvm//10.0.0",
diff --git a/programl/ir/llvm/py/llvm.py b/programl/ir/llvm/py/llvm.py
index 941141bb5..66062b031 100644
--- a/programl/ir/llvm/py/llvm.py
+++ b/programl/ir/llvm/py/llvm.py
@@ -20,7 +20,7 @@
 from programl.proto import program_graph_options_pb2, program_graph_pb2
 from programl.util.py.runfiles_path import runfiles_path
 
-GRAPH_BUILDER_BIN = runfiles_path("programl/programl/ir/llvm/py/graph_builder_bin")
+GRAPH_BUILDER_BIN = runfiles_path("programl/ir/llvm/py/graph_builder_bin-10")
 
 DefaultOptions = program_graph_options_pb2.ProgramGraphOptions()
 
@@ -53,18 +53,25 @@ def BuildProgramGraph(
         f.flush()
         options.ir_path = f.name
         process = subprocess.Popen(
-            ["timeout", "-s9", str(timeout), str(GRAPH_BUILDER_BIN)],
+            [str(GRAPH_BUILDER_BIN)],
             stdout=subprocess.PIPE,
             stderr=subprocess.PIPE,
             stdin=subprocess.PIPE,
         )
-        stdout, stderr = process.communicate(options.SerializeToString())
+        try:
+            stdout, stderr = process.communicate(
+                options.SerializeToString(),
+                timeout=timeout,
+            )
+        except subprocess.TimeoutExpired as e:
+            # Catch and re-raise timeout error using standard exception type.
+            raise TimeoutError(
+                f"Program graph construction exceeded {timeout} seconds"
+            ) from e
 
     proto = program_graph_pb2.ProgramGraph()
     if process.returncode == 2:
         raise ValueError(stderr.decode("utf-8").rstrip())
-    elif process.returncode == 9 or process.returncode == -9:
-        raise TimeoutError(f"Program graph construction exceeded {timeout} seconds")
     elif process.returncode:
         raise OSError(stderr.decode("utf-8").rstrip())
     proto.ParseFromString(stdout)
diff --git a/programl/ir/xla/BUILD b/programl/ir/xla/BUILD
index 2b97765ed..df1280f90 100644
--- a/programl/ir/xla/BUILD
+++ b/programl/ir/xla/BUILD
@@ -25,7 +25,7 @@ cc_library(
         ":xla_stringifier",
         "//programl/graph:program_graph_builder",
         "//programl/proto:programl_cc",
-        "//third_party/tensorflow:xla_cc",
+        "//programl/third_party/tensorflow:xla_cc",
         "@com_google_absl//absl/container:flat_hash_map",
         "@labm8//labm8/cpp:logging",
         "@labm8//labm8/cpp:port",
@@ -41,7 +41,7 @@ cc_library(
     hdrs = ["hlo_proto_reader.h"],
     visibility = ["//tests:__subpackages__"],
     deps = [
-        "//third_party/tensorflow:xla_cc",
+        "//programl/third_party/tensorflow:xla_cc",
         "@labm8//labm8/cpp:status_macros",
         "@labm8//labm8/cpp:statusor",
         "@labm8//labm8/cpp:string",
@@ -53,7 +53,7 @@ cc_library(
     srcs = ["xla_stringifier.cc"],
     hdrs = ["xla_stringifier.h"],
     deps = [
-        "//third_party/tensorflow:xla_cc",
+        "//programl/third_party/tensorflow:xla_cc",
         "@labm8//labm8/cpp:string",
     ],
 )
diff --git a/programl/ir/xla/hlo_module_graph_builder.cc b/programl/ir/xla/hlo_module_graph_builder.cc
index 8e5552f64..cbcc1edcb 100644
--- a/programl/ir/xla/hlo_module_graph_builder.cc
+++ b/programl/ir/xla/hlo_module_graph_builder.cc
@@ -21,7 +21,7 @@
 #include "labm8/cpp/status_macros.h"
 #include "labm8/cpp/string.h"
 #include "programl/ir/xla/xla_stringifier.h"
-#include "third_party/tensorflow/xla.pb.h"
+#include "programl/third_party/tensorflow/xla.pb.h"
 
 namespace programl {
 namespace ir {
diff --git a/programl/ir/xla/hlo_module_graph_builder.h b/programl/ir/xla/hlo_module_graph_builder.h
index bbf12d530..5726b252c 100644
--- a/programl/ir/xla/hlo_module_graph_builder.h
+++ b/programl/ir/xla/hlo_module_graph_builder.h
@@ -25,7 +25,7 @@
 #include "labm8/cpp/statusor.h"
 #include "programl/graph/program_graph_builder.h"
 #include "programl/proto/program_graph.pb.h"
-#include "third_party/tensorflow/xla.pb.h"
+#include "programl/third_party/tensorflow/xla.pb.h"
 
 namespace programl {
 namespace ir {
diff --git a/programl/ir/xla/hlo_proto_reader.h b/programl/ir/xla/hlo_proto_reader.h
index 9ce73553b..761fd804b 100644
--- a/programl/ir/xla/hlo_proto_reader.h
+++ b/programl/ir/xla/hlo_proto_reader.h
@@ -21,7 +21,7 @@
 
 #include "labm8/cpp/statusor.h"
 #include "labm8/cpp/string.h"
-#include "third_party/tensorflow/xla.pb.h"
+#include "programl/third_party/tensorflow/xla.pb.h"
 
 namespace programl {
 namespace ir {
diff --git a/programl/ir/xla/py/BUILD b/programl/ir/xla/py/BUILD
index 328bdcad8..d9472e09d 100644
--- a/programl/ir/xla/py/BUILD
+++ b/programl/ir/xla/py/BUILD
@@ -28,8 +28,7 @@ py_library(
     visibility = ["//visibility:public"],
     deps = [
         "//programl/proto:programl_py",
-        "//third_party/py/networkx",
-        "//third_party/tensorflow:xla_py",
+        "//programl/third_party/tensorflow:xla_py",
     ],
 )
 
@@ -38,7 +37,7 @@ pybind_extension(
     srcs = ["xla_pybind.cc"],
     deps = [
         "//programl/ir/xla:hlo_module_graph_builder",
-        "//third_party/tensorflow:xla_cc",
+        "//programl/third_party/tensorflow:xla_cc",
         "@labm8//labm8/cpp:statusor",
         "@labm8//labm8/cpp:string",
     ],
diff --git a/programl/ir/xla/py/xla.py b/programl/ir/xla/py/xla.py
index 3884f2f3d..25901de7b 100644
--- a/programl/ir/xla/py/xla.py
+++ b/programl/ir/xla/py/xla.py
@@ -16,7 +16,7 @@
 """Generate program graphs from XLA HLO modules."""
 from programl.ir.xla.py import xla_pybind
 from programl.proto import program_graph_pb2
-from third_party.tensorflow import xla_pb2
+from programl.third_party.tensorflow import xla_pb2
 
 
 def BuildProgramGraphProto(
diff --git a/programl/ir/xla/xla_stringifier.h b/programl/ir/xla/xla_stringifier.h
index d91cbee5f..6bc206a61 100644
--- a/programl/ir/xla/xla_stringifier.h
+++ b/programl/ir/xla/xla_stringifier.h
@@ -18,7 +18,7 @@
 #pragma once
 
 #include "labm8/cpp/string.h"
-#include "third_party/tensorflow/xla.pb.h"
+#include "programl/third_party/tensorflow/xla.pb.h"
 
 namespace programl {
 namespace ir {
diff --git a/programl/requirements.txt b/programl/requirements.txt
index b1cd6bdae..eff45900f 100644
--- a/programl/requirements.txt
+++ b/programl/requirements.txt
@@ -1,13 +1,8 @@
-absl-py==0.11.0
-decorator==4.3.0
+absl-py>=0.11.0
+decorator>=4.3.0
 grpcio==1.33.2
-joblib>=0.16.0  # Needed by scikit-learn
 networkx>=2.4
-numpy==1.19.3
+numpy>=1.19.3
 protobuf==3.13.0
-PyYAML==4.2b4
-scikit-learn>=0.23.2
-scipy>=1.5.2
-six==1.15.0  # Needed by absl-py
-torch==1.7.0
-tqdm==4.38.0
+PyYAML>=4.2b4
+tqdm>=4.38.0
diff --git a/third_party/py/ncc/BUILD b/programl/third_party/ncc/BUILD
similarity index 82%
rename from third_party/py/ncc/BUILD
rename to programl/third_party/ncc/BUILD
index 9e2c59253..aad5622e8 100644
--- a/third_party/py/ncc/BUILD
+++ b/programl/third_party/ncc/BUILD
@@ -27,11 +27,11 @@ py_library(
 py_library(
     name = "vocabulary",
     srcs = ["vocabulary.py"],
-    data = ["//third_party/py/ncc/published_results:vocabulary"],
+    data = ["//programl/third_party/ncc/published_results:vocabulary"],
     visibility = ["//visibility:public"],
     deps = [
         ":rgx_utils",
-        "//third_party/py/absl",
-        "//third_party/py/ncc/inst2vec:inst2vec_preprocess",
+        "//programl/third_party/ncc/inst2vec:inst2vec_preprocess",
+        "//programl/util/py:runfiles_path",
     ],
 )
diff --git a/third_party/py/ncc/LICENSE b/programl/third_party/ncc/LICENSE
similarity index 100%
rename from third_party/py/ncc/LICENSE
rename to programl/third_party/ncc/LICENSE
diff --git a/third_party/py/ncc/README.md b/programl/third_party/ncc/README.md
similarity index 100%
rename from third_party/py/ncc/README.md
rename to programl/third_party/ncc/README.md
diff --git a/third_party/py/ncc/inst2vec/BUILD b/programl/third_party/ncc/inst2vec/BUILD
similarity index 78%
rename from third_party/py/ncc/inst2vec/BUILD
rename to programl/third_party/ncc/inst2vec/BUILD
index 0de7cbf42..386355ffc 100644
--- a/third_party/py/ncc/inst2vec/BUILD
+++ b/programl/third_party/ncc/inst2vec/BUILD
@@ -6,8 +6,7 @@ py_library(
     visibility = ["//visibility:public"],
     deps = [
         ":inst2vec_utils",
-        "//third_party/py/ncc:rgx_utils",
-        "//third_party/py/networkx",
+        "//programl/third_party/ncc:rgx_utils",
     ],
 )
 
diff --git a/third_party/py/ncc/inst2vec/inst2vec_preprocess.py b/programl/third_party/ncc/inst2vec/inst2vec_preprocess.py
similarity index 99%
rename from third_party/py/ncc/inst2vec/inst2vec_preprocess.py
rename to programl/third_party/ncc/inst2vec/inst2vec_preprocess.py
index fac1ba14e..a8f7d59a4 100644
--- a/third_party/py/ncc/inst2vec/inst2vec_preprocess.py
+++ b/programl/third_party/ncc/inst2vec/inst2vec_preprocess.py
@@ -20,6 +20,7 @@
 # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 # ==============================================================================
+# flake8: noqa
 """Preprocess LLVM IR code to XFG for inst2vec training"""
 import copy
 import os
@@ -29,8 +30,8 @@
 
 import networkx as nx
 
-from third_party.py.ncc import rgx_utils as rgx
-from third_party.py.ncc.inst2vec import inst2vec_utils as i2v_utils
+from programl.third_party.ncc import rgx_utils as rgx
+from programl.third_party.ncc.inst2vec import inst2vec_utils as i2v_utils
 
 
 ########################################################################################################################
diff --git a/third_party/py/ncc/inst2vec/inst2vec_utils.py b/programl/third_party/ncc/inst2vec/inst2vec_utils.py
similarity index 100%
rename from third_party/py/ncc/inst2vec/inst2vec_utils.py
rename to programl/third_party/ncc/inst2vec/inst2vec_utils.py
diff --git a/third_party/py/ncc/published_results/BUILD b/programl/third_party/ncc/published_results/BUILD
similarity index 100%
rename from third_party/py/ncc/published_results/BUILD
rename to programl/third_party/ncc/published_results/BUILD
diff --git a/third_party/py/ncc/published_results/README.md b/programl/third_party/ncc/published_results/README.md
similarity index 100%
rename from third_party/py/ncc/published_results/README.md
rename to programl/third_party/ncc/published_results/README.md
diff --git a/third_party/py/ncc/published_results/vocabulary.zip b/programl/third_party/ncc/published_results/vocabulary.zip
similarity index 100%
rename from third_party/py/ncc/published_results/vocabulary.zip
rename to programl/third_party/ncc/published_results/vocabulary.zip
diff --git a/third_party/py/ncc/rgx_utils.py b/programl/third_party/ncc/rgx_utils.py
similarity index 99%
rename from third_party/py/ncc/rgx_utils.py
rename to programl/third_party/ncc/rgx_utils.py
index 128e5b294..df8f4c58d 100644
--- a/third_party/py/ncc/rgx_utils.py
+++ b/programl/third_party/ncc/rgx_utils.py
@@ -20,6 +20,7 @@
 # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 # ==============================================================================
+# flake8: noqa
 """Helper variables and functions for regular expressions and statement tags"""
 import re
 
diff --git a/third_party/py/ncc/vocabulary.py b/programl/third_party/ncc/vocabulary.py
similarity index 94%
rename from third_party/py/ncc/vocabulary.py
rename to programl/third_party/ncc/vocabulary.py
index 31dec795d..ea2a111bf 100644
--- a/third_party/py/ncc/vocabulary.py
+++ b/programl/third_party/ncc/vocabulary.py
@@ -7,15 +7,16 @@
 import typing
 import zipfile
 
-from absl import bazelutil, decorators, flags
+from absl import decorators, flags
 
-from third_party.py.ncc import rgx_utils
-from third_party.py.ncc.inst2vec import inst2vec_preprocess as i2v_prep
+from programl.third_party.ncc import rgx_utils
+from programl.third_party.ncc.inst2vec import inst2vec_preprocess as i2v_prep
+from programl.util.py.runfiles_path import runfiles_path
 
 FLAGS = flags.FLAGS
 
 PUBLISHED_VOCABULARY = runfiles_path(
-    "programl/third_party/py/ncc/published_results/vocabulary.zip"
+    "programl/programl/third_party/ncc/published_results/vocabulary.zip"
 )
 
 
@@ -96,7 +97,9 @@ def GetStructDict(bytecode_lines: typing.List[str]) -> typing.Dict[str, str]:
     # If the dictionary is empty
     if not struct_dict:
         for line in bytecode_lines:
-            if re.match(rgx_utils.struct_name + " = type (<?\{ .* \}|opaque|{})", line):
+            if re.match(
+                rgx_utils.struct_name + r" = type (<?\{ .* \}|opaque|{})", line
+            ):
                 # "Structures' dictionary is empty for file containing type definitions"
                 # + data[0] + '\n' + data[1] + '\n' + data + '\n'
                 assert False
diff --git a/third_party/tensorflow/BUILD b/programl/third_party/tensorflow/BUILD
similarity index 100%
rename from third_party/tensorflow/BUILD
rename to programl/third_party/tensorflow/BUILD
diff --git a/third_party/tensorflow/LICENSE b/programl/third_party/tensorflow/LICENSE
similarity index 100%
rename from third_party/tensorflow/LICENSE
rename to programl/third_party/tensorflow/LICENSE
diff --git a/third_party/tensorflow/compiler/xla/service/hlo.proto b/programl/third_party/tensorflow/compiler/xla/service/hlo.proto
similarity index 99%
rename from third_party/tensorflow/compiler/xla/service/hlo.proto
rename to programl/third_party/tensorflow/compiler/xla/service/hlo.proto
index fa07e7ec3..9c707eac7 100644
--- a/third_party/tensorflow/compiler/xla/service/hlo.proto
+++ b/programl/third_party/tensorflow/compiler/xla/service/hlo.proto
@@ -36,7 +36,7 @@ option java_multiple_files = true;
 option java_outer_classname = "HloProto";
 option java_package = "com.xla";
 
-import "third_party/tensorflow/compiler/xla/xla_data.proto";
+import "programl/third_party/tensorflow/compiler/xla/xla_data.proto";
 
 // Serialization of HloInstruction.
 // Next ID: 68
diff --git a/third_party/tensorflow/compiler/xla/xla_data.proto b/programl/third_party/tensorflow/compiler/xla/xla_data.proto
similarity index 100%
rename from third_party/tensorflow/compiler/xla/xla_data.proto
rename to programl/third_party/tensorflow/compiler/xla/xla_data.proto
diff --git a/third_party/tensorflow/xla.proto b/programl/third_party/tensorflow/xla.proto
similarity index 100%
rename from third_party/tensorflow/xla.proto
rename to programl/third_party/tensorflow/xla.proto
diff --git a/programl/util/py/BUILD b/programl/util/py/BUILD
index add3715aa..189713c52 100644
--- a/programl/util/py/BUILD
+++ b/programl/util/py/BUILD
@@ -16,6 +16,22 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+py_library(
+    name = "util",
+    srcs = [
+        "decorators.py",
+        "humanize.py",
+        "init_app.py",
+        "pbutil.py",
+        "progress.py",
+        "runfiles_path.py",
+        "stdin_fmt.py",
+        "stdout_fmt.py",
+        "threaded_iterator.py",
+    ],
+    visibility = ["//visibility:public"],
+)
+
 py_library(
     name = "decorators",
     srcs = ["decorators.py"],
@@ -34,7 +50,6 @@ py_library(
     visibility = ["//visibility:public"],
     deps = [
         "//programl",
-        "//third_party/py/absl",
     ],
 )
 
@@ -43,7 +58,6 @@ py_library(
     srcs = ["pbutil.py"],
     visibility = ["//visibility:public"],
     deps = [
-        "//third_party/py/protobuf",
     ],
 )
 
@@ -53,8 +67,6 @@ py_library(
     visibility = ["//visibility:public"],
     deps = [
         ":humanize",
-        "//third_party/py/absl",
-        "//third_party/py/tqdm",
     ],
 )
 
@@ -73,8 +85,6 @@ py_library(
     visibility = ["//visibility:public"],
     deps = [
         "//programl/util/py:pbutil",
-        "//third_party/py/absl",
-        "//third_party/py/protobuf",
     ],
 )
 
@@ -83,8 +93,6 @@ py_library(
     srcs = ["stdout_fmt.py"],
     visibility = ["//visibility:public"],
     deps = [
-        "//third_party/py/absl",
-        "//third_party/py/protobuf",
     ],
 )
 
diff --git a/programl/util/py/pbutil.py b/programl/util/py/pbutil.py
index fa16c74c5..def354968 100644
--- a/programl/util/py/pbutil.py
+++ b/programl/util/py/pbutil.py
@@ -31,6 +31,9 @@
 # A type alias for protocol enum fields.
 Enum = int
 
+# A type alias for JSON dictionaries.
+JSON = typing.Union[typing.List[typing.Any], typing.Dict[str, typing.Any]]
+
 
 class ProtoValueError(ValueError):
     """Raised in case of a value error from a proto."""
@@ -95,7 +98,7 @@ def FromString(
         raise DecodeError(e)
 
     if not uninitialized_okay and not message.IsInitialized():
-        raise DecodeError(f"Required fields not set")
+        raise DecodeError("Required fields not set")
 
     return message
 
@@ -257,7 +260,7 @@ def ToFile(
     return message
 
 
-def ToJson(message: ProtocolBuffer) -> "jsonutil.JSON":
+def ToJson(message: ProtocolBuffer) -> JSON:
     """Return a JSON encoded representation of a protocol buffer.
     Args:
       message: The message to convert to JSON.
@@ -285,9 +288,9 @@ def _TruncatedString(string: str, n: int = 80) -> str:
 
 
 def _TruncateDictionaryStringValues(
-    data: "jsonutil.JSON",
+    data: JSON,
     n: int = 62,
-) -> "jsonutil.JSON":
+) -> JSON:
     """Truncate all string values in a nested dictionary.
     Args:
       data: A dictionary.
@@ -358,7 +361,7 @@ def ProtoIsReadable(
     try:
         FromFile(pathlib.Path(path), message)
         return True
-    except:
+    except:  # noqa
         return False
 
 
@@ -447,21 +450,24 @@ def RunProcessMessage(
     """
     # Run the C++ worker process, capturing it's output.
     process = subprocess.Popen(
-        ["timeout", "-s9", str(timeout_seconds)] + cmd,
+        cmd,
         stdin=subprocess.PIPE,
         stdout=subprocess.PIPE,
         env=env,
     )
     # Send the input proto to the C++ worker process.
-    stdout, _ = process.communicate(input_proto.SerializeToString())
-
-    if process.returncode == -9 or process.returncode == 9:
+    try:
+        stdout, _ = process.communicate(
+            input_proto.SerializeToString(), timeout=timeout_seconds
+        )
+    except subprocess.TimeoutExpired as e:
         raise ProtoWorkerTimeoutError(
             cmd=cmd,
             timeout_seconds=timeout_seconds,
             returncode=process.returncode,
-        )
-    elif process.returncode:
+        ) from e
+
+    if process.returncode:
         raise subprocess.CalledProcessError(process.returncode, cmd)
 
     return stdout
diff --git a/programl/util/py/runfiles_path.py b/programl/util/py/runfiles_path.py
index 59d735a2e..ce2a2779d 100644
--- a/programl/util/py/runfiles_path.py
+++ b/programl/util/py/runfiles_path.py
@@ -2,6 +2,13 @@
 import os
 from pathlib import Path
 
+# NOTE(cummins): Moving this file may require updating this relative path.
+_PACKAGE_ROOT = Path(os.path.join(os.path.dirname(__file__), "../../../")).resolve(
+    strict=True
+)
+
+_TESTS_ROOT = Path("/tmp/programl/install_tests")
+
 
 def runfiles_path(relpath: str) -> Path:
     """Resolve the path to a runfiles data path.
@@ -13,8 +20,16 @@ def runfiles_path(relpath: str) -> Path:
     if runfiles_path:
         return Path(runfiles_path) / relpath
     else:
-        # Defer importing this module so that if we have set
-        # $COMPILER_GYM_RUNFILES we do not need any bazel dependencies.
-        from rules_python.python.runfiles import runfiles
+        try:
+            from rules_python.python.runfiles import runfiles
 
-        return Path(runfiles.Create().Rlocation(relpath))
+            return Path(
+                runfiles.Create().Rlocation(
+                    "programl" if relpath == "." else f"programl/{relpath}"
+                )
+            )
+        except (ModuleNotFoundError, TypeError):
+            # Special handler for paths to test data.
+            if relpath.startswith("tests/"):
+                return _TESTS_ROOT / relpath
+            return _PACKAGE_ROOT / relpath
diff --git a/programl/util/py/stdout_fmt.py b/programl/util/py/stdout_fmt.py
index 6175b2d86..197d64db9 100644
--- a/programl/util/py/stdout_fmt.py
+++ b/programl/util/py/stdout_fmt.py
@@ -17,7 +17,7 @@
 import sys
 
 import google.protobuf.json_format
-from absl import flags
+from absl import app, flags
 
 flags.DEFINE_string(
     "stdout_fmt",
diff --git a/requirements.txt b/requirements.txt
index 88d8d5bc0..f61cbe561 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,6 @@
 -r programl/requirements.txt
+-r models/requirements.txt
 -r tasks/requirements.txt
+-r tests/requirements.txt
 -r tools/perf_monitor/requirements.txt
 -r tools/requirements.pre_commit.txt
diff --git a/setup.py b/setup.py
new file mode 100644
index 000000000..0f7795a94
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python3
+#
+# Copyright 2019-2020 the ProGraML authors.
+#
+# Contact Chris Cummins <chrisc.101@gmail.com>.
+#
+# 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.
+
+import distutils.util
+import io
+
+import setuptools
+
+with io.open("version.txt", encoding="utf-8") as f:
+    version = f.read().strip()
+with open("README.md") as f:
+    with io.open("README.md", encoding="utf-8") as f:
+        long_description = f.read()
+with open("programl/requirements.txt") as f:
+    requirements = [ln.split("#")[0].rstrip() for ln in f.readlines()]
+
+# When building a bdist_wheel we need to set the appropriate tags: this package
+# includes compiled binaries, and does not include compiled python extensions.
+try:
+    from wheel.bdist_wheel import bdist_wheel as _bdist_wheel
+
+    class bdist_wheel(_bdist_wheel):
+        def finalize_options(self):
+            _bdist_wheel.finalize_options(self)
+            self.root_is_pure = False
+
+        def get_tag(self):
+            python, abi, plat = _bdist_wheel.get_tag(self)
+            python, abi = "py3", "none"
+            return python, abi, plat
+
+
+except ImportError:
+    bdist_wheel = None
+
+setuptools.setup(
+    name="programl",
+    version=version,
+    description="A Graph-based Program Representation for Data Flow Analysis and Compiler Optimizations",
+    author="Chris Cummins",
+    long_description=long_description,
+    long_description_content_type="text/markdown",
+    url="https://github.com/ChrisCummins/ProGraML",
+    license="Apache 2.0",
+    packages=[
+        "programl",
+        "programl.graph.py",
+        "programl.graph.format.py",
+        "programl.graph.analysis.py",
+        "programl.ir.llvm",
+        "programl.third_party.ncc",
+        "programl.third_party.ncc.inst2vec",
+        "programl.third_party.tensorflow",
+        "programl.ir.llvm.py",
+        "programl.ir.xla.py",
+        "programl.util.py",
+        "programl.proto",
+    ],
+    package_dir={
+        "": "bazel-bin/py_package.runfiles/programl",
+    },
+    package_data={
+        "programl": [
+            "bin/llvm2graph",
+            "ir/llvm/py/graph_builder_bin-10",
+            "ir/llvm/py/*.so",
+            "ir/llvm/internal/*.pickle",
+            "ir/xla/py/*.so",
+            "graph/py/*.so",
+            "graph/analysis/py/*.so",
+            "graph/format/py/*.so",
+        ]
+    },
+    install_requires=requirements,
+    include_package_data=True,
+    python_requires=">=3.6",
+    classifiers=[
+        "Development Status :: 3 - Alpha",
+        "Environment :: Console",
+        "Intended Audience :: Developers",
+        "Intended Audience :: Science/Research",
+        "License :: OSI Approved :: Apache Software License",
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3",
+        "Topic :: Scientific/Engineering :: Artificial Intelligence",
+        "Topic :: Software Development :: Compilers",
+    ],
+    cmdclass={"bdist_wheel": bdist_wheel},
+    platforms=[distutils.util.get_platform()],
+    zip_safe=False,
+)
diff --git a/tasks/classifyapp/dataset/BUILD b/tasks/classifyapp/dataset/BUILD
index 123e837a8..61ae996c2 100644
--- a/tasks/classifyapp/dataset/BUILD
+++ b/tasks/classifyapp/dataset/BUILD
@@ -31,8 +31,8 @@ cc_library(
     srcs = ["poj104.cc"],
     hdrs = ["poj104.h"],
     deps = [
-        "//programl/ir/llvm",
         "//programl/ir/llvm:clang",
+        "//programl/ir/llvm:llvm-10",
         "//programl/proto:programl_cc",
         "//programl/util:filesystem_cache",
         "//third_party/tbb",
diff --git a/tasks/dataflow/BUILD b/tasks/dataflow/BUILD
index 9805c83f4..51545309d 100644
--- a/tasks/dataflow/BUILD
+++ b/tasks/dataflow/BUILD
@@ -23,13 +23,12 @@ py_library(
         ":dataflow",
         ":ggnn_batch_builder",
         ":graph_loader",
-        "//programl/models:async_batch_builder",
-        "//programl/models:epoch_batch_iterator",
-        "//programl/models/ggnn",
+        "//models:async_batch_builder",
+        "//models:epoch_batch_iterator",
+        "//models/ggnn",
         "//programl/proto:programl_py",
         "//programl/util/py:humanize",
         "//programl/util/py:pbutil",
-        "//third_party/py/numpy",
     ],
 )
 
@@ -38,12 +37,11 @@ py_library(
     srcs = ["ggnn_batch_builder.py"],
     visibility = ["//visibility:public"],
     deps = [
+        "//models:base_batch_builder",
+        "//models:base_graph_loader",
+        "//models:batch_data",
+        "//models/ggnn:ggnn_batch",
         "//programl/graph/format/py:graph_tuple_builder",
-        "//programl/models:base_batch_builder",
-        "//programl/models:base_graph_loader",
-        "//programl/models:batch_data",
-        "//programl/models/ggnn:ggnn_batch",
-        "//third_party/py/numpy",
     ],
 )
 
@@ -53,12 +51,10 @@ py_library(
     visibility = ["//visibility:public"],
     deps = [
         ":graph_loader",
-        "//programl/models:async_batch_builder",
-        "//programl/models/ggnn",
+        "//models:async_batch_builder",
+        "//models/ggnn",
         "//programl/proto:programl_py",
         "//programl/util/py:pbutil",
-        "//third_party/py/absl",
-        "//third_party/py/numpy",
     ],
 )
 
@@ -70,8 +66,6 @@ py_binary(
         "//programl/util/py:init_app",
         "//programl/util/py:pbutil",
         "//programl/util/py:progress",
-        "//third_party/py/absl",
-        "//third_party/py/pandas",
     ],
 )
 
@@ -79,9 +73,9 @@ py_binary(
     name = "ggnn_test_one",
     srcs = ["ggnn_test_one.py"],
     deps = [
-        "//programl/models:base_graph_loader",
-        "//programl/models:batch_results",
-        "//programl/models/ggnn",
+        "//models:base_graph_loader",
+        "//models:batch_results",
+        "//models/ggnn",
         "//programl/proto:programl_py",
         "//programl/util/py:init_app",
         "//programl/util/py:pbutil",
@@ -89,8 +83,6 @@ py_binary(
         "//tasks/dataflow:ggnn_batch_builder",
         "//tasks/dataflow:vocabulary",
         "//tasks/dataflow/dataset:pathflag",
-        "//third_party/py/absl",
-        "//third_party/py/numpy",
     ],
 )
 
@@ -99,12 +91,11 @@ py_library(
     srcs = ["graph_loader.py"],
     visibility = ["//visibility:public"],
     deps = [
+        "//models:base_graph_loader",
         "//programl/graph/format/py:cdfg",
-        "//programl/models:base_graph_loader",
         "//programl/proto:programl_py",
         "//programl/util/py:humanize",
         "//programl/util/py:pbutil",
-        "//third_party/py/absl",
     ],
 )
 
@@ -113,13 +104,11 @@ py_library(
     srcs = ["lstm_batch_builder.py"],
     visibility = ["//visibility:public"],
     deps = [
+        "//models:base_batch_builder",
+        "//models:base_graph_loader",
+        "//models:batch_data",
+        "//models/lstm:lstm_batch",
         "//programl/graph/format/py:graph_serializer",
-        "//programl/models:base_batch_builder",
-        "//programl/models:base_graph_loader",
-        "//programl/models:batch_data",
-        "//programl/models/lstm:lstm_batch",
-        "//third_party/py/absl",
-        "//third_party/py/numpy",
     ],
 )
 
@@ -134,7 +123,7 @@ py_binary(
     deps = [
         "//programl/proto:programl_py",
         "//programl/util/py:init_app",
-        "//third_party/py/absl",
+        "//programl/util/py:pbutil",
     ],
 )
 
@@ -144,10 +133,11 @@ py_binary(
     deps = [
         ":vocabulary",
         "//programl/proto:programl_py",
+        "//programl/third_party/ncc:vocabulary",
         "//programl/util/py:init_app",
+        "//programl/util/py:pbutil",
+        "//programl/util/py:progress",
         "//tasks/dataflow/dataset:pathflag",
-        "//third_party/py/absl",
-        "//third_party/py/ncc:vocabulary",
     ],
 )
 
@@ -159,7 +149,6 @@ py_binary(
         ":ggnn",
         ":vocabulary",
         "//programl/util/py:init_app",
-        "//third_party/py/absl",
     ],
 )
 
@@ -170,16 +159,14 @@ py_binary(
         ":dataflow",
         ":graph_loader",
         ":lstm_batch_builder",
-        "//programl/models:async_batch_builder",
-        "//programl/models:epoch_batch_iterator",
-        "//programl/models/lstm",
+        "//models:async_batch_builder",
+        "//models:epoch_batch_iterator",
+        "//models/lstm",
         "//programl/proto:programl_py",
+        "//programl/third_party/ncc:vocabulary",
         "//programl/util/py:humanize",
         "//programl/util/py:init_app",
         "//programl/util/py:pbutil",
-        "//third_party/py/absl",
-        "//third_party/py/ncc:vocabulary",
-        "//third_party/py/numpy",
     ],
 )
 
@@ -188,6 +175,5 @@ py_library(
     srcs = ["vocabulary.py"],
     deps = [
         "//programl/util/py:humanize",
-        "//third_party/py/absl",
     ],
 )
diff --git a/tasks/dataflow/dataset/BUILD b/tasks/dataflow/dataset/BUILD
index c0e28981e..21310d0a2 100644
--- a/tasks/dataflow/dataset/BUILD
+++ b/tasks/dataflow/dataset/BUILD
@@ -22,7 +22,6 @@ py_binary(
         "//programl/util/py:pbutil",
         "//programl/util/py:progress",
         "//tasks/dataflow/dataset:pathflag",
-        "//third_party/py/absl",
     ],
 )
 
@@ -36,15 +35,13 @@ py_binary(
     ],
     deps = [
         ":encode_inst2vec",
-        "//programl/util/py:init_app",
         ":pathflag",
         "//programl/ir/llvm/py:llvm",
         "//programl/proto:programl_py",
-        "//third_party/py/absl",
-        "//programl/util/py:runfiles_path",
-        "//programl/util/py:progress",
+        "//programl/util/py:init_app",
         "//programl/util/py:pbutil",
-        # "//third_party/py/mysql",
+        "//programl/util/py:progress",
+        "//programl/util/py:runfiles_path",
     ],
 )
 
@@ -97,7 +94,9 @@ py_binary(
         ":pathflag",
         "//programl/ir/llvm:inst2vec_encoder",
         "//programl/proto:programl_py",
-        "//third_party/py/absl",
+        "//programl/util/py:decorators",
+        "//programl/util/py:pbutil",
+        "//programl/util/py:progress",
     ],
 )
 
@@ -123,7 +122,6 @@ py_library(
     srcs = ["pathflag.py"],
     visibility = ["//tasks:__subpackages__"],
     deps = [
-        "//third_party/py/absl",
     ],
 )
 
diff --git a/tasks/dataflow/dataset/create.py b/tasks/dataflow/dataset/create.py
index 2bcac65e9..5e6c54c63 100644
--- a/tasks/dataflow/dataset/create.py
+++ b/tasks/dataflow/dataset/create.py
@@ -24,7 +24,7 @@
 import shutil
 import subprocess
 
-from absl import flags, logging
+from absl import app, flags, logging
 from MySQLdb import _mysql
 
 from programl.ir.llvm.py import llvm
@@ -46,9 +46,9 @@
 flags.DEFINE_string("db", None, "The database to export from.")
 FLAGS = flags.FLAGS
 
-CREATE_LABELS = runfiles_path("programl/tasks/dataflow/dataset/create_labels")
-CREATE_VOCAB = runfiles_path("programl/tasks/dataflow/dataset/create_vocab")
-UNPACK_IR_LISTS = runfiles_path("programl/tasks/dataflow/dataset/unpack_ir_lists")
+CREATE_LABELS = runfiles_path("tasks/dataflow/dataset/create_labels")
+CREATE_VOCAB = runfiles_path("tasks/dataflow/dataset/create_vocab")
+UNPACK_IR_LISTS = runfiles_path("tasks/dataflow/dataset/unpack_ir_lists")
 
 
 def _ProcessRow(output_directory, row, file_id) -> None:
@@ -119,7 +119,7 @@ def _ProcessRow(output_directory, row, file_id) -> None:
             f"../graphs/{name}.ProgramGraph.pb",
             output_directory / dst / f"{name}.ProgramGraph.pb",
         )
-    except (ValueError, OSError, TimeoutError, AssertionError) as e:
+    except (ValueError, OSError, TimeoutError, AssertionError):
         pass
 
 
diff --git a/tasks/dataflow/dataset/encode_inst2vec.py b/tasks/dataflow/dataset/encode_inst2vec.py
index 7ca1428ae..a6971e56a 100644
--- a/tasks/dataflow/dataset/encode_inst2vec.py
+++ b/tasks/dataflow/dataset/encode_inst2vec.py
@@ -21,17 +21,27 @@
 import pathlib
 import random
 import time
+from itertools import islice
 from typing import List, Tuple
 
-from absl import decorators, flags, labtypes, pbutil, progress
+from absl import app, flags
 
 from programl.ir.llvm.inst2vec_encoder import Inst2vecEncoder
 from programl.proto import ir_pb2, program_graph_pb2
+from programl.util.py import decorators, pbutil, progress
 from tasks.dataflow.dataset import pathflag
 
 FLAGS = flags.FLAGS
 
 
+def chunkify(iterable, n):
+    i = iter(iterable)
+    piece = list(islice(i, n))
+    while piece:
+        yield piece
+        piece = list(islice(i, n))
+
+
 @decorators.timeout(seconds=60)
 def Encode(encoder, graph, graph_path, ir_path):
     if ir_path.is_file():
@@ -96,7 +106,7 @@ def __init__(self, path: pathlib.Path):
 
     def Run(self):
         encoder = Inst2vecEncoder()
-        jobs = [(encoder, chunk) for chunk in list(labtypes.Chunkify(self.paths, 128))]
+        jobs = [(encoder, chunk) for chunk in list(chunkify(self.paths, 128))]
         with open(self.path / "graphs.inst2vec_log.txt", "a") as f:
             with multiprocessing.Pool() as pool:
                 for processed_count, encoded_count, runtime in pool.imap_unordered(
diff --git a/tasks/dataflow/dataset/pathflag.py b/tasks/dataflow/dataset/pathflag.py
index 32fd6cf8e..dda99aba2 100644
--- a/tasks/dataflow/dataset/pathflag.py
+++ b/tasks/dataflow/dataset/pathflag.py
@@ -39,4 +39,4 @@ def path():
       You call it to return path.
       I am computer.
     """
-    return app.FLAGS.path
+    return flags.FLAGS.path
diff --git a/tasks/dataflow/ggnn.py b/tasks/dataflow/ggnn.py
index 4ad263940..33ea4a5db 100644
--- a/tasks/dataflow/ggnn.py
+++ b/tasks/dataflow/ggnn.py
@@ -21,9 +21,9 @@
 import numpy as np
 from absl import logging
 
-from programl.models.async_batch_builder import AsyncBatchBuilder
-from programl.models.epoch_batch_iterator import EpochBatchIterator
-from programl.models.ggnn.ggnn import Ggnn
+from models.async_batch_builder import AsyncBatchBuilder
+from models.epoch_batch_iterator import EpochBatchIterator
+from models.ggnn.ggnn import Ggnn
 from programl.proto import epoch_pb2
 from programl.util.py import humanize, pbutil
 from tasks.dataflow import dataflow
diff --git a/tasks/dataflow/ggnn_batch_builder.py b/tasks/dataflow/ggnn_batch_builder.py
index e62df1031..6c3da1f91 100644
--- a/tasks/dataflow/ggnn_batch_builder.py
+++ b/tasks/dataflow/ggnn_batch_builder.py
@@ -17,12 +17,13 @@
 from typing import Dict, Optional
 
 import numpy as np
+from absl import logging
 
+from models.base_batch_builder import BaseBatchBuilder
+from models.base_graph_loader import BaseGraphLoader
+from models.batch_data import BatchData
+from models.ggnn.ggnn_batch import GgnnBatchData
 from programl.graph.format.py.graph_tuple_builder import GraphTupleBuilder
-from programl.models.base_batch_builder import BaseBatchBuilder
-from programl.models.base_graph_loader import BaseGraphLoader
-from programl.models.batch_data import BatchData
-from programl.models.ggnn.ggnn_batch import GgnnBatchData
 
 
 class DataflowGgnnBatchBuilder(BaseBatchBuilder):
diff --git a/tasks/dataflow/graph_loader.py b/tasks/dataflow/graph_loader.py
index acd8df482..2f84f4e4d 100644
--- a/tasks/dataflow/graph_loader.py
+++ b/tasks/dataflow/graph_loader.py
@@ -26,8 +26,8 @@
 
 from absl import flags, logging
 
+from models import base_graph_loader
 from programl.graph.format.py import cdfg
-from programl.models import base_graph_loader
 from programl.proto import epoch_pb2, program_graph_features_pb2, program_graph_pb2
 from programl.util.py import humanize, pbutil
 
@@ -58,6 +58,8 @@ def __init__(
         require_inst2vec: bool = False,
         max_queue_size: int = 512,
     ):
+        self._stopped = False
+
         self.graph_path = path / epoch_pb2.EpochType.Name(epoch_type).lower()
         if not self.graph_path.is_dir():
             raise FileNotFoundError(str(self.graph_path))
@@ -88,7 +90,6 @@ def __init__(
         self._outq = Queue(maxsize=max_queue_size)
         self._thread = threading.Thread(target=self._Worker)
         self._thread.start()
-        self._stopped = False
 
     def Stop(self):
         if self._stopped:
diff --git a/tasks/dataflow/lstm_batch_builder.py b/tasks/dataflow/lstm_batch_builder.py
index 4c04b9ef2..db21ac2a1 100644
--- a/tasks/dataflow/lstm_batch_builder.py
+++ b/tasks/dataflow/lstm_batch_builder.py
@@ -20,11 +20,11 @@
 import tensorflow as tf
 from absl import flags, logging
 
+from models.base_batch_builder import BaseBatchBuilder
+from models.base_graph_loader import BaseGraphLoader
+from models.batch_data import BatchData
+from models.lstm.lstm_batch import LstmBatchData
 from programl.graph.format.py import graph_serializer
-from programl.models.base_batch_builder import BaseBatchBuilder
-from programl.models.base_graph_loader import BaseGraphLoader
-from programl.models.batch_data import BatchData
-from programl.models.lstm.lstm_batch import LstmBatchData
 
 FLAGS = flags.FLAGS
 
diff --git a/tasks/dataflow/train_lstm.py b/tasks/dataflow/train_lstm.py
index 33e324f64..34e63ac45 100644
--- a/tasks/dataflow/train_lstm.py
+++ b/tasks/dataflow/train_lstm.py
@@ -25,16 +25,16 @@
 import numpy as np
 from absl import app, flags, logging
 
-from programl.models.async_batch_builder import AsyncBatchBuilder
-from programl.models.epoch_batch_iterator import EpochBatchIterator
-from programl.models.lstm.lstm import Lstm
+from models.async_batch_builder import AsyncBatchBuilder
+from models.epoch_batch_iterator import EpochBatchIterator
+from models.lstm.lstm import Lstm
 from programl.proto import epoch_pb2
+from programl.third_party.ncc import vocabulary
 from programl.util.py import humanize, pbutil
 from programl.util.py.init_app import init_app
 from tasks.dataflow import dataflow
 from tasks.dataflow.graph_loader import DataflowGraphLoader
 from tasks.dataflow.lstm_batch_builder import DataflowLstmBatchBuilder
-from third_party.py.ncc import vocabulary
 
 flags.DEFINE_integer(
     "max_data_flow_steps",
diff --git a/tasks/devmap/dataset/BUILD b/tasks/devmap/dataset/BUILD
index 944954ecd..770ad3b4b 100644
--- a/tasks/devmap/dataset/BUILD
+++ b/tasks/devmap/dataset/BUILD
@@ -23,9 +23,16 @@ py_binary(
         "//programl/proto:programl_py",
         "//programl/util/py:pbutil",
         "//tasks/dataflow/dataset:pathflag",
-        "//third_party/py/absl",
-        "//third_party/py/numpy",
-        "//third_party/py/pandas",
-        "//third_party/py/requests",
+    ],
+)
+
+py_test(
+    name = "create_test",
+    timeout = "long",
+    srcs = ["create_test.py"],
+    deps = [
+        "//tasks/devmap/dataset:create",
+        "//tests:test_main",
+        "//tests/plugins:tempdir",
     ],
 )
diff --git a/tasks/devmap/dataset/create.py b/tasks/devmap/dataset/create.py
index c4fb28c60..395a88398 100644
--- a/tasks/devmap/dataset/create.py
+++ b/tasks/devmap/dataset/create.py
@@ -25,7 +25,7 @@
 import numpy as np
 import pandas as pd
 import requests
-from absl import flags
+from absl import app, flags
 
 from programl.ir.llvm.py import llvm
 from programl.util.py import pbutil
diff --git a/third_party/openmp/BUILD b/third_party/openmp/BUILD
deleted file mode 100644
index ec9f6e967..000000000
--- a/third_party/openmp/BUILD
+++ /dev/null
@@ -1,31 +0,0 @@
-# Support for OpenMP.
-
-# A dummy target for compiling with OpenMP features enabled.
-#
-# This "library" contains no flags, but defines the compile-time
-# and link-time options for compiling programs which use OpenMP
-# pragmas.
-#
-# Add this target to your C++ binary/libraryy deps to enable
-# OpenMP, and add the "-fopenmp" copt. For example:
-#
-#   cc_binary(
-#       name = "my_app",
-#       srcs = ["my_app.cc"],
-#       copts = ["-fopenmp"],
-#       deps = ["//third_party/openmp"],
-#   )
-#
-cc_library(
-    name = "openmp",
-    copts = ["-fopenmp"],
-    linkopts = [
-        "-fopenmp",
-    ] + select({
-        "//:darwin": [
-            "-L/usr/local/Cellar/libomp/10.0.0/lib",
-        ],
-        "//conditions:default": [],
-    }),
-    visibility = ["//visibility:public"],
-)
diff --git a/third_party/py/BUILD b/third_party/py/BUILD
index 30f4c2a72..e69de29bb 100644
--- a/third_party/py/BUILD
+++ b/third_party/py/BUILD
@@ -1,14 +0,0 @@
-# Third-party python packages.
-
-# Python targets must have a srcs property, so export an empty file that
-# can be used, e.g.
-#
-#   py_library(
-#        name = "foo",
-#        srcs = ["//third_party/py:empty.py"],
-#        deps = [
-#            requirement("foo"),
-#        ],
-#    )
-#
-exports_files(["empty.py"])
diff --git a/third_party/py/absl/BUILD b/third_party/py/absl/BUILD
deleted file mode 100644
index 1e83ca740..000000000
--- a/third_party/py/absl/BUILD
+++ /dev/null
@@ -1,16 +0,0 @@
-# A wrapper around pip package to pull in undeclared dependencies.
-
-load("@programl_requirements//:requirements.bzl", "requirement")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])  # Apache 2.0
-
-py_library(
-    name = "absl",
-    srcs = ["//third_party/py:empty.py"],
-    deps = [
-        requirement("absl-py"),
-        requirement("six"),
-    ],
-)
diff --git a/third_party/py/decorator/BUILD b/third_party/py/decorator/BUILD
deleted file mode 100644
index a53d492ce..000000000
--- a/third_party/py/decorator/BUILD
+++ /dev/null
@@ -1,15 +0,0 @@
-# A wrapper around pip package to pull in undeclared dependencies.
-
-load("@programl_requirements//:requirements.bzl", "requirement")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])  # BSD
-
-py_library(
-    name = "decorator",
-    srcs = ["//third_party/py:empty.py"],
-    deps = [
-        requirement("decorator"),
-    ],
-)
diff --git a/third_party/py/networkx/BUILD b/third_party/py/networkx/BUILD
deleted file mode 100644
index 245246a2c..000000000
--- a/third_party/py/networkx/BUILD
+++ /dev/null
@@ -1,16 +0,0 @@
-# A wrapper around pip package to pull in undeclared dependencies.
-
-load("@programl_requirements//:requirements.bzl", "requirement")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])  # BSD
-
-py_library(
-    name = "networkx",
-    srcs = ["//third_party/py:empty.py"],
-    deps = [
-        requirement("networkx"),
-        "//third_party/py/decorator",  # Implicit dependency.
-    ],
-)
diff --git a/third_party/py/numpy/BUILD b/third_party/py/numpy/BUILD
deleted file mode 100644
index c5aeee9e7..000000000
--- a/third_party/py/numpy/BUILD
+++ /dev/null
@@ -1,15 +0,0 @@
-# A wrapper around pip package to pull in undeclared dependencies.
-
-load("@programl_requirements//:requirements.bzl", "requirement")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])  # BSD
-
-py_library(
-    name = "numpy",
-    srcs = ["//third_party/py:empty.py"],
-    deps = [
-        requirement("numpy"),
-    ],
-)
diff --git a/third_party/py/pandas/BUILD b/third_party/py/pandas/BUILD
deleted file mode 100644
index 5b23e00c8..000000000
--- a/third_party/py/pandas/BUILD
+++ /dev/null
@@ -1,24 +0,0 @@
-# A wrapper around pandas pip package to pull in undeclared dependencies.
-#
-# If a python target requires pandas, it should depend on this package
-# (i.e. //third_party/py/pandas), instead of requirement("pandas").
-
-load("@programl_requirements//:requirements.bzl", "requirement")
-load("@tasks_requirements//:requirements.bzl", task_requirement = "requirement")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])  # BSD
-
-# Requires pandas!
-py_library(
-    name = "pandas",
-    srcs = ["//third_party/py:empty.py"],
-    deps = [
-        task_requirement("pandas"),
-        task_requirement("python-dateutil"),
-        task_requirement("pytz"),
-        requirement("six"),
-        requirement("numpy"),
-    ],
-)
diff --git a/third_party/py/protobuf/BUILD b/third_party/py/protobuf/BUILD
deleted file mode 100644
index 4a941b53a..000000000
--- a/third_party/py/protobuf/BUILD
+++ /dev/null
@@ -1,16 +0,0 @@
-# A wrapper around pip package to pull in undeclared dependencies.
-
-load("@programl_requirements//:requirements.bzl", "requirement")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])  # BSD
-
-py_library(
-    name = "protobuf",
-    srcs = ["//third_party/py:empty.py"],
-    deps = [
-        requirement("protobuf"),
-        requirement("grpcio"),
-    ],
-)
diff --git a/third_party/py/requests/BUILD b/third_party/py/requests/BUILD
deleted file mode 100644
index a9e431e36..000000000
--- a/third_party/py/requests/BUILD
+++ /dev/null
@@ -1,18 +0,0 @@
-# A wrapper around pandas pip package to pull in undeclared dependencies.
-#
-# If a python target requires pandas, it should depend on this package
-# (i.e. //third_party/py/pandas), instead of requirement("pandas").
-
-load("@tasks_requirements//:requirements.bzl", task_requirement = "requirement")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])  # BSD
-
-py_library(
-    name = "requests",
-    srcs = ["//third_party/py:empty.py"],
-    deps = [
-        task_requirement("requests"),
-    ],
-)
diff --git a/third_party/py/scikit_learn/BUILD b/third_party/py/scikit_learn/BUILD
deleted file mode 100644
index 6e061272c..000000000
--- a/third_party/py/scikit_learn/BUILD
+++ /dev/null
@@ -1,17 +0,0 @@
-# A wrapper around pip package to pull in undeclared dependencies.
-
-load("@programl_requirements//:requirements.bzl", "requirement")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])  # BSD
-
-py_library(
-    name = "scikit_learn",
-    srcs = ["//third_party/py:empty.py"],
-    deps = [
-        requirement("scikit-learn"),
-        requirement("joblib"),
-        "//third_party/py/scipy",
-    ],
-)
diff --git a/third_party/py/scipy/BUILD b/third_party/py/scipy/BUILD
deleted file mode 100644
index 38775f67b..000000000
--- a/third_party/py/scipy/BUILD
+++ /dev/null
@@ -1,15 +0,0 @@
-# A wrapper around pip package to pull in undeclared dependencies.
-
-load("@programl_requirements//:requirements.bzl", "requirement")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])  # BSD
-
-py_library(
-    name = "scipy",
-    srcs = ["//third_party/py:empty.py"],
-    deps = [
-        requirement("scipy"),
-    ],
-)
diff --git a/third_party/py/torch/BUILD b/third_party/py/torch/BUILD
deleted file mode 100644
index 4ae2b4a48..000000000
--- a/third_party/py/torch/BUILD
+++ /dev/null
@@ -1,15 +0,0 @@
-# A wrapper around pip package to pull in undeclared dependencies.
-
-load("@programl_requirements//:requirements.bzl", "requirement")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])  # BSD-style
-
-py_library(
-    name = "torch",
-    srcs = ["//third_party/py:empty.py"],
-    deps = [
-        requirement("torch"),
-    ],
-)
diff --git a/third_party/py/tqdm/BUILD b/third_party/py/tqdm/BUILD
deleted file mode 100644
index 9c1968f30..000000000
--- a/third_party/py/tqdm/BUILD
+++ /dev/null
@@ -1,15 +0,0 @@
-# A wrapper around pip package to pull in undeclared dependencies.
-
-load("@programl_requirements//:requirements.bzl", "requirement")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])  # BSD
-
-py_library(
-    name = "tqdm",
-    srcs = ["//third_party/py:empty.py"],
-    deps = [
-        requirement("tqdm"),
-    ],
-)
diff --git a/tools/BUILD b/tools/BUILD
index b2b1c6ff0..5ac63524b 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -2,6 +2,5 @@ exports_files([
     "bazel",
     "bazel_env.sh",
     "flaky_bazel.sh",
-    "make_bootstrap.py",
     "README.md",
 ])
diff --git a/tools/bzl/deps.bzl b/tools/bzl/deps.bzl
index 497c4b826..761e26ff1 100644
--- a/tools/bzl/deps.bzl
+++ b/tools/bzl/deps.bzl
@@ -22,10 +22,10 @@ def programl_deps():
 
   http_archive(
     name="llvm",
-    sha256 = "02ca111816345d76d7d7687f2d6ed5eaaa0d30f297f3a286171b8409e4502282",
-    strip_prefix="bazel_llvm-a0bffdcbbcb551acda57832d817aef6571b911ee",
+    sha256 = "9bac9169582c2bdc8629f56f390b444e4442ed215695692f3925a996e35c0b88",
+    strip_prefix="bazel_llvm-f0d78833bc1e43f475fd41495dbd55017104cb36",
     urls=[
-      "https://github.com/ChrisCummins/bazel_llvm/archive/a0bffdcbbcb551acda57832d817aef6571b911ee.tar.gz"
+      "https://github.com/ChrisCummins/bazel_llvm/archive/f0d78833bc1e43f475fd41495dbd55017104cb36.tar.gz"
     ],
   )
 
@@ -74,9 +74,9 @@ def programl_deps():
   http_archive(
     name="pybind11_json",
     build_file="@programl//:third_party/pybind11_json.BUILD",
-    sha256="45957f8564e921a412a6de49c578ef1faf3b04e531e859464853e26e1c734ea5",
-    strip_prefix="pybind11_json-0.2.4/include",
-    urls=["https://github.com/pybind/pybind11_json/archive/0.2.4.tar.gz"],
+    sha256="2eaf4c294aaf449ed6de83ae81d23f9275d6286cf184834c7cf6ea6087d98794",
+    strip_prefix="pybind11_json-0.2.10/include",
+    urls=["https://github.com/pybind/pybind11_json/archive/0.2.10.tar.gz"],
   )
 
   http_archive(
@@ -131,9 +131,9 @@ def programl_deps():
   http_archive(
     name="pybind11",
     build_file="@programl//:third_party/pybind11_bazel/pybind11.BUILD",
-    sha256="1eed57bc6863190e35637290f97a20c81cfe4d9090ac0a24f3bbf08f265eb71d",
-    strip_prefix="pybind11-2.4.3",
-    urls=["https://github.com/pybind/pybind11/archive/v2.4.3.tar.gz"],
+    sha256="8ff2fff22df038f5cd02cea8af56622bc67f5b64534f1b83b9f133b8366acff2",
+    strip_prefix="pybind11-2.6.2",
+    urls=["https://github.com/pybind/pybind11/archive/v2.6.2.tar.gz"],
   )
 
   http_archive(
diff --git a/tools/make_bootstrap.py b/tools/make_bootstrap.py
deleted file mode 100755
index bebb14df7..000000000
--- a/tools/make_bootstrap.py
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/usr/bin/env python3
-#
-# Generate a bootstrap.sh script by pulling out commands from DEPS.txt files.
-#
-# Usage:
-#
-#     $ python ./tools/bootstrap.py
-#
-import os
-import sys
-
-
-def GetPlatformNameOrDie():
-    """Return the platform name, one of: {linux,macos}."""
-    if sys.platform == "linux" or sys.platform == "linux2":
-        return "linux"
-    elif sys.platform == "darwin":
-        return "macos"
-    else:
-        print("Unsupported platform: {}".format(sys.platform), file=sys.stderr)
-        sys.exit(1)
-
-
-def EmitDependencyCommandsFromPathOrDie(path, visited_set, platform, file):
-    """Read the dependency file and write out the commadns to execute."""
-
-    def Emit(line, linenum):
-        print(line, " # from:", path + ":" + str(linenum), file=file)
-
-    with open(path) as f:
-        for i, line in enumerate(f.read().rstrip().split("\n")):
-            if line.startswith("#"):
-                Emit(line, i + 1)
-            elif line.startswith("all: "):
-                cmd = line[len("all: ") :]
-                if cmd not in visited_set:
-                    visited_set.add(cmd)
-                    Emit(cmd, i + 1)
-            elif line.startswith("{}: ".format(platform)):
-                cmd = line[len(platform) + 2 :]
-                if cmd not in visited_set:
-                    visited_set.add(cmd)
-                    Emit(cmd, i + 1)
-
-
-def CreateBootstrapScriptOrDie(platform):
-    """Create a ./bootstrap.sh script with additional dependencies to install."""
-    with open("bootstrap.sh", "w") as f:
-        print(f"# Script generated by {sys.argv[0]}.", file=f)
-        print("set -eux", file=f)
-        visited_set = set()
-
-        for root, _, file_names in os.walk("."):
-            if "DEPS.txt" in file_names:
-                path = os.path.join(root, "DEPS.txt")
-                EmitDependencyCommandsFromPathOrDie(path, visited_set, platform, f)
-
-    print("Created bootstrap script to install system dependencies.")
-    print("Inspect and then run the script:")
-    print("    bash ./bootstrap.sh")
-
-
-if __name__ == "__main__":
-    CreateBootstrapScriptOrDie(GetPlatformNameOrDie())
diff --git a/tools/requirements.pre_commit.txt b/tools/requirements.pre_commit.txt
index 58ee5a8b9..ee3173b35 100644
--- a/tools/requirements.pre_commit.txt
+++ b/tools/requirements.pre_commit.txt
@@ -1,3 +1,3 @@
 black==19.10b0
-isort==4.3.21
-pre-commit==2.7.1
+isort>=4.3.21
+pre-commit>=2.9.0
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 000000000..8ee4e3ee9
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,10 @@
+[isort]
+multi_line_output = 3
+include_trailing_comma = True
+force_grid_wrap = 0
+use_parentheses = True
+ensure_newline_before_comments = True
+line_length = 88
+
+[flake8]
+ignore = E501, E302, W503, E203
diff --git a/version.txt b/version.txt
index 6e8bf73aa..0ea3a944b 100644
--- a/version.txt
+++ b/version.txt
@@ -1,1 +1,1 @@
-0.1.0
+0.2.0


Test patch:
diff --git a/.github/workflows/bazel_test.yaml b/.github/workflows/bazel_test.yaml
new file mode 100644
index 000000000..3995c3967
--- /dev/null
+++ b/.github/workflows/bazel_test.yaml
@@ -0,0 +1,36 @@
+---
+name: Bazel Test
+
+on:
+    push:
+        branches:
+            - development
+            - stable
+    pull_request:
+
+jobs:
+    test:
+        runs-on: ${{ matrix.os }}
+
+        strategy:
+            fail-fast: false
+            matrix:
+                os: [ubuntu-latest, macos-latest]
+        steps:
+            - uses: actions/checkout@v2
+
+            - name: Setup python
+              uses: actions/setup-python@v2
+              with:
+                  python-version: 3.9
+
+            - name: Install build dependencies
+              uses: ./.github/actions/install-build-dependencies
+
+            - name: Test
+              run: make test
+              env:
+                  CC: clang
+                  CXX: clang++
+                  BAZEL_OPTS: --batch
+                  BAZEL_TEST_OPTS: --config=ci --test_timeout=300,900,1800,7200
diff --git a/.github/workflows/bin_archive_test.yaml b/.github/workflows/bin_archive_test.yaml
new file mode 100644
index 000000000..3c6049c32
--- /dev/null
+++ b/.github/workflows/bin_archive_test.yaml
@@ -0,0 +1,42 @@
+---
+name: Binary Archive Test
+
+on:
+    push:
+        branches:
+            - development
+            - stable
+    pull_request:
+
+jobs:
+    test:
+        runs-on: ${{ matrix.os }}
+
+        strategy:
+            fail-fast: false
+            matrix:
+                os: [ubuntu-latest, macos-latest]
+        steps:
+            - uses: actions/checkout@v2
+
+            - name: Setup python
+              uses: actions/setup-python@v2
+              with:
+                  python-version: 3.9
+
+            - name: Install build dependencies
+              uses: ./.github/actions/install-build-dependencies
+
+            - name: Install
+              run: |
+                  mkdir "${GITHUB_WORKSPACE}/install"
+                  bazel --batch run --config=ci //:install "${GITHUB_WORKSPACE}/install"
+                  echo "${GITHUB_WORKSPACE}/install/bin" >> $GITHUB_PATH
+                  echo "LD_LIBRARY_PATH=${GITHUB_WORKSPACE}/install/lib" >> $GITHUB_ENV
+              env:
+                  CC: clang
+                  CXX: clang++
+
+            - name: Test installed binaries
+              run: |
+                  llvm2graph --version
diff --git a/.github/workflows/install_test.yaml b/.github/workflows/install_test.yaml
new file mode 100644
index 000000000..961382c0a
--- /dev/null
+++ b/.github/workflows/install_test.yaml
@@ -0,0 +1,54 @@
+---
+name: Install Test
+
+on:
+    push:
+        branches:
+            - development
+            - stable
+    pull_request:
+    schedule:
+        - cron: 0 0 * * 0 # weekly
+
+jobs:
+    test:
+        runs-on: ${{ matrix.os }}
+
+        strategy:
+            fail-fast: false
+            matrix:
+                os: [ubuntu-latest, macos-latest]
+                python: [3.6, 3.7, 3.8, 3.9]
+                exclude:
+                    # Only test recent python versions on macOS.
+                    - os: macos-latest
+                      python: 3.6
+                    - os: macos-latest
+                      python: 3.7
+        steps:
+            - uses: actions/checkout@v2
+
+            - name: Set up Python ${{ matrix.python }}
+              uses: actions/setup-python@v2
+              with:
+                  python-version: ${{ matrix.python }}
+
+            - name: Install build dependencies
+              uses: ./.github/actions/install-build-dependencies
+
+            - name: Install
+              run: make install
+              env:
+                  CC: clang
+                  CXX: clang++
+                  BAZEL_OPTS: --batch
+                  BAZEL_BUILD_OPTS: --config=ci
+
+            - name: Test
+              run: make install-test
+              env:
+                  CC: clang
+                  CXX: clang++
+
+            - name: Uninstall
+              run: make uninstall
diff --git a/tests/models/rolling_results_test.py b/models/rolling_results_test.py
similarity index 90%
rename from tests/models/rolling_results_test.py
rename to models/rolling_results_test.py
index c596f3703..6366acdf2 100644
--- a/tests/models/rolling_results_test.py
+++ b/models/rolling_results_test.py
@@ -18,9 +18,9 @@
 import pytest
 from absl import flags
 
-from programl.models.batch_data import BatchData
-from programl.models.batch_results import BatchResults
-from programl.models.rolling_results import RollingResults
+from models.batch_data import BatchData
+from models.batch_results import BatchResults
+from models.rolling_results import RollingResults
 from tests.test_main import main
 
 
diff --git a/tasks/dataflow/ggnn_test_one.py b/tasks/dataflow/ggnn_test_one.py
index 3bd68d3cd..11d2dcb12 100644
--- a/tasks/dataflow/ggnn_test_one.py
+++ b/tasks/dataflow/ggnn_test_one.py
@@ -19,11 +19,11 @@
 from typing import Any, Iterable
 
 import numpy as np
-from absl import flags
+from absl import app, flags
 
-from programl.models.base_graph_loader import BaseGraphLoader
-from programl.models.batch_results import BatchResults
-from programl.models.ggnn.ggnn import Ggnn
+from models.base_graph_loader import BaseGraphLoader
+from models.batch_results import BatchResults
+from models.ggnn.ggnn import Ggnn
 from programl.proto import (
     checkpoint_pb2,
     epoch_pb2,
diff --git a/tasks/dataflow/test_step_counts.py b/tasks/dataflow/test_step_counts.py
index a428c9ed7..5b68d8a24 100644
--- a/tasks/dataflow/test_step_counts.py
+++ b/tasks/dataflow/test_step_counts.py
@@ -16,9 +16,10 @@
 """Enumerate all test graphs and print their data flow step count to stdout."""
 import pathlib
 
-from absl import flags, pbutil
+from absl import app, flags
 
 from programl.proto import program_graph_features_pb2, program_graph_pb2
+from programl.util.py import pbutil
 from programl.util.py.init_app import init_app
 
 flags.DEFINE_string(
diff --git a/tasks/dataflow/test_vocab.py b/tasks/dataflow/test_vocab.py
index aadef90aa..99faef6af 100644
--- a/tasks/dataflow/test_vocab.py
+++ b/tasks/dataflow/test_vocab.py
@@ -18,13 +18,14 @@
 import pathlib
 from collections import defaultdict
 
-from absl import flags, pbutil, progress
+from absl import app, flags
 
 from programl.proto import program_graph_pb2
+from programl.third_party.ncc.vocabulary import VocabularyZipFile
+from programl.util.py import pbutil, progress
 from programl.util.py.init_app import init_app
 from tasks.dataflow.dataset import pathflag
 from tasks.dataflow.vocabulary import LoadVocabulary
-from third_party.py.ncc.vocabulary import VocabularyZipFile
 
 flags.DEFINE_integer("limit", 0, "Limit the number of files read.")
 FLAGS = flags.FLAGS
diff --git a/tests/tasks/devmap/dataset/create_test.py b/tasks/devmap/dataset/create_test.py
similarity index 100%
rename from tests/tasks/devmap/dataset/create_test.py
rename to tasks/devmap/dataset/create_test.py
diff --git a/tests/BUILD b/tests/BUILD
index 966cb4345..cda360aba 100644
--- a/tests/BUILD
+++ b/tests/BUILD
@@ -27,9 +27,8 @@ py_library(
     name = "test_main",
     testonly = 1,
     srcs = ["test_main.py"],
-    visibility = ["//tests:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
-        "//third_party/py/pytest",
     ],
 )
 
diff --git a/tests/cmd/BUILD b/tests/cmd/BUILD
index 3cc47a045..b1b50a2b3 100644
--- a/tests/cmd/BUILD
+++ b/tests/cmd/BUILD
@@ -24,6 +24,5 @@ py_test(
     deps = [
         "//programl/util/py:runfiles_path",
         "//tests:test_main",
-        "//third_party/py/pytest",
     ],
 )
diff --git a/tests/cmd/llvm2graph_strict_mode.py b/tests/cmd/llvm2graph_strict_mode.py
index 43178894d..c9c66a33a 100644
--- a/tests/cmd/llvm2graph_strict_mode.py
+++ b/tests/cmd/llvm2graph_strict_mode.py
@@ -21,14 +21,12 @@
 from programl.util.py.runfiles_path import runfiles_path
 from tests.test_main import main
 
-LLVM2GRAPH = runfiles_path("programl/bin/llvm2graph")
+LLVM2GRAPH = runfiles_path("bin/llvm2graph")
 
 
 # This IR file contains unreachable instructions, which will be rejected if
 # --strict mode is enabled.
-INVALID_MODULE = runfiles_path(
-    "programl/tests/data/module_with_unreachable_instructions.ll"
-)
+INVALID_MODULE = runfiles_path("tests/data/module_with_unreachable_instructions.ll")
 
 
 def test_invalid_module():
diff --git a/tests/data/BUILD b/tests/data/BUILD
index f31d3366a..f8002b3fb 100644
--- a/tests/data/BUILD
+++ b/tests/data/BUILD
@@ -20,6 +20,21 @@ load("@com_github_grpc_grpc//bazel:python_rules.bzl", "py_proto_library")
 
 package(default_visibility = ["//visibility:public"])
 
+filegroup(
+    name = "data",
+    testonly = 1,
+    srcs = [
+        ":hlo_protos",
+        ":llvm_ir",
+        ":llvm_ir_graphs",
+        ":llvm_ir_reachability_features",
+        ":llvm_ir_tar",
+        ":module_with_unreachable_instructions",
+        ":test_protos",
+        ":test_protos_py",
+    ],
+)
+
 filegroup(
     name = "hlo_protos",
     testonly = 1,
diff --git a/tests/data/test_protos.proto b/tests/data/test_protos.proto
index cd16fdafb..1e2b9c014 100644
--- a/tests/data/test_protos.proto
+++ b/tests/data/test_protos.proto
@@ -24,7 +24,7 @@ option java_multiple_files = true;
 option java_outer_classname = "TestProtosProto";
 option java_package = "com.labm8";
 
-message TestMessage {
+message ATestMessage {
   required string string = 1;
   optional int32 number = 2;
   oneof union_field {
diff --git a/tests/graph/format/py/BUILD b/tests/graph/format/py/BUILD
index e8f187076..22483b22f 100644
--- a/tests/graph/format/py/BUILD
+++ b/tests/graph/format/py/BUILD
@@ -31,7 +31,6 @@ py_test(
         "//programl/graph/format/py:graph_tuple_builder",
         "//programl/proto:programl_py",
         "//tests:test_main",
-        "//third_party/py/pytest",
     ],
 )
 
@@ -42,6 +41,5 @@ py_test(
         "//programl/graph/format/py:nx_format",
         "//programl/proto:programl_py",
         "//tests:test_main",
-        "//third_party/py/networkx",
     ],
 )
diff --git a/tests/graph/format/py/graph_tuple_builder_test.py b/tests/graph/format/py/graph_tuple_builder_test.py
index 6d94bb36d..a2b54a46d 100644
--- a/tests/graph/format/py/graph_tuple_builder_test.py
+++ b/tests/graph/format/py/graph_tuple_builder_test.py
@@ -26,7 +26,7 @@ def test_GraphTuple_empty_shapes():
     """Build from an empty proto."""
     builder = GraphTupleBuilder()
     with pytest.raises(ValueError) as e_ctx:
-        gt = builder.Build()
+        builder.Build()
     assert "contains no graphs" in str(e_ctx.value)
 
 
diff --git a/tests/graph/py/BUILD b/tests/graph/py/BUILD
index d140a4bf3..f040494c1 100644
--- a/tests/graph/py/BUILD
+++ b/tests/graph/py/BUILD
@@ -21,6 +21,5 @@ py_test(
         "//programl/graph/py:program_graph_builder",
         "//programl/proto:programl_py",
         "//tests:test_main",
-        "//third_party/py/pytest",
     ],
 )
diff --git a/tests/graph/py/program_graph_builder_test.py b/tests/graph/py/program_graph_builder_test.py
index f5cdaf3b1..72fdbc154 100644
--- a/tests/graph/py/program_graph_builder_test.py
+++ b/tests/graph/py/program_graph_builder_test.py
@@ -15,10 +15,10 @@
 # limitations under the License.
 """Unit tests for //programl/graph/py:program_graph_builder."""
 import pytest
-from programl.proto.program_graph_options_pb2 import ProgramGraphOptions
 
 from programl.graph.py.program_graph_builder import ProgramGraphBuilder
 from programl.proto import edge_pb2, node_pb2, program_graph_pb2
+from programl.proto.program_graph_options_pb2 import ProgramGraphOptions
 from tests.test_main import main
 
 
diff --git a/tests/ir/llvm/BUILD b/tests/ir/llvm/BUILD
index 9b5ce64be..13cef8eb4 100644
--- a/tests/ir/llvm/BUILD
+++ b/tests/ir/llvm/BUILD
@@ -34,6 +34,5 @@ py_test(
         "//programl/proto:programl_py",
         "//tests:test_main",
         "//tests/plugins:llvm_program_graph",
-        "//third_party/py/absl",
     ],
 )
diff --git a/tests/ir/llvm/py/BUILD b/tests/ir/llvm/py/BUILD
index a46e303b7..63b32846b 100644
--- a/tests/ir/llvm/py/BUILD
+++ b/tests/ir/llvm/py/BUILD
@@ -18,10 +18,7 @@ py_test(
     name = "llvm_test",
     srcs = ["llvm_test.py"],
     deps = [
-        "//programl/ir/llvm/py:llvm",
-        "//programl/proto:programl_py",
+        "//programl",
         "//tests:test_main",
-        "//third_party/py/absl",
-        "//third_party/py/pytest",
     ],
 )
diff --git a/tests/ir/llvm/py/llvm_test.py b/tests/ir/llvm/py/llvm_test.py
index 6280bdd0e..5b1d648bf 100644
--- a/tests/ir/llvm/py/llvm_test.py
+++ b/tests/ir/llvm/py/llvm_test.py
@@ -16,6 +16,7 @@
 """Unit tests for //programl/ir/llvm/py:llvm."""
 import pytest
 
+import programl
 from programl.ir.llvm.py import llvm
 from programl.proto import node_pb2, program_graph_options_pb2, program_graph_pb2
 from tests.test_main import main
@@ -38,14 +39,19 @@
 """
 
 
+@pytest.fixture(params=[programl.build_llvm_graph, llvm.BuildProgramGraph])
+def build_llvm_graph(request):
+    return request.param
+
+
 def GetStringScalar(proto, name):
     return proto.features.feature[name].bytes_list.value[0].decode("utf-8")
 
 
-def test_simple_ir():
+def test_simple_ir(build_llvm_graph):
     """Test equivalence of nodes that pre-process to the same text."""
     options = program_graph_options_pb2.ProgramGraphOptions(opt_level=3)
-    proto = llvm.BuildProgramGraph(SIMPLE_IR, options)
+    proto = build_llvm_graph(SIMPLE_IR, options)
     assert isinstance(proto, program_graph_pb2.ProgramGraph)
 
     assert len(proto.module) == 1
@@ -78,22 +84,22 @@ def test_simple_ir():
     assert GetStringScalar(proto.node[5], "full_text").startswith("i32 %")
 
 
-def test_opt_level():
+def test_opt_level(build_llvm_graph):
     """Test equivalence of nodes that pre-process to the same text."""
     options = program_graph_options_pb2.ProgramGraphOptions(
         opt_level=0,
     )
-    unoptimized = llvm.BuildProgramGraph(SIMPLE_IR)
+    unoptimized = build_llvm_graph(SIMPLE_IR)
     options.opt_level = 3
-    optimized = llvm.BuildProgramGraph(SIMPLE_IR, options)
+    optimized = build_llvm_graph(SIMPLE_IR, options)
 
     assert len(optimized.node) < len(unoptimized.node)
 
 
-def test_invalid_ir():
+def test_invalid_ir(build_llvm_graph):
     """Test equivalence of nodes that pre-process to the same text."""
     with pytest.raises(ValueError) as e_ctx:
-        llvm.BuildProgramGraph("foo bar")
+        build_llvm_graph("foo bar")
     assert "expected top-level entity" in str(e_ctx.value)
 
 
diff --git a/tests/ir/xla/py/BUILD b/tests/ir/xla/py/BUILD
index 8e7d4e166..10f0c83aa 100644
--- a/tests/ir/xla/py/BUILD
+++ b/tests/ir/xla/py/BUILD
@@ -22,11 +22,9 @@ py_test(
     data = ["//tests/data:hlo_protos"],
     deps = [
         "//programl/ir/xla/py:xla",
+        "//programl/third_party/tensorflow:xla_py",
         "//programl/util/py:pbutil",
         "//programl/util/py:runfiles_path",
         "//tests:test_main",
-        "//third_party/py/absl",
-        "//third_party/py/pytest",
-        "//third_party/tensorflow:xla_py",
     ],
 )
diff --git a/tests/ir/xla/py/xla_test.py b/tests/ir/xla/py/xla_test.py
index 9848c70fe..3ed8c501e 100644
--- a/tests/ir/xla/py/xla_test.py
+++ b/tests/ir/xla/py/xla_test.py
@@ -18,14 +18,14 @@
 from absl import flags
 
 from programl.ir.xla.py import xla
+from programl.third_party.tensorflow import xla_pb2
 from programl.util.py import pbutil
 from programl.util.py.runfiles_path import runfiles_path
 from tests.test_main import main
-from third_party.tensorflow import xla_pb2
 
 FLAGS = flags.FLAGS
 
-TEST_PROTO = runfiles_path("programl/tests/data/a.hlo.pb")
+TEST_PROTO = runfiles_path("tests/data/a.hlo.pb")
 
 
 def test_empty_proto():
diff --git a/tests/plugins/BUILD b/tests/plugins/BUILD
index 2cc24af9f..546c9cf3f 100644
--- a/tests/plugins/BUILD
+++ b/tests/plugins/BUILD
@@ -25,7 +25,6 @@ py_library(
     data = ["//tests/data:llvm_ir"],
     deps = [
         "//programl/util/py:runfiles_path",
-        "//third_party/py/pytest",
     ],
 )
 
@@ -38,7 +37,6 @@ py_library(
         "//programl/proto:programl_py",
         "//programl/util/py:pbutil",
         "//programl/util/py:runfiles_path",
-        "//third_party/py/pytest",
     ],
 )
 
@@ -57,6 +55,5 @@ py_library(
     testonly = 1,
     srcs = ["tempdir.py"],
     deps = [
-        "//third_party/py/pytest",
     ],
 )
diff --git a/tests/plugins/llvm_ir.py b/tests/plugins/llvm_ir.py
index e98f6bca3..69bb2f1c0 100644
--- a/tests/plugins/llvm_ir.py
+++ b/tests/plugins/llvm_ir.py
@@ -19,7 +19,7 @@
 
 from programl.util.py.runfiles_path import runfiles_path
 
-LLVM_IR = runfiles_path("programl/tests/data/llvm_ir")
+LLVM_IR = runfiles_path("tests/data/llvm_ir")
 
 
 def EnumerateLlvmIrs() -> Iterable[Tuple[str, str]]:
diff --git a/tests/plugins/llvm_program_graph.py b/tests/plugins/llvm_program_graph.py
index d53a48f76..e75a933ad 100644
--- a/tests/plugins/llvm_program_graph.py
+++ b/tests/plugins/llvm_program_graph.py
@@ -21,7 +21,7 @@
 from programl.util.py import pbutil
 from programl.util.py.runfiles_path import runfiles_path
 
-LLVM_IR_GRAPHS = runfiles_path("programl/tests/data/llvm_ir_graphs")
+LLVM_IR_GRAPHS = runfiles_path("tests/data/llvm_ir_graphs")
 
 
 def EnumerateLlvmProgramGraphs() -> Iterable[
diff --git a/tests/plugins/llvm_reachability_features.py b/tests/plugins/llvm_reachability_features.py
index f5e75a2d8..415899289 100644
--- a/tests/plugins/llvm_reachability_features.py
+++ b/tests/plugins/llvm_reachability_features.py
@@ -17,4 +17,4 @@
 
 # Directory containing ProgramGraphFeatures protocol messages for reachability
 # analysis.
-LLVM_REACHABILITY_FEATURES = runfiles_path("programl/tests/data/llvm_ir_reachability")
+LLVM_REACHABILITY_FEATURES = runfiles_path("tests/data/llvm_ir_reachability")
diff --git a/tests/requirements.txt b/tests/requirements.txt
index 3ea60ad26..c99c25b9c 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -1,11 +1,8 @@
-attrs==20.2.0  # Needed by pytest.
-importlib-metadata==2.0.0  # Needed by pytest.
-iniconfig==1.0.1  # Needed by pytest.
-packaging==20.4  # Needed by pytest.
-pluggy==0.13.1  # Needed by pytest.
-py==1.9.0  # Needed by pytest.
-pyparsing==2.4.7  # Needed by pytest.
 pytest==6.1.0
 pytest-benchmark==3.2.3
+pytest-cov==2.11.1
+pytest-mock==3.6.0
 pytest-shard==0.1.1
-zipp==3.2.0  # Needed by importlib-metadata.
+pytest-stress==1.0.1
+pytest-sugar==0.9.4
+pytest-xdist==2.2.1
diff --git a/tests/tasks/devmap/dataset/BUILD b/tests/tasks/devmap/dataset/BUILD
deleted file mode 100644
index 200672a66..000000000
--- a/tests/tasks/devmap/dataset/BUILD
+++ /dev/null
@@ -1,26 +0,0 @@
-# Copyright 2019-2020 the ProGraML authors.
-#
-# Contact Chris Cummins <chrisc.101@gmail.com>.
-#
-# 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.
-
-py_test(
-    name = "create_test",
-    timeout = "long",
-    srcs = ["create_test.py"],
-    deps = [
-        "//tasks/devmap/dataset:create",
-        "//tests:test_main",
-        "//tests/plugins:tempdir",
-    ],
-)
diff --git a/tests/util/py/BUILD b/tests/util/py/BUILD
index 05a642e5c..4166005f8 100644
--- a/tests/util/py/BUILD
+++ b/tests/util/py/BUILD
@@ -20,7 +20,6 @@ py_test(
     deps = [
         "//programl/util/py:decorators",
         "//tests:test_main",
-        "//third_party/py/pytest",
     ],
 )
 
@@ -32,7 +31,6 @@ py_test(
         "//tests:test_main",
         "//tests/data:test_protos_py",
         "//tests/plugins:tempdir",
-        "//third_party/py/pytest",
     ],
 )
 
@@ -43,6 +41,5 @@ py_test(
     deps = [
         "//programl/util/py:progress",
         "//tests:test_main",
-        "//third_party/py/pytest",
     ],
 )
diff --git a/tests/util/py/pbutil_test.py b/tests/util/py/pbutil_test.py
index 8a27dffa3..770bed6d3 100644
--- a/tests/util/py/pbutil_test.py
+++ b/tests/util/py/pbutil_test.py
@@ -46,7 +46,7 @@
 def test_ToFile_message_missing_required_fields(suffix):
     """Test that EncodeError is raised if required field is not set."""
     with tempfile.NamedTemporaryFile(prefix="labm8_proto_", suffix=suffix) as f:
-        proto = test_protos_pb2.TestMessage(number=1)
+        proto = test_protos_pb2.ATestMessage(number=1)
         with pytest.raises(pbutil.EncodeError):
             pbutil.ToFile(proto, pathlib.Path(f.name))
 
@@ -55,7 +55,7 @@ def test_ToFile_message_missing_required_fields(suffix):
 def test_ToFile_parent_directory_does_not_exist(suffix):
     """Test that FileNotFoundError raised if parent directory doesn't exist."""
     with tempfile.TemporaryDirectory() as d:
-        proto = test_protos_pb2.TestMessage(string="abc", number=1)
+        proto = test_protos_pb2.ATestMessage(string="abc", number=1)
         with pytest.raises(FileNotFoundError):
             pbutil.ToFile(proto, pathlib.Path(d) / "notadir" / f"proto{suffix}")
 
@@ -64,7 +64,7 @@ def test_ToFile_parent_directory_does_not_exist(suffix):
 def test_ToFile_path_is_directory(suffix):
     """Test that IsADirectoryError raised if path is a directory."""
     with tempfile.TemporaryDirectory(suffix=suffix) as d:
-        proto = test_protos_pb2.TestMessage(string="abc", number=1)
+        proto = test_protos_pb2.ATestMessage(string="abc", number=1)
         with pytest.raises(IsADirectoryError) as e_info:
             pbutil.ToFile(proto, pathlib.Path(d))
         assert str(e_info.value).endswith(f"Is a directory: '{d}'")
@@ -82,14 +82,14 @@ def test_FromString_empty_string():
 def test_FromString_empty_string_required_fields():
     """Test that an empty string raises DecodeError if uninitialized fields."""
     with pytest.raises(pbutil.DecodeError):
-        pbutil.FromString("", test_protos_pb2.TestMessage())
+        pbutil.FromString("", test_protos_pb2.ATestMessage())
 
 
 def test_FromString_empty_string_required_fields_uninitialized_okay():
     """Test that an empty string raises DecodeError if uninitialized fields."""
     proto = pbutil.FromString(
         "",
-        test_protos_pb2.TestMessage(),
+        test_protos_pb2.ATestMessage(),
         uninitialized_okay=True,
     )
     assert not proto.string
@@ -104,7 +104,7 @@ def test_FromString_valid_input():
 def test_FromString_DecodeError_unknown_field():
     """Test that DecodeError is raised if string contains unknown field."""
     with pytest.raises(pbutil.DecodeError) as e_info:
-        proto = pbutil.FromString(
+        pbutil.FromString(
             'foo: "bar"',
             test_protos_pb2.AnotherTestMessage(),
         )
@@ -123,7 +123,7 @@ def test_FromFile_FileNotFoundError(suffix):
         with pytest.raises(FileNotFoundError):
             pbutil.FromFile(
                 pathlib.Path(d) / f"proto{suffix}",
-                test_protos_pb2.TestMessage(),
+                test_protos_pb2.ATestMessage(),
             )
 
 
@@ -132,7 +132,7 @@ def test_FromFile_IsADirectoryError(suffix):
     """Test that IsADirectoryError raised if path is a directory."""
     with tempfile.TemporaryDirectory(prefix="labm8_proto_", suffix=suffix) as d:
         with pytest.raises(IsADirectoryError):
-            pbutil.FromFile(pathlib.Path(d), test_protos_pb2.TestMessage())
+            pbutil.FromFile(pathlib.Path(d), test_protos_pb2.ATestMessage())
 
 
 @pytest.mark.parametrize("suffix", SUFFIXES_TO_TEST)
@@ -144,7 +144,7 @@ def test_FromFile_required_fields_not_set(suffix):
             pathlib.Path(f.name),
         )
         with pytest.raises(pbutil.DecodeError) as e_info:
-            pbutil.FromFile(pathlib.Path(f.name), test_protos_pb2.TestMessage())
+            pbutil.FromFile(pathlib.Path(f.name), test_protos_pb2.ATestMessage())
         assert f"Required fields not set: '{f.name}'" == str(e_info.value)
 
 
@@ -152,14 +152,14 @@ def test_FromFile_required_fields_not_set(suffix):
 def test_FromFile_required_fields_not_set_uninitialized_okay(suffix):
     """Test that DecodeError not raised if required fields not set."""
     with tempfile.NamedTemporaryFile(prefix="labm8_proto_", suffix=suffix) as f:
-        proto_in = test_protos_pb2.AnotherTestMessage(number=1)
+        test_protos_pb2.AnotherTestMessage(number=1)
         pbutil.ToFile(
             test_protos_pb2.AnotherTestMessage(number=1),
             pathlib.Path(f.name),
         )
         pbutil.FromFile(
             pathlib.Path(f.name),
-            test_protos_pb2.TestMessage(),
+            test_protos_pb2.ATestMessage(),
             uninitialized_okay=True,
         )
 
@@ -169,10 +169,10 @@ def test_ToFile_FromFile_equivalence(suffix):
     """Test that ToFile() and FromFile() are symmetrical."""
     with tempfile.TemporaryDirectory(prefix="labm8_proto_") as d:
         path = pathlib.Path(d) / f"proto{suffix}"
-        proto_in = test_protos_pb2.TestMessage(string="abc", number=1)
+        proto_in = test_protos_pb2.ATestMessage(string="abc", number=1)
         pbutil.ToFile(proto_in, path)
         assert path.is_file()
-        proto_out = test_protos_pb2.TestMessage()
+        proto_out = test_protos_pb2.ATestMessage()
         pbutil.FromFile(path, proto_out)
         assert proto_out.string == "abc"
         assert proto_out.number == 1
@@ -184,25 +184,25 @@ def test_ToFile_FromFile_equivalence(suffix):
 
 def test_AssertFieldIsSet_invalid_field_name():
     """ValueError is raised if the requested field name does not exist."""
-    t = test_protos_pb2.TestMessage()
+    t = test_protos_pb2.ATestMessage()
     with pytest.raises(ValueError):
         pbutil.AssertFieldIsSet(t, "not_a_real_field")
 
 
 def test_AssertFieldIsSet_field_not_set():
     """ValueError is raised if the requested field is not set."""
-    t = test_protos_pb2.TestMessage()
+    t = test_protos_pb2.ATestMessage()
     with pytest.raises(pbutil.ProtoValueError) as e_info:
         pbutil.AssertFieldIsSet(t, "string")
-    assert "Field not set: 'TestMessage.string'" == str(e_info.value)
+    assert "Field not set: 'ATestMessage.string'" == str(e_info.value)
     with pytest.raises(pbutil.ProtoValueError) as e_info:
         pbutil.AssertFieldIsSet(t, "number")
-    assert "Field not set: 'TestMessage.number'" == str(e_info.value)
+    assert "Field not set: 'ATestMessage.number'" == str(e_info.value)
 
 
 def test_AssertFieldIsSet_field_is_set():
     """Field value is returned when field is set."""
-    t = test_protos_pb2.TestMessage()
+    t = test_protos_pb2.ATestMessage()
     t.string = "foo"
     t.number = 5
     assert "foo" == pbutil.AssertFieldIsSet(t, "string")
@@ -211,7 +211,7 @@ def test_AssertFieldIsSet_field_is_set():
 
 def test_AssertFieldIsSet_user_callback_custom_fail_message():
     """Test that the requested message is returned on callback fail."""
-    t = test_protos_pb2.TestMessage()
+    t = test_protos_pb2.ATestMessage()
     with pytest.raises(pbutil.ProtoValueError) as e_info:
         pbutil.AssertFieldIsSet(t, "string", "Hello, world!")
     assert "Hello, world!" == str(e_info.value)
@@ -222,7 +222,7 @@ def test_AssertFieldIsSet_user_callback_custom_fail_message():
 
 def test_AssFieldIsSet_oneof_field_no_return():
     """Test that no value is returned when a oneof field is set."""
-    t = test_protos_pb2.TestMessage()
+    t = test_protos_pb2.ATestMessage()
     t.option_a = 1
     assert pbutil.AssertFieldIsSet(t, "union_field") is None
     assert 1 == pbutil.AssertFieldIsSet(t, "option_a")
@@ -233,25 +233,25 @@ def test_AssFieldIsSet_oneof_field_no_return():
 
 def test_AssertFieldConstraint_invalid_field_name():
     """ValueError is raised if the requested field name does not exist."""
-    t = test_protos_pb2.TestMessage()
+    t = test_protos_pb2.ATestMessage()
     with pytest.raises(ValueError):
         pbutil.AssertFieldConstraint(t, "not_a_real_field")
 
 
 def test_AssertFieldConstraint_field_not_set():
     """ValueError is raised if the requested field is not set."""
-    t = test_protos_pb2.TestMessage()
+    t = test_protos_pb2.ATestMessage()
     with pytest.raises(pbutil.ProtoValueError) as e_info:
         pbutil.AssertFieldConstraint(t, "string")
-    assert "Field not set: 'TestMessage.string'" == str(e_info.value)
+    assert "Field not set: 'ATestMessage.string'" == str(e_info.value)
     with pytest.raises(pbutil.ProtoValueError) as e_info:
         pbutil.AssertFieldConstraint(t, "number")
-    assert "Field not set: 'TestMessage.number'" == str(e_info.value)
+    assert "Field not set: 'ATestMessage.number'" == str(e_info.value)
 
 
 def test_AssertFieldConstraint_no_callback_return_value():
     """Field value is returned when no callback and field is set."""
-    t = test_protos_pb2.TestMessage()
+    t = test_protos_pb2.ATestMessage()
     t.string = "foo"
     t.number = 5
     assert "foo" == pbutil.AssertFieldConstraint(t, "string")
@@ -260,7 +260,7 @@ def test_AssertFieldConstraint_no_callback_return_value():
 
 def test_AssertFieldConstraint_user_callback_passes():
     """Field value is returned when user callback passes."""
-    t = test_protos_pb2.TestMessage()
+    t = test_protos_pb2.ATestMessage()
     t.string = "foo"
     t.number = 5
     assert "foo" == pbutil.AssertFieldConstraint(
@@ -273,24 +273,24 @@ def test_AssertFieldConstraint_user_callback_passes():
 
 def test_AssertFieldConstraint_user_callback_fails():
     """ProtoValueError raised when when user callback fails."""
-    t = test_protos_pb2.TestMessage()
+    t = test_protos_pb2.ATestMessage()
     t.string = "foo"
     t.number = 5
     with pytest.raises(pbutil.ProtoValueError) as e_info:
         pbutil.AssertFieldConstraint(t, "string", lambda x: x == "bar")
-    assert "Field fails constraint check: 'TestMessage.string'" == str(
+    assert "Field fails constraint check: 'ATestMessage.string'" == str(
         e_info.value,
     )
     with pytest.raises(pbutil.ProtoValueError) as e_info:
         pbutil.AssertFieldConstraint(t, "number", lambda x: 10 < x < 100)
-    assert "Field fails constraint check: 'TestMessage.number'" == str(
+    assert "Field fails constraint check: 'ATestMessage.number'" == str(
         e_info.value,
     )
 
 
 def test_AssertFieldConstraint_user_callback_raises_exception():
     """If callback raises exception, it is passed to calling code."""
-    t = test_protos_pb2.TestMessage()
+    t = test_protos_pb2.ATestMessage()
     t.string = "foo"
 
     def CallbackWhichRaisesException(x):
@@ -304,7 +304,7 @@ def CallbackWhichRaisesException(x):
 
 def test_AssertFieldConstraint_user_callback_custom_fail_message():
     """Test that the requested message is returned on callback fail."""
-    t = test_protos_pb2.TestMessage()
+    t = test_protos_pb2.ATestMessage()
     t.string = "foo"
 
     # Constraint function fails.
@@ -326,45 +326,45 @@ def test_AssertFieldConstraint_user_callback_custom_fail_message():
 # ProtoBackedMixin tests.
 
 
-class TestMessage(pbutil.ProtoBackedMixin):
+class ATestMessage(pbutil.ProtoBackedMixin):
     """Example class which implements ProtoBackedMixin interface."""
 
-    proto_t = test_protos_pb2.TestMessage
+    proto_t = test_protos_pb2.ATestMessage
 
     def __init__(self, string: str, number: int):
         self.string = string
         self.number = number
 
-    def SetProto(self, proto: test_protos_pb2.TestMessage) -> None:
+    def SetProto(self, proto: test_protos_pb2.ATestMessage) -> None:
         """Set a protocol buffer representation."""
         proto.string = self.string
         proto.number = self.number
 
     @staticmethod
-    def FromProto(proto) -> "TestMessage":
+    def FromProto(proto) -> "ATestMessage":
         """Instantiate an object from protocol buffer message."""
-        return TestMessage(proto.string, proto.number)
+        return ATestMessage(proto.string, proto.number)
 
 
 def test_ProtoBackedMixin_FromProto():
     """Test FromProto constructor for proto backed class."""
-    proto = test_protos_pb2.TestMessage(string="Hello, world!", number=42)
-    instance = TestMessage.FromProto(proto)
+    proto = test_protos_pb2.ATestMessage(string="Hello, world!", number=42)
+    instance = ATestMessage.FromProto(proto)
     assert instance.string == "Hello, world!"
     assert instance.number == 42
 
 
 def test_ProtoBackedMixin_SetProto():
     """Test SetProto method for proto backed class."""
-    proto = test_protos_pb2.TestMessage()
-    TestMessage(string="Hello, world!", number=42).SetProto(proto)
+    proto = test_protos_pb2.ATestMessage()
+    ATestMessage(string="Hello, world!", number=42).SetProto(proto)
     assert proto.string == "Hello, world!"
     assert proto.number == 42
 
 
 def test_ProtoBackedMixin_ToProto():
     """Test FromProto constructor for proto backed class."""
-    instance = TestMessage(string="Hello, world!", number=42)
+    instance = ATestMessage(string="Hello, world!", number=42)
     proto = instance.ToProto()
     assert proto.string == "Hello, world!"
     assert proto.number == 42
@@ -375,11 +375,11 @@ def test_ProtoBackedMixin_FromProtoFile(suffix: str, tempdir: pathlib.Path):
     """Test FromProtoFile constructor for proto backed class."""
     proto_path = tempdir / f"proto{suffix}"
     pbutil.ToFile(
-        test_protos_pb2.TestMessage(string="Hello, world!", number=42),
+        test_protos_pb2.ATestMessage(string="Hello, world!", number=42),
         proto_path,
     )
 
-    instance = TestMessage.FromProtoFile(proto_path)
+    instance = ATestMessage.FromProtoFile(proto_path)
     assert instance.string == "Hello, world!"
     assert instance.number == 42
 
diff --git a/third_party/py/pytest/BUILD b/third_party/py/pytest/BUILD
deleted file mode 100644
index dbb9641df..000000000
--- a/third_party/py/pytest/BUILD
+++ /dev/null
@@ -1,26 +0,0 @@
-# A wrapper around pip package to pull in undeclared dependencies.
-
-load("@tests_requirements//:requirements.bzl", "requirement")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-py_library(
-    name = "pytest",
-    testonly = 1,
-    srcs = ["//third_party/py:empty.py"],
-    deps = [
-        requirement("pytest"),
-        requirement("attrs"),
-        requirement("iniconfig"),
-        requirement("packaging"),
-        requirement("pluggy"),
-        requirement("py"),
-        requirement("pyparsing"),
-        requirement("zipp"),
-        requirement("importlib-metadata"),
-        requirement("pytest-shard"),
-        requirement("pytest-benchmark"),
-    ],
-)