Start migrating to upstream pip package management

Rather than packaging up Debian packages, we can start using upstream
wheels.

There are still some issues to figure out. The biggest one is how to
make the upstream packages reproducible. Some packages like numpy are
distributed as wheels which should address the majority of concerns.
Some packages are only distributed in source form though. Those will
be compiled by rules_python against the host system. We'll need to
find a better way to deal with those.

For now you can use the new Python setup if you use
`--config=k8_upstream_python`. Once the majority of the issues are
fixed, I will get rid of the special casing. Then you'll only need
`--config=k8`.

Change-Id: I982ea057e0f47cdfda3d11d58275d7c07e34975a
Signed-off-by: Philipp Schrader <philipp.schrader@gmail.com>
diff --git a/tools/python/BUILD b/tools/python/BUILD
index 9939653..37101f5 100644
--- a/tools/python/BUILD
+++ b/tools/python/BUILD
@@ -1,4 +1,5 @@
 load("@rules_python//python:defs.bzl", "py_runtime_pair")
+load("@rules_python//python:pip.bzl", "compile_pip_requirements")
 
 py_runtime(
     name = "python3_runtime",
@@ -25,7 +26,52 @@
     target_compatible_with = [
         "@platforms//cpu:x86_64",
         "@platforms//os:linux",
+        "//tools/platforms/python:debian_bundled_python",
     ],
     toolchain = ":py_runtime",
     toolchain_type = "@rules_python//python:toolchain_type",
 )
+
+# Invoke this via "bazel run //tools/python:requirements.update".
+compile_pip_requirements(
+    name = "requirements",
+    requirements_in = "requirements.txt",
+    requirements_txt = "requirements.lock.txt",
+    tags = [
+        # The test pings pypi.org to make sure that the lock file matches the
+        # requirements file.
+        # TODO(phil): Disable this if it's too flaky.
+        "requires-network",
+    ],
+)
+
+py_runtime(
+    name = "upstream_python3_runtime",
+    files = [
+        "runtime_binary.sh",
+        "@python3_9_x86_64-unknown-linux-gnu//:files",
+    ],
+    interpreter = "runtime_binary.sh",
+    python_version = "PY3",
+)
+
+py_runtime_pair(
+    name = "upstream_py_runtime",
+    py2_runtime = None,
+    py3_runtime = ":upstream_python3_runtime",
+)
+
+toolchain(
+    name = "upstream_python_toolchain",
+    exec_compatible_with = [
+        "@platforms//cpu:x86_64",
+        "@platforms//os:linux",
+    ],
+    target_compatible_with = [
+        "@platforms//cpu:x86_64",
+        "@platforms//os:linux",
+        "//tools/platforms/python:upstream_bundled_python",
+    ],
+    toolchain = ":upstream_py_runtime",
+    toolchain_type = "@rules_python//python:toolchain_type",
+)
diff --git a/tools/python/pip_configure.py b/tools/python/pip_configure.py
new file mode 100644
index 0000000..4f9dff1
--- /dev/null
+++ b/tools/python/pip_configure.py
@@ -0,0 +1,80 @@
+"""This script generates contents of the @pip repository.
+
+This repository is just a way to have simple-to-write targets to specify in
+BUILD files.  E.g. you can use @pip//numpy.
+
+The pip package names are normalized:
+- Letters are lowercased.
+- Periods are replaced by underscores.
+- Dashes are replaced by underscores.
+
+We do this normalization because it produces predictable names. pip allows a
+wide range of names to refer to the same package. That would be annoying to use
+in BUILD files.
+"""
+
+import sys
+import textwrap
+from pathlib import Path
+
+
+def parse_requirements(requirements_path: Path) -> list[str]:
+    """Parses tools/python/requirements.txt.
+
+    We don't want to parse the lock file since we really only want users to
+    depend on explicitly requested pip packages. We don't want users to depend
+    on transitive dependencies of our requested pip packages.
+    """
+    result = []
+    for line in requirements_path.read_text().splitlines():
+        # Ignore line comments.
+        if not line or line.startswith("#"):
+            continue
+
+        # Remove any inline comments that may or may not exist.
+        # E.g:
+        # numpy==1.2.3  # needed because we like it.
+        result.append(line.split()[0])
+
+    return result
+
+
+def generate_build_files(requirements: list[str]) -> None:
+    """Generate all the BUILD files in the "pip" external repository.
+
+    We create files like this:
+
+        external/pip/numpy/BUILD
+
+    and in that BUILD file we create a "numpy" target. That lets users depend
+    on "@pip//numpy".
+    """
+    for requirement in requirements:
+        requirement = requirement.lower().replace("-", "_").replace(".", "_")
+        requirement_dir = Path(requirement)
+        requirement_dir.mkdir()
+        # We could use an alias() here, but that behaves strangely with
+        # target_compatible_with pre-6.0.
+        (requirement_dir / "BUILD").write_text(
+            textwrap.dedent(f"""\
+            load("@pip_deps//:requirements.bzl", "requirement")
+            py_library(
+                name = "{requirement}",
+                deps = [requirement("{requirement}")],
+                visibility = ["//visibility:public"],
+                target_compatible_with = [
+                    "@//tools/platforms/python:upstream_bundled_python",
+                ],
+            )
+            """))
+
+
+def main(argv):
+    requirements_path = Path(argv[1])
+    requirements = parse_requirements(requirements_path)
+
+    generate_build_files(requirements)
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv))
diff --git a/tools/python/repo_defs.bzl b/tools/python/repo_defs.bzl
new file mode 100644
index 0000000..c0c212b
--- /dev/null
+++ b/tools/python/repo_defs.bzl
@@ -0,0 +1,33 @@
+def _pip_configure_impl(repository_ctx):
+    """Runs tools/python/pip_configure.py."""
+    script_path = repository_ctx.path(repository_ctx.attr._script).realpath
+    interpreter_path = repository_ctx.path(repository_ctx.attr._interpreter).realpath
+    requirements_path = repository_ctx.path(repository_ctx.attr._requirements).realpath
+
+    script_result = repository_ctx.execute([
+        interpreter_path,
+        "-BSs",
+        script_path,
+        requirements_path,
+    ])
+    if script_result.return_code != 0:
+        fail("{} failed: {} ({})".format(
+            script_path,
+            script_result.stdout,
+            script_result.stderr,
+        ))
+
+pip_configure = repository_rule(
+    implementation = _pip_configure_impl,
+    attrs = {
+        "_interpreter": attr.label(
+            default = "@python3_9_x86_64-unknown-linux-gnu//:bin/python3",
+        ),
+        "_script": attr.label(
+            default = "@//tools/python:pip_configure.py",
+        ),
+        "_requirements": attr.label(
+            default = "@//tools/python:requirements.txt",
+        ),
+    },
+)
diff --git a/tools/python/requirements.lock.txt b/tools/python/requirements.lock.txt
new file mode 100644
index 0000000..337eaf8
--- /dev/null
+++ b/tools/python/requirements.lock.txt
@@ -0,0 +1,224 @@
+#
+# This file is autogenerated by pip-compile with python 3.9
+# To update, run:
+#
+#    bazel run //tools/python:requirements.update
+#
+cycler==0.11.0 \
+    --hash=sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3 \
+    --hash=sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f
+    # via matplotlib
+fonttools==4.28.5 \
+    --hash=sha256:545c05d0f7903a863c2020e07b8f0a57517f2c40d940bded77076397872d14ca \
+    --hash=sha256:edf251d5d2cc0580d5f72de4621c338d8c66c5f61abb50cf486640f73c8194d5
+    # via matplotlib
+kiwisolver==1.3.2 \
+    --hash=sha256:0007840186bacfaa0aba4466d5890334ea5938e0bb7e28078a0eb0e63b5b59d5 \
+    --hash=sha256:19554bd8d54cf41139f376753af1a644b63c9ca93f8f72009d50a2080f870f77 \
+    --hash=sha256:1d45d1c74f88b9f41062716c727f78f2a59a5476ecbe74956fafb423c5c87a76 \
+    --hash=sha256:1d819553730d3c2724582124aee8a03c846ec4362ded1034c16fb3ef309264e6 \
+    --hash=sha256:2210f28778c7d2ee13f3c2a20a3a22db889e75f4ec13a21072eabb5693801e84 \
+    --hash=sha256:22521219ca739654a296eea6d4367703558fba16f98688bd8ce65abff36eaa84 \
+    --hash=sha256:25405f88a37c5f5bcba01c6e350086d65e7465fd1caaf986333d2a045045a223 \
+    --hash=sha256:2b65bd35f3e06a47b5c30ea99e0c2b88f72c6476eedaf8cfbc8e66adb5479dcf \
+    --hash=sha256:2ddb500a2808c100e72c075cbb00bf32e62763c82b6a882d403f01a119e3f402 \
+    --hash=sha256:2f8f6c8f4f1cff93ca5058d6ec5f0efda922ecb3f4c5fb76181f327decff98b8 \
+    --hash=sha256:30fa008c172355c7768159983a7270cb23838c4d7db73d6c0f6b60dde0d432c6 \
+    --hash=sha256:3dbb3cea20b4af4f49f84cffaf45dd5f88e8594d18568e0225e6ad9dec0e7967 \
+    --hash=sha256:4116ba9a58109ed5e4cb315bdcbff9838f3159d099ba5259c7c7fb77f8537492 \
+    --hash=sha256:44e6adf67577dbdfa2d9f06db9fbc5639afefdb5bf2b4dfec25c3a7fbc619536 \
+    --hash=sha256:5326ddfacbe51abf9469fe668944bc2e399181a2158cb5d45e1d40856b2a0589 \
+    --hash=sha256:70adc3658138bc77a36ce769f5f183169bc0a2906a4f61f09673f7181255ac9b \
+    --hash=sha256:72be6ebb4e92520b9726d7146bc9c9b277513a57a38efcf66db0620aec0097e0 \
+    --hash=sha256:7843b1624d6ccca403a610d1277f7c28ad184c5aa88a1750c1a999754e65b439 \
+    --hash=sha256:7ba5a1041480c6e0a8b11a9544d53562abc2d19220bfa14133e0cdd9967e97af \
+    --hash=sha256:80efd202108c3a4150e042b269f7c78643420cc232a0a771743bb96b742f838f \
+    --hash=sha256:82f49c5a79d3839bc8f38cb5f4bfc87e15f04cbafa5fbd12fb32c941cb529cfb \
+    --hash=sha256:83d2c9db5dfc537d0171e32de160461230eb14663299b7e6d18ca6dca21e4977 \
+    --hash=sha256:8d93a1095f83e908fc253f2fb569c2711414c0bfd451cab580466465b235b470 \
+    --hash=sha256:8dc3d842fa41a33fe83d9f5c66c0cc1f28756530cd89944b63b072281e852031 \
+    --hash=sha256:9661a04ca3c950a8ac8c47f53cbc0b530bce1b52f516a1e87b7736fec24bfff0 \
+    --hash=sha256:a498bcd005e8a3fedd0022bb30ee0ad92728154a8798b703f394484452550507 \
+    --hash=sha256:a7a4cf5bbdc861987a7745aed7a536c6405256853c94abc9f3287c3fa401b174 \
+    --hash=sha256:b5074fb09429f2b7bc82b6fb4be8645dcbac14e592128beeff5461dcde0af09f \
+    --hash=sha256:b6a5431940f28b6de123de42f0eb47b84a073ee3c3345dc109ad550a3307dd28 \
+    --hash=sha256:ba677bcaff9429fd1bf01648ad0901cea56c0d068df383d5f5856d88221fe75b \
+    --hash=sha256:bcadb05c3d4794eb9eee1dddf1c24215c92fb7b55a80beae7a60530a91060560 \
+    --hash=sha256:bf7eb45d14fc036514c09554bf983f2a72323254912ed0c3c8e697b62c4c158f \
+    --hash=sha256:c358721aebd40c243894298f685a19eb0491a5c3e0b923b9f887ef1193ddf829 \
+    --hash=sha256:c4550a359c5157aaf8507e6820d98682872b9100ce7607f8aa070b4b8af6c298 \
+    --hash=sha256:c6572c2dab23c86a14e82c245473d45b4c515314f1f859e92608dcafbd2f19b8 \
+    --hash=sha256:cba430db673c29376135e695c6e2501c44c256a81495da849e85d1793ee975ad \
+    --hash=sha256:dedc71c8eb9c5096037766390172c34fb86ef048b8e8958b4e484b9e505d66bc \
+    --hash=sha256:e6f5eb2f53fac7d408a45fbcdeda7224b1cfff64919d0f95473420a931347ae9 \
+    --hash=sha256:ec2eba188c1906b05b9b49ae55aae4efd8150c61ba450e6721f64620c50b59eb \
+    --hash=sha256:ee040a7de8d295dbd261ef2d6d3192f13e2b08ec4a954de34a6fb8ff6422e24c \
+    --hash=sha256:eedd3b59190885d1ebdf6c5e0ca56828beb1949b4dfe6e5d0256a461429ac386 \
+    --hash=sha256:f441422bb313ab25de7b3dbfd388e790eceb76ce01a18199ec4944b369017009 \
+    --hash=sha256:f8eb7b6716f5b50e9c06207a14172cf2de201e41912ebe732846c02c830455b9 \
+    --hash=sha256:fc4453705b81d03568d5b808ad8f09c77c47534f6ac2e72e733f9ca4714aa75c
+    # via matplotlib
+matplotlib==3.5.1 \
+    --hash=sha256:14334b9902ec776461c4b8c6516e26b450f7ebe0b3ef8703bf5cdfbbaecf774a \
+    --hash=sha256:2252bfac85cec7af4a67e494bfccf9080bcba8a0299701eab075f48847cca907 \
+    --hash=sha256:2e3484d8455af3fdb0424eae1789af61f6a79da0c80079125112fd5c1b604218 \
+    --hash=sha256:34a1fc29f8f96e78ec57a5eff5e8d8b53d3298c3be6df61e7aa9efba26929522 \
+    --hash=sha256:3e66497cd990b1a130e21919b004da2f1dc112132c01ac78011a90a0f9229778 \
+    --hash=sha256:40e0d7df05e8efe60397c69b467fc8f87a2affeb4d562fe92b72ff8937a2b511 \
+    --hash=sha256:456cc8334f6d1124e8ff856b42d2cc1c84335375a16448189999496549f7182b \
+    --hash=sha256:506b210cc6e66a0d1c2bb765d055f4f6bc2745070fb1129203b67e85bbfa5c18 \
+    --hash=sha256:53273c5487d1c19c3bc03b9eb82adaf8456f243b97ed79d09dded747abaf1235 \
+    --hash=sha256:577ed20ec9a18d6bdedb4616f5e9e957b4c08563a9f985563a31fd5b10564d2a \
+    --hash=sha256:6803299cbf4665eca14428d9e886de62e24f4223ac31ab9c5d6d5339a39782c7 \
+    --hash=sha256:68fa30cec89b6139dc559ed6ef226c53fd80396da1919a1b5ef672c911aaa767 \
+    --hash=sha256:6c094e4bfecd2fa7f9adffd03d8abceed7157c928c2976899de282f3600f0a3d \
+    --hash=sha256:778d398c4866d8e36ee3bf833779c940b5f57192fa0a549b3ad67bc4c822771b \
+    --hash=sha256:7a350ca685d9f594123f652ba796ee37219bf72c8e0fc4b471473d87121d6d34 \
+    --hash=sha256:87900c67c0f1728e6db17c6809ec05c025c6624dcf96a8020326ea15378fe8e7 \
+    --hash=sha256:8a77906dc2ef9b67407cec0bdbf08e3971141e535db888974a915be5e1e3efc6 \
+    --hash=sha256:8e70ae6475cfd0fad3816dcbf6cac536dc6f100f7474be58d59fa306e6e768a4 \
+    --hash=sha256:abf67e05a1b7f86583f6ebd01f69b693b9c535276f4e943292e444855870a1b8 \
+    --hash=sha256:b04fc29bcef04d4e2d626af28d9d892be6aba94856cb46ed52bcb219ceac8943 \
+    --hash=sha256:b19a761b948e939a9e20173aaae76070025f0024fc8f7ba08bef22a5c8573afc \
+    --hash=sha256:b2e9810e09c3a47b73ce9cab5a72243a1258f61e7900969097a817232246ce1c \
+    --hash=sha256:b71f3a7ca935fc759f2aed7cec06cfe10bc3100fadb5dbd9c435b04e557971e1 \
+    --hash=sha256:b8a4fb2a0c5afbe9604f8a91d7d0f27b1832c3e0b5e365f95a13015822b4cd65 \
+    --hash=sha256:bb1c613908f11bac270bc7494d68b1ef6e7c224b7a4204d5dacf3522a41e2bc3 \
+    --hash=sha256:d24e5bb8028541ce25e59390122f5e48c8506b7e35587e5135efcb6471b4ac6c \
+    --hash=sha256:d70a32ee1f8b55eed3fd4e892f0286df8cccc7e0475c11d33b5d0a148f5c7599 \
+    --hash=sha256:e293b16cf303fe82995e41700d172a58a15efc5331125d08246b520843ef21ee \
+    --hash=sha256:e2f28a07b4f82abb40267864ad7b3a4ed76f1b1663e81c7efc84a9b9248f672f \
+    --hash=sha256:e3520a274a0e054e919f5b3279ee5dbccf5311833819ccf3399dab7c83e90a25 \
+    --hash=sha256:e3b6f3fd0d8ca37861c31e9a7cab71a0ef14c639b4c95654ea1dd153158bf0df \
+    --hash=sha256:e486f60db0cd1c8d68464d9484fd2a94011c1ac8593d765d0211f9daba2bd535 \
+    --hash=sha256:e8c87cdaf06fd7b2477f68909838ff4176f105064a72ca9d24d3f2a29f73d393 \
+    --hash=sha256:edf5e4e1d5fb22c18820e8586fb867455de3b109c309cb4fce3aaed85d9468d1 \
+    --hash=sha256:fe8d40c434a8e2c68d64c6d6a04e77f21791a93ff6afe0dce169597c110d3079
+    # via -r tools/python/requirements.txt
+numpy==1.21.5 \
+    --hash=sha256:00c9fa73a6989895b8815d98300a20ac993c49ac36c8277e8ffeaa3631c0dbbb \
+    --hash=sha256:025b497014bc33fc23897859350f284323f32a2fff7654697f5a5fc2a19e9939 \
+    --hash=sha256:08de8472d9f7571f9d51b27b75e827f5296295fa78817032e84464be8bb905bc \
+    --hash=sha256:1964db2d4a00348b7a60ee9d013c8cb0c566644a589eaa80995126eac3b99ced \
+    --hash=sha256:2a9add27d7fc0fdb572abc3b2486eb3b1395da71e0254c5552b2aad2a18b5441 \
+    --hash=sha256:2d8adfca843bc46ac199a4645233f13abf2011a0b2f4affc5c37cd552626f27b \
+    --hash=sha256:301e408a052fdcda5cdcf03021ebafc3c6ea093021bf9d1aa47c54d48bdad166 \
+    --hash=sha256:311283acf880cfcc20369201bd75da907909afc4666966c7895cbed6f9d2c640 \
+    --hash=sha256:341dddcfe3b7b6427a28a27baa59af5ad51baa59bfec3264f1ab287aa3b30b13 \
+    --hash=sha256:3a5098df115340fb17fc93867317a947e1dcd978c3888c5ddb118366095851f8 \
+    --hash=sha256:3c978544be9e04ed12016dd295a74283773149b48f507d69b36f91aa90a643e5 \
+    --hash=sha256:3d893b0871322eaa2f8c7072cdb552d8e2b27645b7875a70833c31e9274d4611 \
+    --hash=sha256:4fe6a006557b87b352c04596a6e3f12a57d6e5f401d804947bd3188e6b0e0e76 \
+    --hash=sha256:507c05c7a37b3683eb08a3ff993bd1ee1e6c752f77c2f275260533b265ecdb6c \
+    --hash=sha256:58ca1d7c8aef6e996112d0ce873ac9dfa1eaf4a1196b4ff7ff73880a09923ba7 \
+    --hash=sha256:61bada43d494515d5b122f4532af226fdb5ee08fe5b5918b111279843dc6836a \
+    --hash=sha256:69a5a8d71c308d7ef33ef72371c2388a90e3495dbb7993430e674006f94797d5 \
+    --hash=sha256:6a5928bc6241264dce5ed509e66f33676fc97f464e7a919edc672fb5532221ee \
+    --hash=sha256:7b9d6b14fc9a4864b08d1ba57d732b248f0e482c7b2ff55c313137e3ed4d8449 \
+    --hash=sha256:a7c4b701ca418cd39e28ec3b496e6388fe06de83f5f0cb74794fa31cfa384c02 \
+    --hash=sha256:a7e8f6216f180f3fd4efb73de5d1eaefb5f5a1ee5b645c67333033e39440e63a \
+    --hash=sha256:b545ebadaa2b878c8630e5bcdb97fc4096e779f335fc0f943547c1c91540c815 \
+    --hash=sha256:c293d3c0321996cd8ffe84215ffe5d269fd9d1d12c6f4ffe2b597a7c30d3e593 \
+    --hash=sha256:c5562bcc1a9b61960fc8950ade44d00e3de28f891af0acc96307c73613d18f6e \
+    --hash=sha256:ca9c23848292c6fe0a19d212790e62f398fd9609aaa838859be8459bfbe558aa \
+    --hash=sha256:cc1b30205d138d1005adb52087ff45708febbef0e420386f58664f984ef56954 \
+    --hash=sha256:dbce7adeb66b895c6aaa1fad796aaefc299ced597f6fbd9ceddb0dd735245354 \
+    --hash=sha256:dc4b2fb01f1b4ddbe2453468ea0719f4dbb1f5caa712c8b21bb3dd1480cd30d9 \
+    --hash=sha256:eed2afaa97ec33b4411995be12f8bdb95c87984eaa28d76cf628970c8a2d689a \
+    --hash=sha256:fc7a7d7b0ed72589fd8b8486b9b42a564f10b8762be8bd4d9df94b807af4a089
+    # via
+    #   -r tools/python/requirements.txt
+    #   matplotlib
+    #   scipy
+packaging==21.3 \
+    --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \
+    --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522
+    # via matplotlib
+pillow==8.4.0 \
+    --hash=sha256:066f3999cb3b070a95c3652712cffa1a748cd02d60ad7b4e485c3748a04d9d76 \
+    --hash=sha256:0a0956fdc5defc34462bb1c765ee88d933239f9a94bc37d132004775241a7585 \
+    --hash=sha256:0b052a619a8bfcf26bd8b3f48f45283f9e977890263e4571f2393ed8898d331b \
+    --hash=sha256:1394a6ad5abc838c5cd8a92c5a07535648cdf6d09e8e2d6df916dfa9ea86ead8 \
+    --hash=sha256:1bc723b434fbc4ab50bb68e11e93ce5fb69866ad621e3c2c9bdb0cd70e345f55 \
+    --hash=sha256:244cf3b97802c34c41905d22810846802a3329ddcb93ccc432870243211c79fc \
+    --hash=sha256:25a49dc2e2f74e65efaa32b153527fc5ac98508d502fa46e74fa4fd678ed6645 \
+    --hash=sha256:2e4440b8f00f504ee4b53fe30f4e381aae30b0568193be305256b1462216feff \
+    --hash=sha256:3862b7256046fcd950618ed22d1d60b842e3a40a48236a5498746f21189afbbc \
+    --hash=sha256:3eb1ce5f65908556c2d8685a8f0a6e989d887ec4057326f6c22b24e8a172c66b \
+    --hash=sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6 \
+    --hash=sha256:493cb4e415f44cd601fcec11c99836f707bb714ab03f5ed46ac25713baf0ff20 \
+    --hash=sha256:4acc0985ddf39d1bc969a9220b51d94ed51695d455c228d8ac29fcdb25810e6e \
+    --hash=sha256:5503c86916d27c2e101b7f71c2ae2cddba01a2cf55b8395b0255fd33fa4d1f1a \
+    --hash=sha256:5b7bb9de00197fb4261825c15551adf7605cf14a80badf1761d61e59da347779 \
+    --hash=sha256:5e9ac5f66616b87d4da618a20ab0a38324dbe88d8a39b55be8964eb520021e02 \
+    --hash=sha256:620582db2a85b2df5f8a82ddeb52116560d7e5e6b055095f04ad828d1b0baa39 \
+    --hash=sha256:62cc1afda735a8d109007164714e73771b499768b9bb5afcbbee9d0ff374b43f \
+    --hash=sha256:70ad9e5c6cb9b8487280a02c0ad8a51581dcbbe8484ce058477692a27c151c0a \
+    --hash=sha256:72b9e656e340447f827885b8d7a15fc8c4e68d410dc2297ef6787eec0f0ea409 \
+    --hash=sha256:72cbcfd54df6caf85cc35264c77ede902452d6df41166010262374155947460c \
+    --hash=sha256:792e5c12376594bfcb986ebf3855aa4b7c225754e9a9521298e460e92fb4a488 \
+    --hash=sha256:7b7017b61bbcdd7f6363aeceb881e23c46583739cb69a3ab39cb384f6ec82e5b \
+    --hash=sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d \
+    --hash=sha256:82aafa8d5eb68c8463b6e9baeb4f19043bb31fefc03eb7b216b51e6a9981ae09 \
+    --hash=sha256:84c471a734240653a0ec91dec0996696eea227eafe72a33bd06c92697728046b \
+    --hash=sha256:8c803ac3c28bbc53763e6825746f05cc407b20e4a69d0122e526a582e3b5e153 \
+    --hash=sha256:93ce9e955cc95959df98505e4608ad98281fff037350d8c2671c9aa86bcf10a9 \
+    --hash=sha256:9a3e5ddc44c14042f0844b8cf7d2cd455f6cc80fd7f5eefbe657292cf601d9ad \
+    --hash=sha256:a4901622493f88b1a29bd30ec1a2f683782e57c3c16a2dbc7f2595ba01f639df \
+    --hash=sha256:a5a4532a12314149d8b4e4ad8ff09dde7427731fcfa5917ff16d0291f13609df \
+    --hash=sha256:b8831cb7332eda5dc89b21a7bce7ef6ad305548820595033a4b03cf3091235ed \
+    --hash=sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed \
+    --hash=sha256:c70e94281588ef053ae8998039610dbd71bc509e4acbc77ab59d7d2937b10698 \
+    --hash=sha256:c8a17b5d948f4ceeceb66384727dde11b240736fddeda54ca740b9b8b1556b29 \
+    --hash=sha256:d82cdb63100ef5eedb8391732375e6d05993b765f72cb34311fab92103314649 \
+    --hash=sha256:d89363f02658e253dbd171f7c3716a5d340a24ee82d38aab9183f7fdf0cdca49 \
+    --hash=sha256:d99ec152570e4196772e7a8e4ba5320d2d27bf22fdf11743dd882936ed64305b \
+    --hash=sha256:ddc4d832a0f0b4c52fff973a0d44b6c99839a9d016fe4e6a1cb8f3eea96479c2 \
+    --hash=sha256:e3dacecfbeec9a33e932f00c6cd7996e62f53ad46fbe677577394aaa90ee419a \
+    --hash=sha256:eb9fc393f3c61f9054e1ed26e6fe912c7321af2f41ff49d3f83d05bacf22cc78
+    # via matplotlib
+pyparsing==3.0.6 \
+    --hash=sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4 \
+    --hash=sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81
+    # via
+    #   matplotlib
+    #   packaging
+python-dateutil==2.8.2 \
+    --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \
+    --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9
+    # via matplotlib
+scipy==1.7.3 \
+    --hash=sha256:033ce76ed4e9f62923e1f8124f7e2b0800db533828c853b402c7eec6e9465d80 \
+    --hash=sha256:173308efba2270dcd61cd45a30dfded6ec0085b4b6eb33b5eb11ab443005e088 \
+    --hash=sha256:21b66200cf44b1c3e86495e3a436fc7a26608f92b8d43d344457c54f1c024cbc \
+    --hash=sha256:2c56b820d304dffcadbbb6cbfbc2e2c79ee46ea291db17e288e73cd3c64fefa9 \
+    --hash=sha256:304dfaa7146cffdb75fbf6bb7c190fd7688795389ad060b970269c8576d038e9 \
+    --hash=sha256:3f78181a153fa21c018d346f595edd648344751d7f03ab94b398be2ad083ed3e \
+    --hash=sha256:4d242d13206ca4302d83d8a6388c9dfce49fc48fdd3c20efad89ba12f785bf9e \
+    --hash=sha256:5d1cc2c19afe3b5a546ede7e6a44ce1ff52e443d12b231823268019f608b9b12 \
+    --hash=sha256:5f2cfc359379c56b3a41b17ebd024109b2049f878badc1e454f31418c3a18436 \
+    --hash=sha256:65bd52bf55f9a1071398557394203d881384d27b9c2cad7df9a027170aeaef93 \
+    --hash=sha256:7edd9a311299a61e9919ea4192dd477395b50c014cdc1a1ac572d7c27e2207fa \
+    --hash=sha256:8499d9dd1459dc0d0fe68db0832c3d5fc1361ae8e13d05e6849b358dc3f2c279 \
+    --hash=sha256:866ada14a95b083dd727a845a764cf95dd13ba3dc69a16b99038001b05439709 \
+    --hash=sha256:87069cf875f0262a6e3187ab0f419f5b4280d3dcf4811ef9613c605f6e4dca95 \
+    --hash=sha256:93378f3d14fff07572392ce6a6a2ceb3a1f237733bd6dcb9eb6a2b29b0d19085 \
+    --hash=sha256:95c2d250074cfa76715d58830579c64dff7354484b284c2b8b87e5a38321672c \
+    --hash=sha256:ab5875facfdef77e0a47d5fd39ea178b58e60e454a4c85aa1e52fcb80db7babf \
+    --hash=sha256:b0e0aeb061a1d7dcd2ed59ea57ee56c9b23dd60100825f98238c06ee5cc4467e \
+    --hash=sha256:b78a35c5c74d336f42f44106174b9851c783184a85a3fe3e68857259b37b9ffb \
+    --hash=sha256:c9e04d7e9b03a8a6ac2045f7c5ef741be86727d8f49c45db45f244bdd2bcff17 \
+    --hash=sha256:ca36e7d9430f7481fc7d11e015ae16fbd5575615a8e9060538104778be84addf \
+    --hash=sha256:ceebc3c4f6a109777c0053dfa0282fddb8893eddfb0d598574acfb734a926168 \
+    --hash=sha256:e2c036492e673aad1b7b0d0ccdc0cb30a968353d2c4bf92ac8e73509e1bf212c \
+    --hash=sha256:eb326658f9b73c07081300daba90a8746543b5ea177184daed26528273157294 \
+    --hash=sha256:eb7ae2c4dbdb3c9247e07acc532f91077ae6dbc40ad5bd5dca0bb5a176ee9bda \
+    --hash=sha256:edad1cf5b2ce1912c4d8ddad20e11d333165552aba262c882e28c78bbc09dbf6 \
+    --hash=sha256:eef93a446114ac0193a7b714ce67659db80caf940f3232bad63f4c7a81bc18df \
+    --hash=sha256:f7eaea089345a35130bc9a39b89ec1ff69c208efa97b3f8b25ea5d4c41d88094 \
+    --hash=sha256:f99d206db1f1ae735a8192ab93bd6028f3a42f6fa08467d37a14eb96c9dd34a3
+    # via -r tools/python/requirements.txt
+six==1.16.0 \
+    --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
+    --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
+    # via python-dateutil
diff --git a/tools/python/requirements.txt b/tools/python/requirements.txt
new file mode 100644
index 0000000..7bb0cc1
--- /dev/null
+++ b/tools/python/requirements.txt
@@ -0,0 +1,5 @@
+# After updating this file, run:
+# $ bazel run //tools/python:requirements.update
+matplotlib
+numpy
+scipy
diff --git a/tools/python/runtime_binary.sh b/tools/python/runtime_binary.sh
index a36f085..1a4b3dc 100755
--- a/tools/python/runtime_binary.sh
+++ b/tools/python/runtime_binary.sh
@@ -19,22 +19,26 @@
 # example in a genrule the Python runtime is in the runfiles folder of the
 # tool, not of the genrule.
 # TODO(philipp): Is there a better way to do this?
-BASE_PATH=""
+PYTHON_BIN=""
 for path in ${PYTHONPATH//:/ }; do
-  if [[ "$path" == *.runfiles/python_repo ]]; then
-    BASE_PATH="$path"
-    export LD_LIBRARY_PATH="$path"/lib/x86_64-linux-gnu:"$path"/usr/lib:"$path"/usr/lib/x86_64-linux-gnu:"$path"/../matplotlib_repo/usr/lib
+  if [[ "$path" == *.runfiles/python3_9_x86_64-unknown-linux-gnu ]]; then
+    PYTHON_BIN="$path"/bin/python3
+    export LD_LIBRARY_PATH="$path"/lib
+    break
+  elif [[ "$path" == *.runfiles/python_repo ]]; then
+    PYTHON_BIN="$path"/usr/bin/python3
+    LD_LIBRARY_PATH="${path}/lib/x86_64-linux-gnu:${path}/usr/lib:${path}/usr/lib/x86_64-linux-gnu:${path}/../matplotlib_repo/usr/lib"
+    LD_LIBRARY_PATH+=":${path}/usr/lib/lapack:${path}/usr/lib/libblas:${path}/../matplotlib_repo/rpathed3/usr/lib:${path}/usr/lib/x86_64-linux-gnu/lapack:${path}/usr/lib/x86_64-linux-gnu/blas"
+    export LD_LIBRARY_PATH
     break
   fi
 done
 
-if [[ -z "$BASE_PATH" ]]; then
+if [[ -z "$PYTHON_BIN" ]]; then
   echo "Could not find Python base path." >&2
   echo "More sophisticated logic may be needed." >&2
   exit 1
 fi
 
-export LD_LIBRARY_PATH="${BASE_PATH}/usr/lib/lapack:${BASE_PATH}/usr/lib/libblas:${BASE_PATH}/usr/lib/x86_64-linux-gnu:${BASE_PATH}/../matplotlib_repo/rpathed3/usr/lib:${BASE_PATH}/usr/lib/x86_64-linux-gnu/lapack:${BASE_PATH}/usr/lib/x86_64-linux-gnu/blas"
-
 # Prevent Python from importing the host's installed packages.
-exec "$BASE_PATH"/usr/bin/python3 -sS "$@"
+exec "$PYTHON_BIN" -sS "$@"