diff --git a/docs/sphinx/BUILD.bazel b/docs/sphinx/BUILD.bazel index 947ebbae1..03c0e446b 100644 --- a/docs/sphinx/BUILD.bazel +++ b/docs/sphinx/BUILD.bazel @@ -19,7 +19,7 @@ load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: dis load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility load("//sphinxdocs:readthedocs.bzl", "readthedocs_install") 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 @@ -38,9 +38,7 @@ _TARGET_COMPATIBLE_WITH = select({ # * `ibazel build //docs/sphinx:docs` to automatically rebuild docs sphinx_docs( name = "docs", - srcs = [ - ":bzl_api_docs", - ] + glob( + srcs = glob( include = [ "*.md", "**/*.md", @@ -66,43 +64,56 @@ sphinx_docs( strip_prefix = package_name() + "/", tags = ["docs"], target_compatible_with = _TARGET_COMPATIBLE_WITH, + deps = [ + ":bzl_api_docs", + "//sphinxdocs/docs:docs_lib", + ], ) sphinx_stardocs( name = "bzl_api_docs", - docs = { - "api/python/cc/py_cc_toolchain.md": dict( - dep = "//python/private:py_cc_toolchain_bzl", - input = "//python/private:py_cc_toolchain_rule.bzl", - public_load_path = "//python/cc:py_cc_toolchain.bzl", - ), - "api/python/cc/py_cc_toolchain_info.md": "//python/cc:py_cc_toolchain_info_bzl", - "api/python/defs.md": "//python:defs_bzl", - "api/python/entry_points/py_console_script_binary.md": "//python/entry_points:py_console_script_binary_bzl", - "api/python/packaging.md": "//python:packaging_bzl", - "api/python/pip.md": "//python:pip_bzl", - "api/python/private/common/py_binary_rule_bazel.md": "//python/private/common:py_binary_rule_bazel_bzl", - "api/python/private/common/py_library_rule_bazel.md": "//python/private/common:py_library_rule_bazel_bzl", - "api/python/private/common/py_runtime_rule.md": "//python/private/common:py_runtime_rule_bzl", - "api/python/private/common/py_test_rule_bazel.md": "//python/private/common:py_test_rule_bazel_bzl", - "api/python/py_binary.md": "//python:py_binary_bzl", - "api/python/py_cc_link_params_info.md": "//python:py_cc_link_params_info_bzl", - "api/python/py_library.md": "//python:py_library_bzl", - "api/python/py_runtime.md": "//python:py_runtime_bzl", - "api/python/py_runtime_info.md": "//python:py_runtime_info_bzl", - "api/python/py_runtime_pair.md": dict( - dep = "//python/private:py_runtime_pair_rule_bzl", - input = "//python/private:py_runtime_pair_rule.bzl", - public_load_path = "//python:py_runtime_pair.bzl", - ), - "api/python/py_test.md": "//python:py_test_bzl", - } | ({ + srcs = [ + "//python:defs_bzl", + "//python:packaging_bzl", + "//python:pip_bzl", + "//python:py_binary_bzl", + "//python:py_cc_link_params_info_bzl", + "//python:py_library_bzl", + "//python:py_runtime_bzl", + "//python:py_runtime_info_bzl", + "//python:py_test_bzl", + "//python/cc:py_cc_toolchain_info_bzl", + "//python/entry_points:py_console_script_binary_bzl", + "//python/private/common:py_binary_rule_bazel_bzl", + "//python/private/common:py_library_rule_bazel_bzl", + "//python/private/common:py_runtime_rule_bzl", + "//python/private/common:py_test_rule_bazel_bzl", + ] + ([ # Bazel 6 + Stardoc isn't able to parse something about the python bzlmod extension - "api/python/extensions/python.md": "//python/extensions:python_bzl", - } if IS_BAZEL_7_OR_HIGHER else {}) | ({ + "//python/extensions:python_bzl", + ] if IS_BAZEL_7_OR_HIGHER else []) + ([ # This depends on @pythons_hub, which is only created under bzlmod, - "api/python/extensions/pip.md": "//python/extensions:pip_bzl", - } if IS_BAZEL_7_OR_HIGHER and BZLMOD_ENABLED else {}), + "//python/extensions:pip_bzl", + ] if IS_BAZEL_7_OR_HIGHER and BZLMOD_ENABLED else []), + prefix = "api/", + tags = ["docs"], + target_compatible_with = _TARGET_COMPATIBLE_WITH, +) + +sphinx_stardoc( + name = "py_cc_toolchain", + src = "//python/private:py_cc_toolchain_rule.bzl", + prefix = "api/", + public_load_path = "//python/cc:py_cc_toolchain.bzl", + tags = ["docs"], + target_compatible_with = _TARGET_COMPATIBLE_WITH, + deps = ["//python/cc:py_cc_toolchain_bzl"], +) + +sphinx_stardoc( + name = "py_runtime_pair", + src = "//python/private:py_runtime_pair_rule_bzl", + public_load_path = "//python:py_runtime_pair.bzl", tags = ["docs"], target_compatible_with = _TARGET_COMPATIBLE_WITH, ) diff --git a/docs/sphinx/_stardoc_footer.md b/docs/sphinx/_stardoc_footer.md deleted file mode 100644 index 7aa33f778..000000000 --- a/docs/sphinx/_stardoc_footer.md +++ /dev/null @@ -1,15 +0,0 @@ - -[`Action`]: https://bazel.build/rules/lib/Action -[`bool`]: https://bazel.build/rules/lib/bool -[`depset`]: https://bazel.build/rules/lib/depset -[`dict`]: https://bazel.build/rules/lib/dict -[`File`]: https://bazel.build/rules/lib/File -[`Label`]: https://bazel.build/rules/lib/Label -[`list`]: https://bazel.build/rules/lib/list -[`str`]: https://bazel.build/rules/lib/string -[str]: https://bazel.build/rules/lib/string -[`int`]: https://bazel.build/rules/lib/int -[`struct`]: https://bazel.build/rules/lib/builtins/struct -[`Target`]: https://bazel.build/rules/lib/Target -[target-name]: https://bazel.build/concepts/labels#target-names -[attr-label]: https://bazel.build/concepts/labels diff --git a/docs/sphinx/conf.py b/docs/sphinx/conf.py index b3155778e..3200466ef 100644 --- a/docs/sphinx/conf.py +++ b/docs/sphinx/conf.py @@ -144,7 +144,12 @@ # -- Options for EPUB output epub_show_urls = "footnote" -suppress_warnings = [] +suppress_warnings = [ + # The autosectionlabel extension turns header titles into referencable + # names. Unfortunately, CHANGELOG.md has many duplicate header titles, + # which creates lots of warning spam. Just ignore them. + "autosectionlabel.*" +] def setup(app): diff --git a/docs/sphinx/index.md b/docs/sphinx/index.md index 8405eacb3..445cf2026 100644 --- a/docs/sphinx/index.md +++ b/docs/sphinx/index.md @@ -67,6 +67,7 @@ support Changelog api/index environment-variables +Sphinxdocs glossary genindex ``` diff --git a/sphinxdocs/BUILD.bazel b/sphinxdocs/BUILD.bazel index 6cb69ba88..9ad1e1eef 100644 --- a/sphinxdocs/BUILD.bazel +++ b/sphinxdocs/BUILD.bazel @@ -47,6 +47,12 @@ bzl_library( deps = ["//sphinxdocs/private:sphinx_bzl"], ) +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"], diff --git a/sphinxdocs/docs/BUILD.bazel b/sphinxdocs/docs/BUILD.bazel new file mode 100644 index 000000000..ed384e7d8 --- /dev/null +++ b/sphinxdocs/docs/BUILD.bazel @@ -0,0 +1,41 @@ +load("//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library") +load("//sphinxdocs:sphinx_stardoc.bzl", "sphinx_stardocs") + +package(default_visibility = ["//:__subpackages__"]) + +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/", +) diff --git a/sphinxdocs/docs/api/sphinxdocs/index.md b/sphinxdocs/docs/api/sphinxdocs/index.md new file mode 100644 index 000000000..bd4e9b6ee --- /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 000000000..a03645ed4 --- /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 000000000..ac857d625 --- /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 000000000..c6dc430dd --- /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 ` +``` + +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 000000000..d131607c8 --- /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 a7f022232..445f0f71f 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.label_list bzl:type 1 rules/lib/toplevel/attr#label_list - 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 ec6a945ac..d91e048e8 100644 --- a/sphinxdocs/private/BUILD.bazel +++ b/sphinxdocs/private/BUILD.bazel @@ -26,25 +26,44 @@ package( # 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", ], visibility = ["//visibility:public"], ) +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 @@ bzl_library( 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 ee8e7aa0e..a62c51b86 100644 --- a/sphinxdocs/private/readthedocs.bzl +++ b/sphinxdocs/private/readthedocs.bzl @@ -27,11 +27,11 @@ def readthedocs_install(name, docs, **kwargs): 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 a5ac83152..a198291a8 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 @@ def sphinx_build_binary(name, py_binary_rule = py_binary, **kwargs): 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 @@ def sphinx_docs( name, *, srcs = [], + deps = [], renamed_srcs = {}, sphinx, config, @@ -61,53 +75,61 @@ def sphinx_docs( """Generate docs using Sphinx. This generates three public targets: - * ``: 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. - * `_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. - * `.serve`: A binary that locally serves the HTML output. This - allows previewing docs during development. + * ``: 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. + * `_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. + * `.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_docs( - name = name, + _sphinx_source_tree( + name = name + "/_sources", srcs = srcs, + deps = deps, renamed_srcs = renamed_srcs, - sphinx = sphinx, config = config, - formats = formats, strip_prefix = strip_prefix, + **common_kwargs + ) + _sphinx_docs( + name = name, + sphinx = sphinx, + formats = formats, + source_tree = name + "/_sources", extra_opts = extra_opts, tools = tools, **kwargs @@ -132,8 +154,15 @@ def sphinx_docs( **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 @@ def _sphinx_docs_impl(ctx): _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 @@ _sphinx_docs = rule( 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 @@ _sphinx_docs = rule( }, ) -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 @@ def _run_sphinx(ctx, format, source_path, inputs, output_prefix): ) 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 @@ repeated_string_list_flag = rule( 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 @@ def sphinx_inventory(name, src, **kwargs): * `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 000000000..076ed7225 --- /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 000000000..de40d8dee --- /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 000000000..095b3769c --- /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 e2b1756e1..85cb82d41 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 @@ def sphinx_stardocs(name, docs, **kwargs): 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,8 +65,14 @@ def sphinx_stardocs(name, docs, **kwargs): * 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) @@ -51,50 +83,164 @@ def sphinx_stardocs(name, docs, **kwargs): 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, + **kwargs # For target_compatible_with + ) + +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) + + 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_pb = stardoc_name + ".binaryproto" + stardoc_name = internal_name + "_stardoc" - if not public_load_path: - public_load_path = str(kwargs["input"]) + # 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" 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], + **kwargs ) + if create_test: + build_test( + name = name + "_build_test", + targets = [name], + **kwargs # To capture target_compatible_with + ) + +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 +249,16 @@ def _stardoc_proto_to_markdown_impl(ctx): 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 +267,23 @@ def _stardoc_proto_to_markdown_impl(ctx): 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 +291,7 @@ _stardoc_proto_to_markdown = rule( ), }, ) + +def _name_from_label(label): + label = label.lstrip("/").lstrip(":").replace(":", "/") + return label diff --git a/sphinxdocs/sphinx.bzl b/sphinxdocs/sphinx.bzl index d9385bda3..3c9dc6b51 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 000000000..e86432996 --- /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 623bc64d0..991396435 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 be38d8a7c..d09914b31 100644 --- a/sphinxdocs/src/sphinx_bzl/bzl.py +++ b/sphinxdocs/src/sphinx_bzl/bzl.py @@ -1413,6 +1413,7 @@ class _BzlDomain(domains.Domain): "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 b141e5f0f..712623aba 100644 --- a/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel +++ b/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel @@ -1,7 +1,7 @@ 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") sphinx_docs( name = "docs", @@ -9,7 +9,7 @@ sphinx_docs( include = [ "*.md", ], - ) + [":bzl_docs"], + ), config = "conf.py", formats = [ "html", @@ -26,30 +26,45 @@ sphinx_docs( "@platforms//os:macos": [], "//conditions:default": ["@platforms//:incompatible"], }) if IS_BAZEL_7_OR_HIGHER else ["@platforms//:incompatible"], + 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", - ), - }, + name = "simple_bzl_docs", + srcs = [":bzl_rule_bzl"], target_compatible_with = [] if IS_BAZEL_7_OR_HIGHER else ["@platforms//:incompatible"], ) +sphinx_stardoc( + name = "bzl_function", + src = ":bzl_function.bzl", + deps = [":func_and_providers_bzl"], +) + +sphinx_stardoc( + name = "bzl_providers", + src = ":bzl_providers.bzl", + prefix = "addprefix_", + 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(