xref: /aosp_15_r20/external/bazelbuild-rules_python/sphinxdocs/private/sphinx_stardoc.bzl (revision 60517a1edbc8ecf509223e9af94a7adec7d736b8)
1# Copyright 2023 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Rules to generate Sphinx-compatible documentation for bzl files."""
16
17load("@bazel_skylib//:bzl_library.bzl", "StarlarkLibraryInfo")
18load("@bazel_skylib//lib:paths.bzl", "paths")
19load("@bazel_skylib//lib:types.bzl", "types")
20load("@bazel_skylib//rules:build_test.bzl", "build_test")
21load("@io_bazel_stardoc//stardoc:stardoc.bzl", "stardoc")
22load("//python/private:util.bzl", "add_tag", "copy_propagating_kwargs")  # buildifier: disable=bzl-visibility
23load("//sphinxdocs/private:sphinx_docs_library_macro.bzl", "sphinx_docs_library")
24
25_StardocInputHelperInfo = provider(
26    doc = "Extracts the single source file from a bzl library.",
27    fields = {
28        "file": """
29:type: File
30
31The sole output file from the wrapped target.
32""",
33    },
34)
35
36def sphinx_stardocs(
37        *,
38        name,
39        srcs = [],
40        deps = [],
41        docs = {},
42        prefix = None,
43        strip_prefix = None,
44        **kwargs):
45    """Generate Sphinx-friendly Markdown docs using Stardoc for bzl libraries.
46
47    A `build_test` for the docs is also generated to ensure Stardoc is able
48    to process the files.
49
50    NOTE: This generates MyST-flavored Markdown.
51
52    Args:
53        name: {type}`Name`, the name of the resulting file group with the generated docs.
54        srcs: {type}`list[label]` Each source is either the bzl file to process
55            or a `bzl_library` target with one source file of the bzl file to
56            process.
57        deps: {type}`list[label]` Targets that provide files loaded by `src`
58        docs: {type}`dict[str, str|dict]` of the bzl files to generate documentation
59            for. The `output` key is the path of the output filename, e.g.,
60            `foo/bar.md`. The `source` values can be either of:
61            * A `str` label that points to a `bzl_library` target. The target
62              name will replace `_bzl` with `.bzl` and use that as the input
63              bzl file to generate docs for. The target itself provides the
64              necessary dependencies.
65            * A `dict` with keys `input` and `dep`. The `input` key is a string
66              label to the bzl file to generate docs for. The `dep` key is a
67              string label to a `bzl_library` providing the necessary dependencies.
68        prefix: {type}`str` Prefix to add to the output file path. It is prepended
69            after `strip_prefix` is removed.
70        strip_prefix: {type}`str | None` Prefix to remove from the input file path;
71            it is removed before `prefix` is prepended. If not specified, then
72            {any}`native.package_name` is used.
73        **kwargs: Additional kwargs to pass onto each `sphinx_stardoc` target
74    """
75    internal_name = "_{}".format(name)
76    add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_stardocs")
77    common_kwargs = copy_propagating_kwargs(kwargs)
78    common_kwargs["target_compatible_with"] = kwargs.get("target_compatible_with")
79
80    stardocs = []
81    for out_name, entry in docs.items():
82        stardoc_kwargs = {}
83        stardoc_kwargs.update(kwargs)
84
85        if types.is_string(entry):
86            stardoc_kwargs["deps"] = [entry]
87            stardoc_kwargs["src"] = entry.replace("_bzl", ".bzl")
88        else:
89            stardoc_kwargs.update(entry)
90
91            # input is accepted for backwards compatiblity. Remove when ready.
92            if "src" not in stardoc_kwargs and "input" in stardoc_kwargs:
93                stardoc_kwargs["src"] = stardoc_kwargs.pop("input")
94            stardoc_kwargs["deps"] = [stardoc_kwargs.pop("dep")]
95
96        doc_name = "{}_{}".format(internal_name, _name_from_label(out_name))
97        sphinx_stardoc(
98            name = doc_name,
99            output = out_name,
100            create_test = False,
101            **stardoc_kwargs
102        )
103        stardocs.append(doc_name)
104
105    for label in srcs:
106        doc_name = "{}_{}".format(internal_name, _name_from_label(label))
107        sphinx_stardoc(
108            name = doc_name,
109            src = label,
110            # NOTE: We set prefix/strip_prefix here instead of
111            # on the sphinx_docs_library so that building the
112            # target produces markdown files in the expected location, which
113            # is convenient.
114            prefix = prefix,
115            strip_prefix = strip_prefix,
116            deps = deps,
117            create_test = False,
118            **common_kwargs
119        )
120        stardocs.append(doc_name)
121
122    sphinx_docs_library(
123        name = name,
124        deps = stardocs,
125        **common_kwargs
126    )
127    if stardocs:
128        build_test(
129            name = name + "_build_test",
130            targets = stardocs,
131            **common_kwargs
132        )
133
134def sphinx_stardoc(
135        name,
136        src,
137        deps = [],
138        public_load_path = None,
139        prefix = None,
140        strip_prefix = None,
141        create_test = True,
142        output = None,
143        **kwargs):
144    """Generate Sphinx-friendly Markdown for a single bzl file.
145
146    Args:
147        name: {type}`Name` name for the target.
148        src: {type}`label` The bzl file to process, or a `bzl_library`
149            target with one source file of the bzl file to process.
150        deps: {type}`list[label]` Targets that provide files loaded by `src`
151        public_load_path: {type}`str | None` override the file name that
152            is reported as the file being.
153        prefix: {type}`str | None` prefix to add to the output file path
154        strip_prefix: {type}`str | None` Prefix to remove from the input file path.
155            If not specified, then {any}`native.package_name` is used.
156        create_test: {type}`bool` True if a test should be defined to verify the
157            docs are buildable, False if not.
158        output: {type}`str | None` Optional explicit output file to use. If
159            not set, the output name will be derived from `src`.
160        **kwargs: {type}`dict` common args passed onto rules.
161    """
162    internal_name = "_{}".format(name.lstrip("_"))
163    add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_stardoc")
164    common_kwargs = copy_propagating_kwargs(kwargs)
165    common_kwargs["target_compatible_with"] = kwargs.get("target_compatible_with")
166
167    input_helper_name = internal_name + ".primary_bzl_src"
168    _stardoc_input_helper(
169        name = input_helper_name,
170        target = src,
171        **common_kwargs
172    )
173
174    stardoc_name = internal_name + "_stardoc"
175
176    # NOTE: The .binaryproto suffix is an optimization. It makes the stardoc()
177    # call avoid performing a copy of the output to the desired name.
178    stardoc_pb = stardoc_name + ".binaryproto"
179
180    stardoc(
181        name = stardoc_name,
182        input = input_helper_name,
183        out = stardoc_pb,
184        format = "proto",
185        deps = [src] + deps,
186        **common_kwargs
187    )
188
189    pb2md_name = internal_name + "_pb2md"
190    _stardoc_proto_to_markdown(
191        name = pb2md_name,
192        src = stardoc_pb,
193        output = output,
194        output_name_from = input_helper_name if not output else None,
195        public_load_path = public_load_path,
196        strip_prefix = strip_prefix,
197        prefix = prefix,
198        **common_kwargs
199    )
200    sphinx_docs_library(
201        name = name,
202        srcs = [pb2md_name],
203        **common_kwargs
204    )
205    if create_test:
206        build_test(
207            name = name + "_build_test",
208            targets = [name],
209            **common_kwargs
210        )
211
212def _stardoc_input_helper_impl(ctx):
213    target = ctx.attr.target
214    if StarlarkLibraryInfo in target:
215        files = ctx.attr.target[StarlarkLibraryInfo].srcs
216    else:
217        files = target[DefaultInfo].files.to_list()
218
219    if len(files) == 0:
220        fail("Target {} produces no files, but must produce exactly 1 file".format(
221            ctx.attr.target.label,
222        ))
223    elif len(files) == 1:
224        primary = files[0]
225    else:
226        fail("Target {} produces {} files, but must produce exactly 1 file.".format(
227            ctx.attr.target.label,
228            len(files),
229        ))
230
231    return [
232        DefaultInfo(
233            files = depset([primary]),
234        ),
235        _StardocInputHelperInfo(
236            file = primary,
237        ),
238    ]
239
240_stardoc_input_helper = rule(
241    implementation = _stardoc_input_helper_impl,
242    attrs = {
243        "target": attr.label(allow_files = True),
244    },
245)
246
247def _stardoc_proto_to_markdown_impl(ctx):
248    args = ctx.actions.args()
249    args.use_param_file("@%s")
250    args.set_param_file_format("multiline")
251
252    inputs = [ctx.file.src]
253    args.add("--proto", ctx.file.src)
254
255    if not ctx.outputs.output:
256        output_name = ctx.attr.output_name_from[_StardocInputHelperInfo].file.short_path
257        output_name = paths.replace_extension(output_name, ".md")
258        output_name = ctx.attr.prefix + output_name.removeprefix(ctx.attr.strip_prefix)
259        output = ctx.actions.declare_file(output_name)
260    else:
261        output = ctx.outputs.output
262
263    args.add("--output", output)
264
265    if ctx.attr.public_load_path:
266        args.add("--public-load-path={}".format(ctx.attr.public_load_path))
267
268    ctx.actions.run(
269        executable = ctx.executable._proto_to_markdown,
270        arguments = [args],
271        inputs = inputs,
272        outputs = [output],
273        mnemonic = "SphinxStardocProtoToMd",
274        progress_message = "SphinxStardoc: converting proto to markdown: %{input} -> %{output}",
275    )
276    return [DefaultInfo(
277        files = depset([output]),
278    )]
279
280_stardoc_proto_to_markdown = rule(
281    implementation = _stardoc_proto_to_markdown_impl,
282    attrs = {
283        "output": attr.output(mandatory = False),
284        "output_name_from": attr.label(),
285        "prefix": attr.string(),
286        "public_load_path": attr.string(),
287        "src": attr.label(allow_single_file = True, mandatory = True),
288        "strip_prefix": attr.string(),
289        "_proto_to_markdown": attr.label(
290            default = "//sphinxdocs/private:proto_to_markdown",
291            executable = True,
292            cfg = "exec",
293        ),
294    },
295)
296
297def _name_from_label(label):
298    label = label.lstrip("/").lstrip(":").replace(":", "/")
299    return label
300