Add mkdocs bazel rules

I want to add some more detailed AOS documentation. mkdocs seems like a
reasonable choice, since it allows markdown (so GitHub and Gerrit will
still be able to render it even for people not using mkdocs), and it
was reasonably straightforwards to integrate.

Now that I am calling some python code that is using pip in the exec
configuration, also needed to set --host_platform for the
k8_upstream_python config.

Change-Id: I987f531759170272686f6488271be732827d0f9b
Signed-off-by: James Kuszmaul <james.kuszmaul@bluerivertech.com>
diff --git a/.bazelrc b/.bazelrc
index 5b9fb37..9c94391 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -21,7 +21,7 @@
 
 # Shortcuts for selecting the target platform.
 build:k8 --platforms=//tools/platforms:linux_x86
-build:k8_upstream_python --platforms=//tools/platforms:linux_x86_upstream_python
+build:k8_upstream_python --platforms=//tools/platforms:linux_x86_upstream_python --host_platform=//tools/platforms:linux_x86_upstream_python
 build:roborio --platforms=//tools/platforms:linux_roborio
 build:roborio --platform_suffix=-roborio
 build:armv7 --platforms=//tools/platforms:linux_armv7
diff --git a/documentation/BUILD b/documentation/BUILD
index 5099a7d..a75eb46 100644
--- a/documentation/BUILD
+++ b/documentation/BUILD
@@ -1,7 +1,16 @@
 load("//tools/build_rules:pandoc.bzl", "pandoc_html")
 
+exports_files(["mkdocs_bin.py"])
+
 pandoc_html(
     name = "index",
     src = "README.md",
     target_compatible_with = ["@platforms//os:linux"],
 )
+
+py_binary(
+    name = "mkdocs_bin",
+    srcs = ["mkdocs_bin.py"],
+    visibility = ["//visibility:public"],
+    deps = ["@pip//mkdocs"],
+)
diff --git a/documentation/mkdocs_bin.py b/documentation/mkdocs_bin.py
new file mode 100755
index 0000000..86b15ee
--- /dev/null
+++ b/documentation/mkdocs_bin.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python3
+"""This is a small wrapper to allow bazel to call mkdocs as a script.
+
+Because the mkdocs library handles consuming command-line arguments directly,
+we (rather than attempting to mess with arguments ourselves), use environment
+variables to communicate certain flags. Namely:
+If the OUTPUT environment variable is set, then we will tar up the output of
+the SITE_DIR directory into the OUTPUT file. This is used to make the mkdocs
+output bazel-friendly by only outputting a single file.
+"""
+from mkdocs.__main__ import cli
+import os
+import sys
+import tarfile
+try:
+    cli()
+except SystemExit as err:
+    if err.code != 0:
+        raise err
+if "OUTPUT" in os.environ:
+    with tarfile.open(os.environ["OUTPUT"], "w") as tarball:
+        tarball.add(os.environ["SITE_DIR"], arcname="")
diff --git a/tools/build_rules/mkdocs.bzl b/tools/build_rules/mkdocs.bzl
new file mode 100644
index 0000000..b3a82f9
--- /dev/null
+++ b/tools/build_rules/mkdocs.bzl
@@ -0,0 +1,62 @@
+def _mkdocs_impl(ctx):
+    output_file = ctx.actions.declare_file(ctx.attr.name + ".tar")
+    build_args = ctx.actions.args()
+    build_args.add("build")
+    site_subdir = "site"
+    build_args.add("-f")
+    build_args.add_all(ctx.files.config)
+    build_args.add("--site-dir")
+    build_args.add(site_subdir)
+    site_dir = ctx.files.config[0].dirname + "/" + site_subdir
+    ctx.actions.run(
+        mnemonic = "MkDocsBuild",
+        executable = ctx.executable._mkdocs,
+        arguments = [build_args],
+        inputs = ctx.files.srcs + ctx.files.config,
+        env = {"OUTPUT": output_file.path, "SITE_DIR": site_dir},
+        outputs = [output_file],
+    )
+    return [DefaultInfo(files = depset([output_file]))]
+
+_mkdocs = rule(
+    implementation = _mkdocs_impl,
+    attrs = {
+        "srcs": attr.label_list(
+            allow_files = [".md"],
+            doc = "A list of markdown files to generate docs for.",
+        ),
+        "config": attr.label(
+            doc = "mkdocs.yaml configuration file to use for the documentation.",
+            allow_files = [".yaml"],
+            mandatory = True,
+        ),
+        "_mkdocs": attr.label(default = Label("@org_frc971//documentation:mkdocs_bin"), executable = True, cfg = "exec"),
+    },
+)
+
+def mkdocs(name, srcs, config, **kwargs):
+    """Bazel rule to build and serve mkdocs documentation.
+
+    Build a tarball of the HTML files to be served by building "name", or get
+    the functionality of `mkdocs serve` by doing a bazel run on "name.serve".
+
+    Args:
+      name: name of the rule.
+      srcs: A list of markdown files to generate documentation from.
+      config: A .yaml specifying the mkdocs configuration to use.
+        See https://www.mkdocs.org/user-guide/configuration/
+        Note that mkdocs does not allow the mkdocs.yaml configuration to be
+        in the same folder as the markdown files (canonically, if the
+        mkdocs.yaml is at foo/mkdocs.yaml, the markdown files will be
+        at foo/docs/*.md).
+    """
+    _mkdocs(name = name, srcs = srcs, config = config, **kwargs)
+    native.py_binary(
+        name = name + ".serve",
+        srcs = ["@org_frc971//documentation:mkdocs_bin.py"],
+        main = "mkdocs_bin.py",
+        args = ["serve", "-f", "$(location %s)" % (config,)],
+        deps = ["@pip//mkdocs"],
+        data = srcs + [config],
+        **kwargs
+    )
diff --git a/tools/python/requirements.lock.txt b/tools/python/requirements.lock.txt
index ba7e2d7..e9d264f 100644
--- a/tools/python/requirements.lock.txt
+++ b/tools/python/requirements.lock.txt
@@ -12,6 +12,10 @@
     --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 \
     --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f
     # via requests
+click==8.1.3 \
+    --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \
+    --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48
+    # via mkdocs
 cycler==0.11.0 \
     --hash=sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3 \
     --hash=sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f
@@ -20,10 +24,24 @@
     --hash=sha256:545c05d0f7903a863c2020e07b8f0a57517f2c40d940bded77076397872d14ca \
     --hash=sha256:edf251d5d2cc0580d5f72de4621c338d8c66c5f61abb50cf486640f73c8194d5
     # via matplotlib
+ghp-import==2.1.0 \
+    --hash=sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 \
+    --hash=sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343
+    # via mkdocs
 idna==3.4 \
     --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
     --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
     # via requests
+importlib-metadata==5.0.0 \
+    --hash=sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab \
+    --hash=sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43
+    # via
+    #   markdown
+    #   mkdocs
+jinja2==3.1.2 \
+    --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
+    --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
+    # via mkdocs
 kiwisolver==1.3.2 \
     --hash=sha256:0007840186bacfaa0aba4466d5890334ea5938e0bb7e28078a0eb0e63b5b59d5 \
     --hash=sha256:19554bd8d54cf41139f376753af1a644b63c9ca93f8f72009d50a2080f870f77 \
@@ -70,6 +88,52 @@
     --hash=sha256:f8eb7b6716f5b50e9c06207a14172cf2de201e41912ebe732846c02c830455b9 \
     --hash=sha256:fc4453705b81d03568d5b808ad8f09c77c47534f6ac2e72e733f9ca4714aa75c
     # via matplotlib
+markdown==3.3.7 \
+    --hash=sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874 \
+    --hash=sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621
+    # via mkdocs
+markupsafe==2.1.1 \
+    --hash=sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003 \
+    --hash=sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88 \
+    --hash=sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5 \
+    --hash=sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7 \
+    --hash=sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a \
+    --hash=sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603 \
+    --hash=sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1 \
+    --hash=sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135 \
+    --hash=sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247 \
+    --hash=sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6 \
+    --hash=sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601 \
+    --hash=sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77 \
+    --hash=sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02 \
+    --hash=sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e \
+    --hash=sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63 \
+    --hash=sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f \
+    --hash=sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980 \
+    --hash=sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b \
+    --hash=sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812 \
+    --hash=sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff \
+    --hash=sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96 \
+    --hash=sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1 \
+    --hash=sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925 \
+    --hash=sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a \
+    --hash=sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6 \
+    --hash=sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e \
+    --hash=sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f \
+    --hash=sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4 \
+    --hash=sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f \
+    --hash=sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3 \
+    --hash=sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c \
+    --hash=sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a \
+    --hash=sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417 \
+    --hash=sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a \
+    --hash=sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a \
+    --hash=sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37 \
+    --hash=sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452 \
+    --hash=sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933 \
+    --hash=sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a \
+    --hash=sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7
+    # via jinja2
 matplotlib==3.5.1 \
     --hash=sha256:14334b9902ec776461c4b8c6516e26b450f7ebe0b3ef8703bf5cdfbbaecf774a \
     --hash=sha256:2252bfac85cec7af4a67e494bfccf9080bcba8a0299701eab075f48847cca907 \
@@ -107,6 +171,14 @@
     --hash=sha256:edf5e4e1d5fb22c18820e8586fb867455de3b109c309cb4fce3aaed85d9468d1 \
     --hash=sha256:fe8d40c434a8e2c68d64c6d6a04e77f21791a93ff6afe0dce169597c110d3079
     # via -r tools/python/requirements.txt
+mergedeep==1.3.4 \
+    --hash=sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8 \
+    --hash=sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307
+    # via mkdocs
+mkdocs==1.4.0 \
+    --hash=sha256:ce057e9992f017b8e1496b591b6c242cbd34c2d406e2f9af6a19b97dd6248faa \
+    --hash=sha256:e5549a22d59e7cb230d6a791edd2c3d06690908454c0af82edc31b35d57e3069
+    # via -r tools/python/requirements.txt
 numpy==1.21.5 \
     --hash=sha256:00c9fa73a6989895b8815d98300a20ac993c49ac36c8277e8ffeaa3631c0dbbb \
     --hash=sha256:025b497014bc33fc23897859350f284323f32a2fff7654697f5a5fc2a19e9939 \
@@ -145,7 +217,9 @@
 packaging==21.3 \
     --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \
     --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522
-    # via matplotlib
+    # via
+    #   matplotlib
+    #   mkdocs
 pillow==8.4.0 \
     --hash=sha256:066f3999cb3b070a95c3652712cffa1a748cd02d60ad7b4e485c3748a04d9d76 \
     --hash=sha256:0a0956fdc5defc34462bb1c765ee88d933239f9a94bc37d132004775241a7585 \
@@ -202,7 +276,57 @@
 python-dateutil==2.8.2 \
     --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \
     --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9
-    # via matplotlib
+    # via
+    #   ghp-import
+    #   matplotlib
+pyyaml==6.0 \
+    --hash=sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf \
+    --hash=sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293 \
+    --hash=sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b \
+    --hash=sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57 \
+    --hash=sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b \
+    --hash=sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4 \
+    --hash=sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07 \
+    --hash=sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba \
+    --hash=sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9 \
+    --hash=sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287 \
+    --hash=sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513 \
+    --hash=sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0 \
+    --hash=sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782 \
+    --hash=sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0 \
+    --hash=sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92 \
+    --hash=sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f \
+    --hash=sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 \
+    --hash=sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc \
+    --hash=sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1 \
+    --hash=sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c \
+    --hash=sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86 \
+    --hash=sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4 \
+    --hash=sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c \
+    --hash=sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34 \
+    --hash=sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b \
+    --hash=sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d \
+    --hash=sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c \
+    --hash=sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb \
+    --hash=sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7 \
+    --hash=sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737 \
+    --hash=sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3 \
+    --hash=sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d \
+    --hash=sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358 \
+    --hash=sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53 \
+    --hash=sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78 \
+    --hash=sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803 \
+    --hash=sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a \
+    --hash=sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f \
+    --hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \
+    --hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5
+    # via
+    #   mkdocs
+    #   pyyaml-env-tag
+pyyaml-env-tag==0.1 \
+    --hash=sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb \
+    --hash=sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069
+    # via mkdocs
 requests==2.28.1 \
     --hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 \
     --hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349
@@ -246,3 +370,34 @@
     --hash=sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e \
     --hash=sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997
     # via requests
+watchdog==2.1.9 \
+    --hash=sha256:083171652584e1b8829581f965b9b7723ca5f9a2cd7e20271edf264cfd7c1412 \
+    --hash=sha256:117ffc6ec261639a0209a3252546b12800670d4bf5f84fbd355957a0595fe654 \
+    --hash=sha256:186f6c55abc5e03872ae14c2f294a153ec7292f807af99f57611acc8caa75306 \
+    --hash=sha256:195fc70c6e41237362ba720e9aaf394f8178bfc7fa68207f112d108edef1af33 \
+    --hash=sha256:226b3c6c468ce72051a4c15a4cc2ef317c32590d82ba0b330403cafd98a62cfd \
+    --hash=sha256:247dcf1df956daa24828bfea5a138d0e7a7c98b1a47cf1fa5b0c3c16241fcbb7 \
+    --hash=sha256:255bb5758f7e89b1a13c05a5bceccec2219f8995a3a4c4d6968fe1de6a3b2892 \
+    --hash=sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609 \
+    --hash=sha256:4f4e1c4aa54fb86316a62a87b3378c025e228178d55481d30d857c6c438897d6 \
+    --hash=sha256:5952135968519e2447a01875a6f5fc8c03190b24d14ee52b0f4b1682259520b1 \
+    --hash=sha256:64a27aed691408a6abd83394b38503e8176f69031ca25d64131d8d640a307591 \
+    --hash=sha256:6b17d302850c8d412784d9246cfe8d7e3af6bcd45f958abb2d08a6f8bedf695d \
+    --hash=sha256:70af927aa1613ded6a68089a9262a009fbdf819f46d09c1a908d4b36e1ba2b2d \
+    --hash=sha256:7a833211f49143c3d336729b0020ffd1274078e94b0ae42e22f596999f50279c \
+    --hash=sha256:8250546a98388cbc00c3ee3cc5cf96799b5a595270dfcfa855491a64b86ef8c3 \
+    --hash=sha256:97f9752208f5154e9e7b76acc8c4f5a58801b338de2af14e7e181ee3b28a5d39 \
+    --hash=sha256:9f05a5f7c12452f6a27203f76779ae3f46fa30f1dd833037ea8cbc2887c60213 \
+    --hash=sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330 \
+    --hash=sha256:ad576a565260d8f99d97f2e64b0f97a48228317095908568a9d5c786c829d428 \
+    --hash=sha256:b530ae007a5f5d50b7fbba96634c7ee21abec70dc3e7f0233339c81943848dc1 \
+    --hash=sha256:bfc4d351e6348d6ec51df007432e6fe80adb53fd41183716017026af03427846 \
+    --hash=sha256:d3dda00aca282b26194bdd0adec21e4c21e916956d972369359ba63ade616153 \
+    --hash=sha256:d9820fe47c20c13e3c9dd544d3706a2a26c02b2b43c993b62fcd8011bcc0adb3 \
+    --hash=sha256:ed80a1628cee19f5cfc6bb74e173f1b4189eb532e705e2a13e3250312a62e0c9 \
+    --hash=sha256:ee3e38a6cc050a8830089f79cbec8a3878ec2fe5160cdb2dc8ccb6def8552658
+    # via mkdocs
+zipp==3.8.1 \
+    --hash=sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2 \
+    --hash=sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009
+    # via importlib-metadata
diff --git a/tools/python/requirements.txt b/tools/python/requirements.txt
index 5d0f583..af2eb1c 100644
--- a/tools/python/requirements.txt
+++ b/tools/python/requirements.txt
@@ -6,3 +6,4 @@
 pkginfo
 requests
 scipy
+mkdocs
diff --git a/tools/python/whl_overrides.json b/tools/python/whl_overrides.json
index 9499780..6221bc8 100644
--- a/tools/python/whl_overrides.json
+++ b/tools/python/whl_overrides.json
@@ -7,6 +7,10 @@
         "sha256": "83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f",
         "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/charset_normalizer-2.1.1-py3-none-any.whl"
     },
+    "click==8.1.3": {
+        "sha256": "bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48",
+        "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/click-8.1.3-py3-none-any.whl"
+    },
     "cycler==0.11.0": {
         "sha256": "3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3",
         "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/cycler-0.11.0-py3-none-any.whl"
@@ -15,18 +19,46 @@
         "sha256": "edf251d5d2cc0580d5f72de4621c338d8c66c5f61abb50cf486640f73c8194d5",
         "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/fonttools-4.28.5-py3-none-any.whl"
     },
+    "ghp-import==2.1.0": {
+        "sha256": "8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619",
+        "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/ghp_import-2.1.0-py3-none-any.whl"
+    },
     "idna==3.4": {
         "sha256": "90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2",
         "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/idna-3.4-py3-none-any.whl"
     },
+    "importlib-metadata==5.0.0": {
+        "sha256": "ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43",
+        "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/importlib_metadata-5.0.0-py3-none-any.whl"
+    },
+    "jinja2==3.1.2": {
+        "sha256": "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61",
+        "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/Jinja2-3.1.2-py3-none-any.whl"
+    },
     "kiwisolver==1.3.2": {
         "sha256": "30fa008c172355c7768159983a7270cb23838c4d7db73d6c0f6b60dde0d432c6",
         "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/kiwisolver-1.3.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl"
     },
+    "markdown==3.3.7": {
+        "sha256": "f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621",
+        "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/Markdown-3.3.7-py3-none-any.whl"
+    },
+    "markupsafe==2.1.1": {
+        "sha256": "56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77",
+        "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
+    },
     "matplotlib==3.5.1": {
         "sha256": "87900c67c0f1728e6db17c6809ec05c025c6624dcf96a8020326ea15378fe8e7",
         "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/matplotlib-3.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl"
     },
+    "mergedeep==1.3.4": {
+        "sha256": "70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307",
+        "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/mergedeep-1.3.4-py3-none-any.whl"
+    },
+    "mkdocs==1.4.0": {
+        "sha256": "ce057e9992f017b8e1496b591b6c242cbd34c2d406e2f9af6a19b97dd6248faa",
+        "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/mkdocs-1.4.0-py3-none-any.whl"
+    },
     "numpy==1.21.5": {
         "sha256": "c293d3c0321996cd8ffe84215ffe5d269fd9d1d12c6f4ffe2b597a7c30d3e593",
         "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/numpy-1.21.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl"
@@ -51,6 +83,14 @@
         "sha256": "961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9",
         "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/python_dateutil-2.8.2-py2.py3-none-any.whl"
     },
+    "pyyaml==6.0": {
+        "sha256": "40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0",
+        "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl"
+    },
+    "pyyaml-env-tag==0.1": {
+        "sha256": "af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069",
+        "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/pyyaml_env_tag-0.1-py3-none-any.whl"
+    },
     "requests==2.28.1": {
         "sha256": "8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349",
         "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/requests-2.28.1-py3-none-any.whl"
@@ -66,5 +106,13 @@
     "urllib3==1.26.12": {
         "sha256": "b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997",
         "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/urllib3-1.26.12-py2.py3-none-any.whl"
+    },
+    "watchdog==2.1.9": {
+        "sha256": "4f4e1c4aa54fb86316a62a87b3378c025e228178d55481d30d857c6c438897d6",
+        "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/watchdog-2.1.9-py3-none-manylinux2014_x86_64.whl"
+    },
+    "zipp==3.8.1": {
+        "sha256": "47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009",
+        "url": "https://software.frc971.org/Build-Dependencies/wheelhouse/zipp-3.8.1-py3-none-any.whl"
     }
 }