feat: Allow additional tools for sphinx_docs (#1831)

Some Sphinx plugins require that other (not necessarily Python) tools
are available.

One example is the plugin
https://github.com/basejumpa/sphinxcontrib-umlet, which requires that
UMLet is somehow within the sandbox.

* Adds `tools` arg to `sphinx_docs` to allow passing in arbitrary tools
  that are made available at runtime
* Performs location expansion on `extra_opts`, which allows passing the
  location of the tools onto sphinx.
diff --git a/sphinxdocs/private/sphinx.bzl b/sphinxdocs/private/sphinx.bzl
index daff02d..4226391 100644
--- a/sphinxdocs/private/sphinx.bzl
+++ b/sphinxdocs/private/sphinx.bzl
@@ -55,6 +55,7 @@
         formats,
         strip_prefix = "",
         extra_opts = [],
+        tools = [],
         **kwargs):
     """Generate docs using Sphinx.
 
@@ -88,6 +89,11 @@
             source files. e.g., given `//docs:foo.md`, stripping `docs/`
             makes Sphinx see `foo.md` in its generated source directory.
         extra_opts: (list[str]) Additional options to pass onto Sphinx building.
+            On each provided option, a location expansion is performed.
+            See `ctx.expand_location()`.
+        tools: (list[label]) Additional tools that are used by Sphinx and its plugins.
+            This just makes the tools available during Sphinx execution. To locate
+            them, use `extra_opts` and `$(location)`.
         **kwargs: (dict) Common attributes to pass onto rules.
     """
     add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_docs")
@@ -102,6 +108,7 @@
         formats = formats,
         strip_prefix = strip_prefix,
         extra_opts = extra_opts,
+        tools = tools,
         **kwargs
     )
 
@@ -174,6 +181,10 @@
             doc = "Doc source files for Sphinx.",
         ),
         "strip_prefix": attr.string(doc = "Prefix to remove from input file paths."),
+        "tools": attr.label_list(
+            cfg = "exec",
+            doc = "Additional tools that are used by Sphinx and its plugins.",
+        ),
         "_extra_defines_flag": attr.label(default = "//sphinxdocs:extra_defines"),
         "_extra_env_flag": attr.label(default = "//sphinxdocs:extra_env"),
     },
@@ -238,7 +249,8 @@
     args.add("-j", "auto")  # Build in parallel, if possible
     args.add("-E")  # Don't try to use cache files. Bazel can't make use of them.
     args.add("-a")  # Write all files; don't try to detect "changed" files
-    args.add_all(ctx.attr.extra_opts)
+    for opt in ctx.attr.extra_opts:
+        args.add(ctx.expand_location(opt))
     args.add_all(ctx.attr._extra_defines_flag[_FlagInfo].value, before_each = "-D")
     args.add(source_path)
     args.add(output_dir.path)
@@ -248,11 +260,16 @@
         for v in ctx.attr._extra_env_flag[_FlagInfo].value
     ])
 
+    tools = []
+    for tool in ctx.attr.tools:
+        tools.append(tool[DefaultInfo].files_to_run)
+
     ctx.actions.run(
         executable = ctx.executable.sphinx,
         arguments = [args],
         inputs = inputs,
         outputs = [output_dir],
+        tools = tools,
         mnemonic = "SphinxBuildDocs",
         progress_message = "Sphinx building {} for %{{label}}".format(format),
         env = env,