# Copyright 2023 The Bazel Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """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") _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 to process the files. NOTE: This generates MyST-flavored Markdown. Args: 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 name will replace `_bzl` with `.bzl` and use that as the input bzl file to generate docs for. The target itself provides the necessary dependencies. * 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(): stardoc_kwargs = {} stardoc_kwargs.update(kwargs) if types.is_string(entry): stardoc_kwargs["deps"] = [entry] 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(internal_name, _name_from_label(out_name)) sphinx_stardoc( name = doc_name, output = out_name, create_test = False, **stardoc_kwargs ) stardocs.append(doc_name) 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, deps = stardocs, **common_kwargs ) 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 ) 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" stardoc( name = stardoc_name, input = input_helper_name, out = stardoc_pb, format = "proto", deps = [src] + deps, **common_kwargs ) pb2md_name = internal_name + "_pb2md" _stardoc_proto_to_markdown( name = pb2md_name, src = stardoc_pb, 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() args.use_param_file("@%s") args.set_param_file_format("multiline") inputs = [ctx.file.src] args.add("--proto", ctx.file.src) 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)) ctx.actions.run( executable = ctx.executable._proto_to_markdown, arguments = [args], inputs = inputs, 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 = 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, cfg = "exec", ), }, ) def _name_from_label(label): label = label.lstrip("/").lstrip(":").replace(":", "/") return label