sphinxdocs: add docs; support sources from other directories (#2128)
Documents how to use Sphinx syntax when writing docs. There's a variety
of features the `sphinx_bzl` plugin enables, but without docs, they're
somewhat hard to discover and figure out how to use.
Because sphinxdocs is almost entirely separate, adding its docs under
`//sphinxdocs/docs` is a more natural fit. Unfortunately, this became
very verbose, repetitive, and tedious, for two reasons:
1. The only way `sphinx_docs` could accept files from other directories
was using the `rename_srcs` arg and manually renaming files one-by-one.
2. Similarly, `sphinx_stardocs` required a one-by-one mapping of each
bzl file to its output file, which then had to be repeated in
`rename_srcs`.
To fix (1), the `sphinx_docs.deps` attribute and `sphinx_docs_library`
rule are added. The library targets collect files, and `sphinx_docs`
moves then into the final Sphinx sources directory.
To fix (2), the `sphinx_stardoc.srcs` attribute is added, which accepts
`bzl_library` targets. I noticed that, in almost all cases, the output
name was simply the input name with the `.md` extension, so the rule now
does that by default. For special cases, the `sphinx_stardoc` (singular)
rule can be called directly.
Also:
* Adds `bzl:rule` as a cross reference lookup role
* Removes some defunct stuff relating to the stardoc template files that
aren't used anymore.
* Disables warnings from the autosectionlabel extension. These were
spamming warnings because CHANGELOG.md has many headers with the same
name.
* Adds more entries to bazel inventory (all of native and ctx)
diff --git a/sphinxdocs/BUILD.bazel b/sphinxdocs/BUILD.bazel
index 6cb69ba..9ad1e1e 100644
--- a/sphinxdocs/BUILD.bazel
+++ b/sphinxdocs/BUILD.bazel
@@ -48,6 +48,12 @@
)
bzl_library(
+ name = "sphinx_docs_library_bzl",
+ srcs = ["sphinx_docs_library.bzl"],
+ deps = ["//sphinxdocs/private:sphinx_docs_library_macro_bzl"],
+)
+
+bzl_library(
name = "sphinx_stardoc_bzl",
srcs = ["sphinx_stardoc.bzl"],
deps = ["//sphinxdocs/private:sphinx_stardoc_bzl"],
diff --git a/sphinxdocs/docs/BUILD.bazel b/sphinxdocs/docs/BUILD.bazel
new file mode 100644
index 0000000..a85155b
--- /dev/null
+++ b/sphinxdocs/docs/BUILD.bazel
@@ -0,0 +1,55 @@
+load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility
+load("//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//sphinxdocs:sphinx_stardoc.bzl", "sphinx_stardocs")
+
+package(default_visibility = ["//:__subpackages__"])
+
+# We only build for Linux and Mac because:
+# 1. The actual doc process only runs on Linux
+# 2. Mac is a common development platform, and is close enough to Linux
+# it's feasible to make work.
+# Making CI happy under Windows is too much of a headache, though, so we don't
+# bother with that.
+_TARGET_COMPATIBLE_WITH = select({
+ "@platforms//os:linux": [],
+ "@platforms//os:macos": [],
+ "//conditions:default": ["@platforms//:incompatible"],
+}) if IS_BAZEL_7_OR_HIGHER else ["@platforms//:incompatible"]
+
+sphinx_docs_library(
+ name = "docs_lib",
+ deps = [
+ ":artisian_api_docs",
+ ":artisian_docs",
+ ":bzl_docs",
+ ],
+)
+
+sphinx_docs_library(
+ name = "artisian_docs",
+ srcs = glob(
+ ["**/*.md"],
+ exclude = ["api/**"],
+ ),
+ prefix = "sphinxdocs/",
+)
+
+sphinx_docs_library(
+ name = "artisian_api_docs",
+ srcs = glob(
+ ["api/**/*.md"],
+ ),
+)
+
+sphinx_stardocs(
+ name = "bzl_docs",
+ srcs = [
+ "//sphinxdocs:readthedocs_bzl",
+ "//sphinxdocs:sphinx_bzl",
+ "//sphinxdocs:sphinx_docs_library_bzl",
+ "//sphinxdocs:sphinx_stardoc_bzl",
+ "//sphinxdocs/private:sphinx_docs_library_bzl",
+ ],
+ prefix = "api/",
+ target_compatible_with = _TARGET_COMPATIBLE_WITH,
+)
diff --git a/sphinxdocs/docs/api/sphinxdocs/index.md b/sphinxdocs/docs/api/sphinxdocs/index.md
new file mode 100644
index 0000000..bd4e9b6
--- /dev/null
+++ b/sphinxdocs/docs/api/sphinxdocs/index.md
@@ -0,0 +1,29 @@
+:::{bzl:currentfile} //sphinxdocs:BUILD.bazel
+:::
+
+# //sphinxdocs
+
+:::{bzl:flag} extra_defines
+Additional `-D` values to add to every Sphinx build.
+
+This is a list flag. Multiple uses are accumulated.
+
+This is most useful for overriding e.g. the version when performing
+release builds.
+:::
+
+:::{bzl:flag} extra_env
+Additional environment variables to for every Sphinx build.
+
+This is a list flag. Multiple uses are accumulated. Values are `key=value`
+format.
+:::
+
+:::{bzl:flag} quiet
+Whether to add the `-q` arg to Sphinx invocations.
+
+This is a boolean flag.
+
+This is useful for debugging invocations or developing extensions. The Sphinx
+`-q` flag causes sphinx to produce additional output on stdout.
+:::
diff --git a/sphinxdocs/docs/api/sphinxdocs/inventories/index.md b/sphinxdocs/docs/api/sphinxdocs/inventories/index.md
new file mode 100644
index 0000000..a03645e
--- /dev/null
+++ b/sphinxdocs/docs/api/sphinxdocs/inventories/index.md
@@ -0,0 +1,11 @@
+:::{bzl:currentfile} //sphinxdocs/inventories:BUILD.bazel
+:::
+
+# //sphinxdocs/inventories
+
+:::{bzl:target} bazel_inventory
+A Sphinx inventory of Bazel objects.
+
+By including this target in your Sphinx build and enabling intersphinx, cross
+references to builtin Bazel objects can be written.
+:::
diff --git a/sphinxdocs/docs/index.md b/sphinxdocs/docs/index.md
new file mode 100644
index 0000000..ac857d6
--- /dev/null
+++ b/sphinxdocs/docs/index.md
@@ -0,0 +1,20 @@
+# Docgen using Sphinx with Bazel
+
+The `sphinxdocs` project allows using Bazel to run Sphinx to generate
+documentation. It comes with:
+
+* Rules for running Sphinx
+* Rules for generating documentation for Starlark code.
+* A Sphinx plugin for documenting Starlark and Bazel objects.
+* Rules for readthedocs build integration.
+
+While it is primarily oriented towards docgen for Starlark code, the core of it
+is agnostic as to what is being documented.
+
+
+```{toctree}
+:hidden:
+
+starlark-docgen
+sphinx-bzl
+```
diff --git a/sphinxdocs/docs/sphinx-bzl.md b/sphinxdocs/docs/sphinx-bzl.md
new file mode 100644
index 0000000..c6dc430
--- /dev/null
+++ b/sphinxdocs/docs/sphinx-bzl.md
@@ -0,0 +1,203 @@
+# Bazel plugin for Sphinx
+
+The `sphinx_bzl` Python package is a Sphinx plugin that defines a custom domain
+("bzl") in the Sphinx system. This provides first-class integration with Sphinx
+and allows code comments to provide rich information and allows manually writing
+docs for objects that aren't directly representable in bzl source code. For
+example, the fields of a provider can use `:type:` to indicate the type of a
+field, or manually written docs can use the `{bzl:target}` directive to document
+a well known target.
+
+## Configuring Sphinx
+
+To enable the plugin in Sphinx, depend on
+`@rules_python//sphinxdocs/src/sphinx_bzl` and enable it in `conf.py`:
+
+```
+extensions = [
+ "sphinx_bzl.bzl",
+]
+```
+
+## Brief introduction to Sphinx terminology
+
+To aid understanding how to write docs, lets define a few common terms:
+
+* **Role**: A role is the "bzl:obj" part when writing ``{bzl:obj}`ref` ``.
+ Roles mark inline text as needing special processing. There's generally
+ two types of processing: creating cross references, or role-specific custom
+ rendering. For example `{bzl:obj}` will create a cross references, while
+ `{bzl:default-value}` indicates the default value of an argument.
+* **Directive**: A directive is indicated with `:::` and allows defining an
+ entire object and its parts. For example, to describe a function and its
+ arguments, the `:::{bzl:function}` directive is used.
+* **Directive Option**: A directive option is the "type" part when writing
+ `:type:` within a directive. Directive options are how directives are told
+ the meaning of certain values, such as the type of a provider field. Depending
+ on the object being documented, a directive option may be used instead of
+ special role to indicate semantic values.
+
+Most often, you'll be using roles to refer other objects or indicate special
+values in doc strings. For directives, you're likely to only use them when
+manually writing docs to document flags, targets, or other objects that
+`sphinx_stardoc` generates for you.
+
+## MyST vs RST
+
+By default, Sphinx uses ReStructured Text (RST) syntax for its documents.
+Unfortunately, RST syntax is very different than the popular Markdown syntax. To
+bridge the gap, MyST translates Markdown-style syntax into the RST equivalents.
+This allows easily using Markdown in bzl files.
+
+While MyST isn't required for the core `sphinx_bzl` plugin to work, this
+document uses MyST syntax because `sphinx_stardoc` bzl doc gen rule requires
+MyST.
+
+## Type expressions
+
+Several roles or fields accept type expressions. Type expressions use
+Python-style annotation syntax to describe data types. For example `None | list[str]`
+describes a type of "None or a list of strings". Each component of the
+expression is parsed and cross reference to its associated type definition.
+
+## Cross references
+
+In brief, to reference bzl objects, use the `bzl:obj` role and use the
+Bazel label string you would use to refer to the object in Bazel (using `%` to
+denote names within a file). For example, to unambiguously refer to `py_binary`:
+
+```
+{bzl:obj}`@rules_python//python:py_binary.bzl%py_binary`
+```
+
+The above is pretty long, so shorter names are also supported, and `sphinx_bzl`
+will try to find something that matches. Additionally, in `.bzl` code, the
+`bzl:` prefix is set as the default. The above can then be shortened to:
+
+```
+{obj}`py_binary`
+```
+
+The text that is displayed by be customized by putting the reference string in
+chevrons (`<>`):
+
+```
+{obj}`the binary rule <py_binary>`
+```
+
+Finally, specific types of objects (rules, functions, providers, etc) can be
+specified to help disambiguate short names:
+
+```
+{function}`py_binary` # Refers to the wrapping macro
+{rule}`py_binary` # Refers to the underlying rule
+```
+
+Those are the basics of cross referencing. Sphinx has several additional
+syntaxes for finding and referencing objects; see
+[the MyST docs for supported
+syntaxes](https://myst-parser.readthedocs.io/en/latest/syntax/cross-referencing.html#reference-roles)
+
+
+### Cross reference roles
+
+A cross reference role is the `obj` portion of `{bzl:obj}`. It affects what is
+searched and matched. Supported cross reference roles are:
+
+* `{bzl:arg}`: Refer to a function argument.
+* `{bzl:attr}`: Refer to a rule attribute.
+* `{bzl:obj}`: Refer to any type of Bazel object
+* `{bzl:rule}`: Refer to a rule.
+* `{bzl:target}`: Refer to a target.
+* `{bzl:type}`: Refer to a type or type expression; can also be used in argument
+ documentation.
+
+## Special roles
+
+There are several special roles that can be used to annotate parts of objects,
+such as the type of arguments or their default values.
+
+### Role bzl:default-value
+
+Indicate the default value for a function argument or rule attribute. Use it in
+the Args doc of a function or the doc text of an attribute.
+
+```
+def func(arg=1):
+ """Do stuff
+
+ Args:
+ foo: {default-value}`1` the arg
+
+my_rule = rule(attrs = {
+ "foo": attr.string(doc="{default-value}`bar`)
+})
+
+```
+
+### Role bzl:return-type
+
+Indicates the return type for a function. Use it in the Returns doc of a
+function.
+
+```
+def func():
+ """Do stuff
+
+ Returns:
+ {return-type}`int`
+ """
+ return 1
+```
+
+### Role bzl:type
+
+Indicates the type of an argument for a function. Use it in the Args doc of
+a function.
+
+```
+def func(arg):
+ """Do stuff
+
+ Args:
+ arg: {type}`int`
+ """
+ print(arg + 1)
+```
+
+## Directives
+
+Most directives are automatically generated by `sphinx_stardoc`. Here, we only
+document ones that must be manually written.
+
+To write a directive, a line starts with 3 to 6 colons (`:`), followed by the
+directive name in braces (`{}`), and eventually ended by the same number of
+colons on their own line. For example:
+
+```
+:::{bzl:target} //my:target
+
+Doc about target
+:::
+```
+
+### Directive bzl:currentfile
+
+This directive indicates the Bazel file that objects defined in the current
+documentation file are in. This is required for any page that defines Bazel
+objects.
+
+### Directive bzl:target
+
+Documents a target. It takes no directive options
+
+```
+:::{bzl:target} //foo:target
+
+My docs
+:::
+```
+
+### Directive bzl:flag
+
+Documents a flag. It has the same format as `bzl:target`
diff --git a/sphinxdocs/docs/starlark-docgen.md b/sphinxdocs/docs/starlark-docgen.md
new file mode 100644
index 0000000..d131607
--- /dev/null
+++ b/sphinxdocs/docs/starlark-docgen.md
@@ -0,0 +1,75 @@
+# Starlark docgen
+
+Using the `sphinx_stardoc` rule, API documentation can be generated from bzl
+source code. This rule requires both MyST-based markdown and the `sphinx_bzl`
+Sphinx extension are enabled. This allows source code to use Markdown and
+Sphinx syntax to create rich documentation with cross references, types, and
+more.
+
+
+## Configuring Sphinx
+
+While the `sphinx_stardoc` rule doesn't require Sphinx itself, the source
+it generates requires some additional Sphinx plugins and config settings.
+
+When defining the `sphinx_build_binary` target, also depend on:
+* `@rules_python//sphinxdocs/src/sphinx_bzl:sphinx_bzl`
+* `myst_parser` (e.g. `@pypi//myst_parser`)
+* `typing_extensions` (e.g. `@pypi//myst_parser`)
+
+```
+sphinx_build_binary(
+ name = "sphinx-build",
+ deps = [
+ "@rules_python//sphinxdocs/src/sphinx_bzl",
+ "@pypi//myst_parser",
+ "@pypi//typing_extensions",
+ ...
+ ]
+)
+```
+
+In `conf.py`, enable the `sphinx_bzl` extension, `myst_parser` extension,
+and the `colon_fence` MyST extension.
+
+```
+extensions = [
+ "myst_parser",
+ "sphinx_bzl.bzl",
+]
+
+myst_enable_extensions = [
+ "colon_fence",
+]
+```
+
+## Generating docs from bzl files
+
+To convert the bzl code to Sphinx doc sources, `sphinx_stardocs` is the primary
+rule to do so. It takes a list of `bzl_library` targets or files and generates docs for
+each. When a `bzl_library` target is passed, the `bzl_library.srcs` value can only
+have a single file.
+
+Example:
+
+```
+sphinx_stardocs(
+ name = "my_docs",
+ srcs = [
+ ":binary_bzl",
+ ":library_bzl",
+ ]
+)
+
+bzl_library(
+ name = "binary_bzl",
+ srcs = ["binary.bzl"],
+ deps = ...
+)
+
+bzl_library(
+ name = "library_bzl",
+ srcs = ["library.bzl"],
+ deps = ...
+)
+```
diff --git a/sphinxdocs/inventories/bazel_inventory.txt b/sphinxdocs/inventories/bazel_inventory.txt
index a7f0222..445f0f7 100644
--- a/sphinxdocs/inventories/bazel_inventory.txt
+++ b/sphinxdocs/inventories/bazel_inventory.txt
@@ -1,16 +1,50 @@
# Sphinx inventory version 2
# Project: Bazel
-# Version: 7.0.0
+# Version: 7.3.0
# The remainder of this file is compressed using zlib
Action bzl:type 1 rules/lib/Action -
File bzl:type 1 rules/lib/File -
Label bzl:type 1 rules/lib/Label -
Target bzl:type 1 rules/lib/builtins/Target -
bool bzl:type 1 rules/lib/bool -
+ctx.actions bzl:obj 1 rules/lib/builtins/ctx#actions -
+ctx.aspect_ids bzl:obj 1 rules/lib/builtins/ctx#aspect_ids -
+ctx.attr bzl:obj 1 rules/lib/builtins/ctx#attr -
+ctx.bin_dir bzl:obj 1 rules/lib/builtins/ctx#bin_dir -
+ctx.build_file_path bzl:obj 1 rules/lib/builtins/ctx#build_file_path -
+ctx.build_setting_value bzl:obj 1 rules/lib/builtins/ctx#build_setting_value -
+ctx.configuration bzl:obj 1 rules/lib/builtins/ctx#configuration -
+ctx.coverage_instrumented bzl:function 1 rules/lib/builtins/ctx#coverage_instrumented -
+ctx.created_actions bzl:function 1 rules/lib/builtins/ctx#created_actions -
+ctx.disabled_features bzl:obj 1 rules/lib/builtins/ctx#disabled_features -
+ctx.exec_groups bzl:obj 1 rules/lib/builtins/ctx#exec_groups -
+ctx.executable bzl:obj 1 rules/lib/builtins/ctx#executable -
+ctx.expand_location bzl:function 1 rules/lib/builtins/ctx#expand_location -
+ctx.expand_location bzl:function 1 rules/lib/builtins/ctx#expand_location - -
+ctx.expand_make_variables bzl:function 1 rules/lib/builtins/ctx#expand_make_variables -
+ctx.features bzl:obj 1 rules/lib/builtins/ctx#features -
+ctx.file bzl:obj 1 rules/lib/builtins/ctx#file -
+ctx.files bzl:obj 1 rules/lib/builtins/ctx#files -
+ctx.fragments bzl:obj 1 rules/lib/builtins/ctx#fragments -
+ctx.genfiles_dir bzl:obj 1 rules/lib/builtins/ctx#genfiles_dir -
+ctx.info_file bzl:obj 1 rules/lib/builtins/ctx#info_file -
+ctx.label bzl:obj 1 rules/lib/builtins/ctx#label -
+ctx.outputs bzl:obj 1 rules/lib/builtins/ctx#outputs -
+ctx.resolve_command bzl:function 1 rules/lib/builtins/ctx#resolve_command -
+ctx.resolve_tools bzl:function 1 rules/lib/builtins/ctx#resolve_tools -
+ctx.rule bzl:obj 1 rules/lib/builtins/ctx#rule -
+ctx.runfiles bzl:function 1 rules/lib/builtins/ctx#runfiles -
+ctx.split_attr bzl:obj 1 rules/lib/builtins/ctx#split_attr -
+ctx.super bzl:obj 1 rules/lib/builtins/ctx#super -
+ctx.target_platform_has_constraint bzl:function 1 rules/lib/builtins/ctx#target_platform_has_constraint -
+ctx.toolchains bzl:obj 1 rules/lib/builtins/ctx#toolchains -
+ctx.var bzl:obj 1 rules/lib/builtins/ctx#var -
+ctx.version_file bzl:obj 1 rules/lib/builtins/ctx#version_file -
+ctx.workspace_name bzl:obj 1 rules/lib/builtins/ctx#workspace_name -
int bzl:type 1 rules/lib/int -
depset bzl:type 1 rules/lib/depset -
dict bzl:type 1 rules/lib/dict -
-label bzl:doc 1 concepts/labels -
+label bzl:type 1 concepts/labels -
attr.bool bzl:type 1 rules/lib/toplevel/attr#bool -
attr.int bzl:type 1 rules/lib/toplevel/attr#int -
attr.label bzl:type 1 rules/lib/toplevel/attr#label -
@@ -18,7 +52,17 @@
attr.string bzl:type 1 rules/lib/toplevel/attr#string -
attr.string_list bzl:type 1 rules/lib/toplevel/attr#string_list -
list bzl:type 1 rules/lib/list -
-python bzl:doc 1 reference/be/python -
+native.existing_rule bzl:function 1 rules/lib/toplevel/native#existing_rule -
+native.existing_rules bzl:function 1 rules/lib/toplevel/native#existing_rules -
+native.exports_files bzl:function 1 rules/lib/toplevel/native#exports_files -
+native.glob bzl:function 1 rules/lib/toplevel/native#glob -
+native.module_name bzl:function 1 rules/lib/toplevel/native#module_name -
+native.module_version bzl:function 1 rules/lib/toplevel/native#module_version -
+native.package_group bzl:function 1 rules/lib/toplevel/native#package_group -
+native.package_name bzl:function 1 rules/lib/toplevel/native#package_name -
+native.package_relative_label bzl:function 1 rules/lib/toplevel/native#package_relative_label -
+native.repo_name bzl:function 1 rules/lib/toplevel/native#repo_name -
+native.repository_name bzl:function 1 rules/lib/toplevel/native#repository_name -
str bzl:type 1 rules/lib/string -
struct bzl:type 1 rules/lib/builtins/struct -
Name bzl:type 1 concepts/labels#target-names -
diff --git a/sphinxdocs/private/BUILD.bazel b/sphinxdocs/private/BUILD.bazel
index ec6a945..d91e048 100644
--- a/sphinxdocs/private/BUILD.bazel
+++ b/sphinxdocs/private/BUILD.bazel
@@ -26,11 +26,7 @@
# referenced by the //sphinxdocs macros.
exports_files(
[
- "func_template.vm",
- "header_template.vm",
- "provider_template.vm",
"readthedocs_install.py",
- "rule_template.vm",
"sphinx_build.py",
"sphinx_server.py",
],
@@ -38,13 +34,36 @@
)
bzl_library(
+ name = "sphinx_docs_library_macro_bzl",
+ srcs = ["sphinx_docs_library_macro.bzl"],
+ deps = [
+ ":sphinx_docs_library_bzl",
+ "//python/private:util_bzl",
+ ],
+)
+
+bzl_library(
+ name = "sphinx_docs_library_bzl",
+ srcs = ["sphinx_docs_library.bzl"],
+ deps = [":sphinx_docs_library_info_bzl"],
+)
+
+bzl_library(
+ name = "sphinx_docs_library_info_bzl",
+ srcs = ["sphinx_docs_library_info.bzl"],
+)
+
+bzl_library(
name = "sphinx_bzl",
srcs = ["sphinx.bzl"],
deps = [
+ ":sphinx_docs_library_info_bzl",
"//python:py_binary_bzl",
+ "@bazel_skylib//:bzl_library",
"@bazel_skylib//lib:paths",
"@bazel_skylib//lib:types",
"@bazel_skylib//rules:build_test",
+ "@bazel_skylib//rules:common_settings",
"@io_bazel_stardoc//stardoc:stardoc_lib",
],
)
@@ -53,7 +72,11 @@
name = "sphinx_stardoc_bzl",
srcs = ["sphinx_stardoc.bzl"],
deps = [
+ ":sphinx_docs_library_macro_bzl",
"//python/private:util_bzl",
+ "//sphinxdocs:sphinx_bzl",
+ "@bazel_skylib//:bzl_library",
+ "@bazel_skylib//lib:paths",
"@bazel_skylib//lib:types",
"@bazel_skylib//rules:build_test",
"@io_bazel_stardoc//stardoc:stardoc_lib",
diff --git a/sphinxdocs/private/readthedocs.bzl b/sphinxdocs/private/readthedocs.bzl
index ee8e7aa..a62c51b 100644
--- a/sphinxdocs/private/readthedocs.bzl
+++ b/sphinxdocs/private/readthedocs.bzl
@@ -27,11 +27,11 @@
for more information.
Args:
- name: (str) name of the installer
- docs: (label list) list of targets that generate directories to copy
+ name: {type}`Name` name of the installer
+ docs: {type}`list[label]` list of targets that generate directories to copy
into the directories readthedocs expects final output in. This
- is typically a single `sphinx_stardocs` target.
- **kwargs: (dict) additional kwargs to pass onto the installer
+ is typically a single {obj}`sphinx_stardocs` target.
+ **kwargs: {type}`dict` additional kwargs to pass onto the installer
"""
add_tag(kwargs, "@rules_python//sphinxdocs:readthedocs_install")
py_binary(
diff --git a/sphinxdocs/private/sphinx.bzl b/sphinxdocs/private/sphinx.bzl
index a5ac831..a198291 100644
--- a/sphinxdocs/private/sphinx.bzl
+++ b/sphinxdocs/private/sphinx.bzl
@@ -15,13 +15,26 @@
"""Implementation of sphinx rules."""
load("@bazel_skylib//lib:paths.bzl", "paths")
+load("@bazel_skylib//rules:build_test.bzl", "build_test")
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
load("//python:py_binary.bzl", "py_binary")
load("//python/private:util.bzl", "add_tag", "copy_propagating_kwargs") # buildifier: disable=bzl-visibility
+load(":sphinx_docs_library_info.bzl", "SphinxDocsLibraryInfo")
_SPHINX_BUILD_MAIN_SRC = Label("//sphinxdocs/private:sphinx_build.py")
_SPHINX_SERVE_MAIN_SRC = Label("//sphinxdocs/private:sphinx_server.py")
+_SphinxSourceTreeInfo = provider(
+ doc = "Information about source tree for Sphinx to build.",
+ fields = {
+ "source_root": """
+:type: str
+
+Path of the root directory for the source files (which are in DefaultInfo.files)
+""",
+ },
+)
+
def sphinx_build_binary(name, py_binary_rule = py_binary, **kwargs):
"""Create an executable with the sphinx-build command line interface.
@@ -29,13 +42,13 @@
needs at runtime.
Args:
- name: (str) name of the target. The name "sphinx-build" is the
+ name: {type}`str` name of the target. The name "sphinx-build" is the
conventional name to match what Sphinx itself uses.
- py_binary_rule: (optional callable) A `py_binary` compatible callable
+ py_binary_rule: {type}`callable` A `py_binary` compatible callable
for creating the target. If not set, the regular `py_binary`
rule is used. This allows using the version-aware rules, or
other alternative implementations.
- **kwargs: Additional kwargs to pass onto `py_binary`. The `srcs` and
+ **kwargs: {type}`dict` Additional kwargs to pass onto `py_binary`. The `srcs` and
`main` attributes must not be specified.
"""
add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_build_binary")
@@ -50,6 +63,7 @@
name,
*,
srcs = [],
+ deps = [],
renamed_srcs = {},
sphinx,
config,
@@ -61,53 +75,61 @@
"""Generate docs using Sphinx.
This generates three public targets:
- * `<name>`: The output of this target is a directory for each
- format Sphinx creates. This target also has a separate output
- group for each format. e.g. `--output_group=html` will only build
- the "html" format files.
- * `<name>_define`: A multi-string flag to add additional `-D`
- arguments to the Sphinx invocation. This is useful for overriding
- the version information in the config file for builds.
- * `<name>.serve`: A binary that locally serves the HTML output. This
- allows previewing docs during development.
+ * `<name>`: The output of this target is a directory for each
+ format Sphinx creates. This target also has a separate output
+ group for each format. e.g. `--output_group=html` will only build
+ the "html" format files.
+ * `<name>_define`: A multi-string flag to add additional `-D`
+ arguments to the Sphinx invocation. This is useful for overriding
+ the version information in the config file for builds.
+ * `<name>.serve`: A binary that locally serves the HTML output. This
+ allows previewing docs during development.
Args:
- name: (str) name of the docs rule.
- srcs: (label list) The source files for Sphinx to process.
- renamed_srcs: (label_keyed_string_dict) Doc source files for Sphinx that
+ name: {type}`Name` name of the docs rule.
+ srcs: {type}`list[label]` The source files for Sphinx to process.
+ deps: {type}`list[label]` of {obj}`sphinx_docs_library` targets.
+ renamed_srcs: {type}`dict[label, dict]` Doc source files for Sphinx that
are renamed. This is typically used for files elsewhere, such as top
level files in the repo.
- sphinx: (label) the Sphinx tool to use for building
+ sphinx: {type}`label` the Sphinx tool to use for building
documentation. Because Sphinx supports various plugins, you must
construct your own binary with the necessary dependencies. The
- `sphinx_build_binary` rule can be used to define such a binary, but
+ {obj}`sphinx_build_binary` rule can be used to define such a binary, but
any executable supporting the `sphinx-build` command line interface
can be used (typically some `py_binary` program).
- config: (label) the Sphinx config file (`conf.py`) to use.
+ config: {type}`label` the Sphinx config file (`conf.py`) to use.
formats: (list of str) the formats (`-b` flag) to generate documentation
in. Each format will become an output group.
- strip_prefix: (str) A prefix to remove from the file paths of the
- 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.
+ strip_prefix: {type}`str` A prefix to remove from the file paths of the
+ source files. e.g., given `//docs:foo.md`, stripping `docs/` makes
+ Sphinx see `foo.md` in its generated source directory. If not
+ specified, then {any}`native.package_name` is used.
+ extra_opts: {type}`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.
+ See {any}`ctx.expand_location`.
+ tools: {type}`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.
+ them, use {obj}`extra_opts` and `$(location)`.
+ **kwargs: {type}`dict` Common attributes to pass onto rules.
"""
add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_docs")
common_kwargs = copy_propagating_kwargs(kwargs)
+ _sphinx_source_tree(
+ name = name + "/_sources",
+ srcs = srcs,
+ deps = deps,
+ renamed_srcs = renamed_srcs,
+ config = config,
+ strip_prefix = strip_prefix,
+ **common_kwargs
+ )
_sphinx_docs(
name = name,
- srcs = srcs,
- renamed_srcs = renamed_srcs,
sphinx = sphinx,
- config = config,
formats = formats,
- strip_prefix = strip_prefix,
+ source_tree = name + "/_sources",
extra_opts = extra_opts,
tools = tools,
**kwargs
@@ -132,8 +154,15 @@
**common_kwargs
)
+ build_test(
+ name = name + "_build_test",
+ targets = [name],
+ **kwargs # kwargs used to pick up target_compatible_with
+ )
+
def _sphinx_docs_impl(ctx):
- source_dir_path, _, inputs = _create_sphinx_source_tree(ctx)
+ source_dir_path = ctx.attr.source_tree[_SphinxSourceTreeInfo].source_root
+ inputs = ctx.attr.source_tree[DefaultInfo].files
outputs = {}
for format in ctx.attr.formats:
@@ -156,21 +185,14 @@
_sphinx_docs = rule(
implementation = _sphinx_docs_impl,
attrs = {
- "config": attr.label(
- allow_single_file = True,
- mandatory = True,
- doc = "Config file for Sphinx",
- ),
"extra_opts": attr.string_list(
doc = "Additional options to pass onto Sphinx. These are added after " +
"other options, but before the source/output args.",
),
"formats": attr.string_list(doc = "Output formats for Sphinx to create."),
- "renamed_srcs": attr.label_keyed_string_dict(
- allow_files = True,
- doc = "Doc source files for Sphinx that are renamed. This is " +
- "typically used for files elsewhere, such as top level " +
- "files in the repo.",
+ "source_tree": attr.label(
+ doc = "Directory of files for Sphinx to process.",
+ providers = [_SphinxSourceTreeInfo],
),
"sphinx": attr.label(
executable = True,
@@ -178,11 +200,6 @@
mandatory = True,
doc = "Sphinx binary to generate documentation.",
),
- "srcs": attr.label_list(
- allow_files = True,
- 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.",
@@ -193,55 +210,6 @@
},
)
-def _create_sphinx_source_tree(ctx):
- # Sphinx only accepts a single directory to read its doc sources from.
- # Because plain files and generated files are in different directories,
- # we need to merge the two into a single directory.
- source_prefix = paths.join(ctx.label.name, "_sources")
- sphinx_source_files = []
-
- def _symlink_source(orig):
- source_rel_path = orig.short_path
- if source_rel_path.startswith(ctx.attr.strip_prefix):
- source_rel_path = source_rel_path[len(ctx.attr.strip_prefix):]
-
- sphinx_source = ctx.actions.declare_file(paths.join(source_prefix, source_rel_path))
- ctx.actions.symlink(
- output = sphinx_source,
- target_file = orig,
- progress_message = "Symlinking Sphinx source %{input} to %{output}",
- )
- sphinx_source_files.append(sphinx_source)
- return sphinx_source
-
- # Though Sphinx has a -c flag, we move the config file into the sources
- # directory to make the config more intuitive because some configuration
- # options are relative to the config location, not the sources directory.
- source_conf_file = _symlink_source(ctx.file.config)
- sphinx_source_dir_path = paths.dirname(source_conf_file.path)
-
- for orig_file in ctx.files.srcs:
- _symlink_source(orig_file)
-
- for src_target, dest in ctx.attr.renamed_srcs.items():
- src_files = src_target.files.to_list()
- if len(src_files) != 1:
- fail("A single file must be specified to be renamed. Target {} " +
- "generate {} files: {}".format(
- src_target,
- len(src_files),
- src_files,
- ))
- sphinx_src = ctx.actions.declare_file(paths.join(source_prefix, dest))
- ctx.actions.symlink(
- output = sphinx_src,
- target_file = src_files[0],
- progress_message = "Symlinking (renamed) Sphinx source %{input} to %{output}",
- )
- sphinx_source_files.append(sphinx_src)
-
- return sphinx_source_dir_path, source_conf_file, sphinx_source_files
-
def _run_sphinx(ctx, format, source_path, inputs, output_prefix):
output_dir = ctx.actions.declare_directory(paths.join(output_prefix, format))
@@ -281,6 +249,93 @@
)
return output_dir
+def _sphinx_source_tree_impl(ctx):
+ # Sphinx only accepts a single directory to read its doc sources from.
+ # Because plain files and generated files are in different directories,
+ # we need to merge the two into a single directory.
+ source_prefix = ctx.label.name
+ sphinx_source_files = []
+
+ # Materialize a file under the `_sources` dir
+ def _relocate(source_file, dest_path = None):
+ if not dest_path:
+ dest_path = source_file.short_path.removeprefix(ctx.attr.strip_prefix)
+ dest_file = ctx.actions.declare_file(paths.join(source_prefix, dest_path))
+ ctx.actions.symlink(
+ output = dest_file,
+ target_file = source_file,
+ progress_message = "Symlinking Sphinx source %{input} to %{output}",
+ )
+ sphinx_source_files.append(dest_file)
+ return dest_file
+
+ # Though Sphinx has a -c flag, we move the config file into the sources
+ # directory to make the config more intuitive because some configuration
+ # options are relative to the config location, not the sources directory.
+ source_conf_file = _relocate(ctx.file.config)
+ sphinx_source_dir_path = paths.dirname(source_conf_file.path)
+
+ for src in ctx.attr.srcs:
+ if SphinxDocsLibraryInfo in src:
+ fail((
+ "In attribute srcs: target {src} is misplaced here: " +
+ "sphinx_docs_library targets belong in the deps attribute."
+ ).format(src = src))
+
+ for orig_file in ctx.files.srcs:
+ _relocate(orig_file)
+
+ for src_target, dest in ctx.attr.renamed_srcs.items():
+ src_files = src_target.files.to_list()
+ if len(src_files) != 1:
+ fail("A single file must be specified to be renamed. Target {} " +
+ "generate {} files: {}".format(
+ src_target,
+ len(src_files),
+ src_files,
+ ))
+ _relocate(src_files[0], dest)
+
+ for t in ctx.attr.deps:
+ info = t[SphinxDocsLibraryInfo]
+ for entry in info.transitive.to_list():
+ for original in entry.files:
+ new_path = entry.prefix + original.short_path.removeprefix(entry.strip_prefix)
+ _relocate(original, new_path)
+
+ return [
+ DefaultInfo(
+ files = depset(sphinx_source_files),
+ ),
+ _SphinxSourceTreeInfo(
+ source_root = sphinx_source_dir_path,
+ ),
+ ]
+
+_sphinx_source_tree = rule(
+ implementation = _sphinx_source_tree_impl,
+ attrs = {
+ "config": attr.label(
+ allow_single_file = True,
+ mandatory = True,
+ doc = "Config file for Sphinx",
+ ),
+ "deps": attr.label_list(
+ providers = [SphinxDocsLibraryInfo],
+ ),
+ "renamed_srcs": attr.label_keyed_string_dict(
+ allow_files = True,
+ doc = "Doc source files for Sphinx that are renamed. This is " +
+ "typically used for files elsewhere, such as top level " +
+ "files in the repo.",
+ ),
+ "srcs": attr.label_list(
+ allow_files = True,
+ doc = "Doc source files for Sphinx.",
+ ),
+ "strip_prefix": attr.string(doc = "Prefix to remove from input file paths."),
+ },
+)
_FlagInfo = provider(
doc = "Provider for a flag value",
fields = ["value"],
@@ -294,7 +349,7 @@
build_setting = config.string_list(flag = True, repeatable = True),
)
-def sphinx_inventory(name, src, **kwargs):
+def sphinx_inventory(*, name, src, **kwargs):
"""Creates a compressed inventory file from an uncompressed on.
The Sphinx inventory format isn't formally documented, but is understood
@@ -324,11 +379,14 @@
* `display name` is a string. It can contain spaces, or simply be
the value `-` to indicate it is the same as `name`
+ :::{seealso}
+ {bzl:obj}`//sphinxdocs/inventories` for inventories of Bazel objects.
+ :::
Args:
- name: [`target-name`] name of the target.
- src: [`label`] Uncompressed inventory text file.
- **kwargs: additional kwargs of common attributes.
+ name: {type}`Name` name of the target.
+ src: {type}`label` Uncompressed inventory text file.
+ **kwargs: {type}`dict` additional kwargs of common attributes.
"""
_sphinx_inventory(name = name, src = src, **kwargs)
diff --git a/sphinxdocs/private/sphinx_docs_library.bzl b/sphinxdocs/private/sphinx_docs_library.bzl
new file mode 100644
index 0000000..076ed72
--- /dev/null
+++ b/sphinxdocs/private/sphinx_docs_library.bzl
@@ -0,0 +1,51 @@
+"""Implementation of sphinx_docs_library."""
+
+load(":sphinx_docs_library_info.bzl", "SphinxDocsLibraryInfo")
+
+def _sphinx_docs_library_impl(ctx):
+ strip_prefix = ctx.attr.strip_prefix or (ctx.label.package + "/")
+ direct_entries = []
+ if ctx.files.srcs:
+ entry = struct(
+ strip_prefix = strip_prefix,
+ prefix = ctx.attr.prefix,
+ files = ctx.files.srcs,
+ )
+ direct_entries.append(entry)
+
+ return [
+ SphinxDocsLibraryInfo(
+ strip_prefix = strip_prefix,
+ prefix = ctx.attr.prefix,
+ files = ctx.files.srcs,
+ transitive = depset(
+ direct = direct_entries,
+ transitive = [t[SphinxDocsLibraryInfo].transitive for t in ctx.attr.deps],
+ ),
+ ),
+ DefaultInfo(
+ files = depset(ctx.files.srcs),
+ ),
+ ]
+
+sphinx_docs_library = rule(
+ implementation = _sphinx_docs_library_impl,
+ attrs = {
+ "deps": attr.label_list(
+ doc = """
+Additional `sphinx_docs_library` targets to include. They do not have the
+`prefix` and `strip_prefix` attributes applied to them.""",
+ providers = [SphinxDocsLibraryInfo],
+ ),
+ "prefix": attr.string(
+ doc = "Prefix to prepend to file paths. Added after `strip_prefix` is removed.",
+ ),
+ "srcs": attr.label_list(
+ allow_files = True,
+ doc = "Files that are part of the library.",
+ ),
+ "strip_prefix": attr.string(
+ doc = "Prefix to remove from file paths. Removed before `prefix` is prepended.",
+ ),
+ },
+)
diff --git a/sphinxdocs/private/sphinx_docs_library_info.bzl b/sphinxdocs/private/sphinx_docs_library_info.bzl
new file mode 100644
index 0000000..de40d8d
--- /dev/null
+++ b/sphinxdocs/private/sphinx_docs_library_info.bzl
@@ -0,0 +1,30 @@
+"""Provider for collecting doc files as libraries."""
+
+SphinxDocsLibraryInfo = provider(
+ doc = "Information about a collection of doc files.",
+ fields = {
+ "files": """
+:type: depset[File]
+
+The documentation files for the library.
+""",
+ "prefix": """
+:type: str
+
+Prefix to prepend to file paths in `files`. It is added after `strip_prefix`
+is removed.
+""",
+ "strip_prefix": """
+:type: str
+
+Prefix to remove from file paths in `files`. It is removed before `prefix`
+is prepended.
+""",
+ "transitive": """
+:type: depset[struct]
+
+Depset of transitive library information. Each entry in the depset is a struct
+with fields matching the fields of this provider.
+""",
+ },
+)
diff --git a/sphinxdocs/private/sphinx_docs_library_macro.bzl b/sphinxdocs/private/sphinx_docs_library_macro.bzl
new file mode 100644
index 0000000..095b376
--- /dev/null
+++ b/sphinxdocs/private/sphinx_docs_library_macro.bzl
@@ -0,0 +1,13 @@
+"""Implementation of sphinx_docs_library macro."""
+
+load("//python/private:util.bzl", "add_tag") # buildifier: disable=bzl-visibility
+load(":sphinx_docs_library.bzl", _sphinx_docs_library = "sphinx_docs_library")
+
+def sphinx_docs_library(**kwargs):
+ """Collection of doc files for use by `sphinx_docs`.
+
+ Args:
+ **kwargs: Args passed onto underlying {bzl:rule}`sphinx_docs_library` rule
+ """
+ add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_docs_library")
+ _sphinx_docs_library(**kwargs)
diff --git a/sphinxdocs/private/sphinx_stardoc.bzl b/sphinxdocs/private/sphinx_stardoc.bzl
index e2b1756..d5869b0 100644
--- a/sphinxdocs/private/sphinx_stardoc.bzl
+++ b/sphinxdocs/private/sphinx_stardoc.bzl
@@ -14,12 +14,34 @@
"""Rules to generate Sphinx-compatible documentation for bzl files."""
+load("@bazel_skylib//:bzl_library.bzl", "StarlarkLibraryInfo")
+load("@bazel_skylib//lib:paths.bzl", "paths")
load("@bazel_skylib//lib:types.bzl", "types")
load("@bazel_skylib//rules:build_test.bzl", "build_test")
load("@io_bazel_stardoc//stardoc:stardoc.bzl", "stardoc")
load("//python/private:util.bzl", "add_tag", "copy_propagating_kwargs") # buildifier: disable=bzl-visibility
+load("//sphinxdocs/private:sphinx_docs_library_macro.bzl", "sphinx_docs_library")
-def sphinx_stardocs(name, docs, **kwargs):
+_StardocInputHelperInfo = provider(
+ doc = "Extracts the single source file from a bzl library.",
+ fields = {
+ "file": """
+:type: File
+
+The sole output file from the wrapped target.
+""",
+ },
+)
+
+def sphinx_stardocs(
+ *,
+ name,
+ srcs = [],
+ deps = [],
+ docs = {},
+ prefix = None,
+ strip_prefix = None,
+ **kwargs):
"""Generate Sphinx-friendly Markdown docs using Stardoc for bzl libraries.
A `build_test` for the docs is also generated to ensure Stardoc is able
@@ -28,8 +50,12 @@
NOTE: This generates MyST-flavored Markdown.
Args:
- name: `str`, the name of the resulting file group with the generated docs.
- docs: `dict[str output, source]` of the bzl files to generate documentation
+ name: {type}`Name`, the name of the resulting file group with the generated docs.
+ srcs: {type}`list[label]` Each source is either the bzl file to process
+ or a `bzl_library` target with one source file of the bzl file to
+ process.
+ deps: {type}`list[label]` Targets that provide files loaded by `src`
+ docs: {type}`dict[str, str|dict]` of the bzl files to generate documentation
for. The `output` key is the path of the output filename, e.g.,
`foo/bar.md`. The `source` values can be either of:
* A `str` label that points to a `bzl_library` target. The target
@@ -39,10 +65,17 @@
* A `dict` with keys `input` and `dep`. The `input` key is a string
label to the bzl file to generate docs for. The `dep` key is a
string label to a `bzl_library` providing the necessary dependencies.
+ prefix: {type}`str` Prefix to add to the output file path. It is prepended
+ after `strip_prefix` is removed.
+ strip_prefix: {type}`str | None` Prefix to remove from the input file path;
+ it is removed before `prefix` is prepended. If not specified, then
+ {any}`native.package_name` is used.
**kwargs: Additional kwargs to pass onto each `sphinx_stardoc` target
"""
+ internal_name = "_{}".format(name)
add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_stardocs")
common_kwargs = copy_propagating_kwargs(kwargs)
+ common_kwargs["target_compatible_with"] = kwargs.get("target_compatible_with")
stardocs = []
for out_name, entry in docs.items():
@@ -51,50 +84,165 @@
if types.is_string(entry):
stardoc_kwargs["deps"] = [entry]
- stardoc_kwargs["input"] = entry.replace("_bzl", ".bzl")
+ stardoc_kwargs["src"] = entry.replace("_bzl", ".bzl")
else:
stardoc_kwargs.update(entry)
+
+ # input is accepted for backwards compatiblity. Remove when ready.
+ if "src" not in stardoc_kwargs and "input" in stardoc_kwargs:
+ stardoc_kwargs["src"] = stardoc_kwargs.pop("input")
stardoc_kwargs["deps"] = [stardoc_kwargs.pop("dep")]
- doc_name = "_{}_{}".format(name.lstrip("_"), out_name.replace("/", "_"))
- _sphinx_stardoc(
+ doc_name = "{}_{}".format(internal_name, _name_from_label(out_name))
+ sphinx_stardoc(
name = doc_name,
- out = out_name,
+ output = out_name,
+ create_test = False,
**stardoc_kwargs
)
stardocs.append(doc_name)
- native.filegroup(
+ for label in srcs:
+ doc_name = "{}_{}".format(internal_name, _name_from_label(label))
+ sphinx_stardoc(
+ name = doc_name,
+ src = label,
+ # NOTE: We set prefix/strip_prefix here instead of
+ # on the sphinx_docs_library so that building the
+ # target produces markdown files in the expected location, which
+ # is convenient.
+ prefix = prefix,
+ strip_prefix = strip_prefix,
+ deps = deps,
+ create_test = False,
+ **common_kwargs
+ )
+ stardocs.append(doc_name)
+
+ sphinx_docs_library(
name = name,
- srcs = stardocs,
+ deps = stardocs,
**common_kwargs
)
- build_test(
- name = name + "_build_test",
- targets = stardocs,
+ if stardocs:
+ build_test(
+ name = name + "_build_test",
+ targets = stardocs,
+ **common_kwargs
+ )
+
+def sphinx_stardoc(
+ name,
+ src,
+ deps = [],
+ public_load_path = None,
+ prefix = None,
+ strip_prefix = None,
+ create_test = True,
+ output = None,
+ **kwargs):
+ """Generate Sphinx-friendly Markdown for a single bzl file.
+
+ Args:
+ name: {type}`Name` name for the target.
+ src: {type}`label` The bzl file to process, or a `bzl_library`
+ target with one source file of the bzl file to process.
+ deps: {type}`list[label]` Targets that provide files loaded by `src`
+ public_load_path: {type}`str | None` override the file name that
+ is reported as the file being.
+ prefix: {type}`str | None` prefix to add to the output file path
+ strip_prefix: {type}`str | None` Prefix to remove from the input file path.
+ If not specified, then {any}`native.package_name` is used.
+ create_test: {type}`bool` True if a test should be defined to verify the
+ docs are buildable, False if not.
+ output: {type}`str | None` Optional explicit output file to use. If
+ not set, the output name will be derived from `src`.
+ **kwargs: {type}`dict` common args passed onto rules.
+ """
+ internal_name = "_{}".format(name.lstrip("_"))
+ add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_stardoc")
+ common_kwargs = copy_propagating_kwargs(kwargs)
+ common_kwargs["target_compatible_with"] = kwargs.get("target_compatible_with")
+
+ input_helper_name = internal_name + ".primary_bzl_src"
+ _stardoc_input_helper(
+ name = input_helper_name,
+ target = src,
**common_kwargs
)
-def _sphinx_stardoc(*, name, out, public_load_path = None, **kwargs):
- stardoc_name = "_{}_stardoc".format(name.lstrip("_"))
+ stardoc_name = internal_name + "_stardoc"
+
+ # NOTE: The .binaryproto suffix is an optimization. It makes the stardoc()
+ # call avoid performing a copy of the output to the desired name.
stardoc_pb = stardoc_name + ".binaryproto"
- if not public_load_path:
- public_load_path = str(kwargs["input"])
-
stardoc(
name = stardoc_name,
+ input = input_helper_name,
out = stardoc_pb,
format = "proto",
- **kwargs
+ deps = [src] + deps,
+ **common_kwargs
)
+ pb2md_name = internal_name + "_pb2md"
_stardoc_proto_to_markdown(
- name = name,
+ name = pb2md_name,
src = stardoc_pb,
- output = out,
+ output = output,
+ output_name_from = input_helper_name if not output else None,
public_load_path = public_load_path,
+ strip_prefix = strip_prefix,
+ prefix = prefix,
+ **common_kwargs
)
+ sphinx_docs_library(
+ name = name,
+ srcs = [pb2md_name],
+ **common_kwargs
+ )
+ if create_test:
+ build_test(
+ name = name + "_build_test",
+ targets = [name],
+ **common_kwargs
+ )
+
+def _stardoc_input_helper_impl(ctx):
+ target = ctx.attr.target
+ if StarlarkLibraryInfo in target:
+ files = ctx.attr.target[StarlarkLibraryInfo].srcs
+ else:
+ files = target[DefaultInfo].files.to_list()
+
+ if len(files) == 0:
+ fail("Target {} produces no files, but must produce exactly 1 file".format(
+ ctx.attr.target.label,
+ ))
+ elif len(files) == 1:
+ primary = files[0]
+ else:
+ fail("Target {} produces {} files, but must produce exactly 1 file.".format(
+ ctx.attr.target.label,
+ len(files),
+ ))
+
+ return [
+ DefaultInfo(
+ files = depset([primary]),
+ ),
+ _StardocInputHelperInfo(
+ file = primary,
+ ),
+ ]
+
+_stardoc_input_helper = rule(
+ implementation = _stardoc_input_helper_impl,
+ attrs = {
+ "target": attr.label(allow_files = True),
+ },
+)
def _stardoc_proto_to_markdown_impl(ctx):
args = ctx.actions.args()
@@ -103,7 +251,16 @@
inputs = [ctx.file.src]
args.add("--proto", ctx.file.src)
- args.add("--output", ctx.outputs.output)
+
+ if not ctx.outputs.output:
+ output_name = ctx.attr.output_name_from[_StardocInputHelperInfo].file.short_path
+ output_name = paths.replace_extension(output_name, ".md")
+ output_name = ctx.attr.prefix + output_name.removeprefix(ctx.attr.strip_prefix)
+ output = ctx.actions.declare_file(output_name)
+ else:
+ output = ctx.outputs.output
+
+ args.add("--output", output)
if ctx.attr.public_load_path:
args.add("--public-load-path={}".format(ctx.attr.public_load_path))
@@ -112,17 +269,23 @@
executable = ctx.executable._proto_to_markdown,
arguments = [args],
inputs = inputs,
- outputs = [ctx.outputs.output],
+ outputs = [output],
mnemonic = "SphinxStardocProtoToMd",
progress_message = "SphinxStardoc: converting proto to markdown: %{input} -> %{output}",
)
+ return [DefaultInfo(
+ files = depset([output]),
+ )]
_stardoc_proto_to_markdown = rule(
implementation = _stardoc_proto_to_markdown_impl,
attrs = {
- "output": attr.output(mandatory = True),
+ "output": attr.output(mandatory = False),
+ "output_name_from": attr.label(),
+ "prefix": attr.string(),
"public_load_path": attr.string(),
"src": attr.label(allow_single_file = True, mandatory = True),
+ "strip_prefix": attr.string(),
"_proto_to_markdown": attr.label(
default = "//sphinxdocs/private:proto_to_markdown",
executable = True,
@@ -130,3 +293,7 @@
),
},
)
+
+def _name_from_label(label):
+ label = label.lstrip("/").lstrip(":").replace(":", "/")
+ return label
diff --git a/sphinxdocs/sphinx.bzl b/sphinxdocs/sphinx.bzl
index d9385bd..3c9dc6b 100644
--- a/sphinxdocs/sphinx.bzl
+++ b/sphinxdocs/sphinx.bzl
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-"""# Rules to generate Sphinx documentation.
+"""Rules to generate Sphinx documentation.
The general usage of the Sphinx rules requires two pieces:
diff --git a/sphinxdocs/sphinx_docs_library.bzl b/sphinxdocs/sphinx_docs_library.bzl
new file mode 100644
index 0000000..e864329
--- /dev/null
+++ b/sphinxdocs/sphinx_docs_library.bzl
@@ -0,0 +1,5 @@
+"""Library-like rule to collect docs."""
+
+load("//sphinxdocs/private:sphinx_docs_library_macro.bzl", _sphinx_docs_library = "sphinx_docs_library")
+
+sphinx_docs_library = _sphinx_docs_library
diff --git a/sphinxdocs/sphinx_stardoc.bzl b/sphinxdocs/sphinx_stardoc.bzl
index 623bc64..9913964 100644
--- a/sphinxdocs/sphinx_stardoc.bzl
+++ b/sphinxdocs/sphinx_stardoc.bzl
@@ -14,6 +14,7 @@
"""Rules to generate Sphinx-compatible documentation for bzl files."""
-load("//sphinxdocs/private:sphinx_stardoc.bzl", _sphinx_stardocs = "sphinx_stardocs")
+load("//sphinxdocs/private:sphinx_stardoc.bzl", _sphinx_stardoc = "sphinx_stardoc", _sphinx_stardocs = "sphinx_stardocs")
sphinx_stardocs = _sphinx_stardocs
+sphinx_stardoc = _sphinx_stardoc
diff --git a/sphinxdocs/src/sphinx_bzl/bzl.py b/sphinxdocs/src/sphinx_bzl/bzl.py
index be38d8a..d09914b 100644
--- a/sphinxdocs/src/sphinx_bzl/bzl.py
+++ b/sphinxdocs/src/sphinx_bzl/bzl.py
@@ -1413,6 +1413,7 @@
"obj": roles.XRefRole(),
"required-providers": _RequiredProvidersRole(),
"return-type": _ReturnTypeRole(),
+ "rule": roles.XRefRole(),
"target": roles.XRefRole(),
"type": _TypeRole(),
}
diff --git a/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel b/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel
index b141e5f..e2837ff 100644
--- a/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel
+++ b/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel
@@ -1,7 +1,19 @@
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility
load("//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs")
-load("//sphinxdocs:sphinx_stardoc.bzl", "sphinx_stardocs")
+load("//sphinxdocs:sphinx_stardoc.bzl", "sphinx_stardoc", "sphinx_stardocs")
+
+# We only build for Linux and Mac because:
+# 1. The actual doc process only runs on Linux
+# 2. Mac is a common development platform, and is close enough to Linux
+# it's feasible to make work.
+# Making CI happy under Windows is too much of a headache, though, so we don't
+# bother with that.
+_TARGET_COMPATIBLE_WITH = select({
+ "@platforms//os:linux": [],
+ "@platforms//os:macos": [],
+ "//conditions:default": ["@platforms//:incompatible"],
+}) if IS_BAZEL_7_OR_HIGHER else ["@platforms//:incompatible"]
sphinx_docs(
name = "docs",
@@ -9,7 +21,7 @@
include = [
"*.md",
],
- ) + [":bzl_docs"],
+ ),
config = "conf.py",
formats = [
"html",
@@ -19,37 +31,48 @@
},
sphinx = ":sphinx-build",
strip_prefix = package_name() + "/",
- # We only develop the docs using Linux/Mac, and there are deps that
- # don't work for Windows, so just skip Windows.
- target_compatible_with = select({
- "@platforms//os:linux": [],
- "@platforms//os:macos": [],
- "//conditions:default": ["@platforms//:incompatible"],
- }) if IS_BAZEL_7_OR_HIGHER else ["@platforms//:incompatible"],
+ target_compatible_with = _TARGET_COMPATIBLE_WITH,
+ deps = [
+ ":bzl_function",
+ ":bzl_providers",
+ ":simple_bzl_docs",
+ ],
)
sphinx_stardocs(
- name = "bzl_docs",
- docs = {
- "bzl_function.md": dict(
- dep = ":all_bzl",
- input = "//sphinxdocs/tests/sphinx_stardoc:bzl_function.bzl",
- ),
- "bzl_providers.md": dict(
- dep = ":all_bzl",
- input = "//sphinxdocs/tests/sphinx_stardoc:bzl_providers.bzl",
- ),
- "bzl_rule.md": dict(
- dep = ":all_bzl",
- input = "//sphinxdocs/tests/sphinx_stardoc:bzl_rule.bzl",
- ),
- },
- target_compatible_with = [] if IS_BAZEL_7_OR_HIGHER else ["@platforms//:incompatible"],
+ name = "simple_bzl_docs",
+ srcs = [":bzl_rule_bzl"],
+ target_compatible_with = _TARGET_COMPATIBLE_WITH,
+)
+
+sphinx_stardoc(
+ name = "bzl_function",
+ src = ":bzl_function.bzl",
+ target_compatible_with = _TARGET_COMPATIBLE_WITH,
+ deps = [":func_and_providers_bzl"],
+)
+
+sphinx_stardoc(
+ name = "bzl_providers",
+ src = ":bzl_providers.bzl",
+ prefix = "addprefix_",
+ target_compatible_with = _TARGET_COMPATIBLE_WITH,
+ deps = [":func_and_providers_bzl"],
+)
+
+# A bzl_library with multiple sources
+bzl_library(
+ name = "func_and_providers_bzl",
+ srcs = [
+ "bzl_function.bzl",
+ "bzl_providers.bzl",
+ ],
)
bzl_library(
- name = "all_bzl",
- srcs = glob(["*.bzl"]),
+ name = "bzl_rule_bzl",
+ srcs = ["bzl_rule.bzl"],
+ deps = [":func_and_providers_bzl"],
)
sphinx_build_binary(