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