gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/website/defs.bzl (about)

     1  """Wrappers for website documentation."""
     2  
     3  load("//tools:defs.bzl", "short_path")
     4  
     5  # DocInfo is a provider which simple adds sufficient metadata to the source
     6  # files (and additional data files) so that a jeyll header can be constructed
     7  # dynamically. This is done the via BUILD system so that the plain
     8  # documentation files can be viewable without non-compliant markdown headers.
     9  DocInfo = provider(
    10      "Encapsulates information for a documentation page.",
    11      fields = [
    12          "layout",
    13          "description",
    14          "permalink",
    15          "category",
    16          "subcategory",
    17          "weight",
    18          "editpath",
    19          "authors",
    20          "include_in_menu",
    21      ],
    22  )
    23  
    24  def _doc_impl(ctx):
    25      return [
    26          DefaultInfo(
    27              files = depset(ctx.files.src + ctx.files.data),
    28          ),
    29          DocInfo(
    30              layout = ctx.attr.layout,
    31              description = ctx.attr.description,
    32              permalink = ctx.attr.permalink,
    33              category = ctx.attr.category,
    34              subcategory = ctx.attr.subcategory,
    35              weight = ctx.attr.weight,
    36              editpath = short_path(ctx.files.src[0].short_path),
    37              authors = ctx.attr.authors,
    38              include_in_menu = ctx.attr.include_in_menu,
    39          ),
    40      ]
    41  
    42  doc = rule(
    43      implementation = _doc_impl,
    44      doc = "Annotate a document for jekyll headers.",
    45      attrs = {
    46          "src": attr.label(
    47              doc = "The markdown source file.",
    48              mandatory = True,
    49              allow_single_file = True,
    50          ),
    51          "data": attr.label_list(
    52              doc = "Additional data files (e.g. images).",
    53              allow_files = True,
    54          ),
    55          "layout": attr.string(
    56              doc = "The document layout.",
    57              default = "docs",
    58          ),
    59          "description": attr.string(
    60              doc = "The document description.",
    61              default = "",
    62          ),
    63          "permalink": attr.string(
    64              doc = "The document permalink.",
    65              mandatory = True,
    66          ),
    67          "category": attr.string(
    68              doc = "The document category.",
    69              default = "",
    70          ),
    71          "subcategory": attr.string(
    72              doc = "The document subcategory.",
    73              default = "",
    74          ),
    75          "weight": attr.string(
    76              doc = "The document weight.",
    77              default = "50",
    78          ),
    79          "authors": attr.string_list(),
    80          "include_in_menu": attr.bool(
    81              doc = "Include document in the navigation menu.",
    82              default = True,
    83          ),
    84      },
    85  )
    86  
    87  def _docs_impl(ctx):
    88      # Tarball is the actual output.
    89      tarball = ctx.actions.declare_file(ctx.label.name + ".tgz")
    90  
    91      # But we need an intermediate builder to translate the files.
    92      builder = ctx.actions.declare_file("%s-builder" % ctx.label.name)
    93      builder_content = [
    94          "#!/bin/bash",
    95          "set -euo pipefail",
    96          "declare -r T=$(mktemp -d)",
    97          "function cleanup {",
    98          "    rm -rf $T",
    99          "}",
   100          "trap cleanup EXIT",
   101      ]
   102      for dep in ctx.attr.deps:
   103          doc = dep[DocInfo]
   104  
   105          # Sanity check the permalink.
   106          if not doc.permalink.endswith("/"):
   107              fail("permalink %s for target %s should end with /" % (
   108                  doc.permalink,
   109                  ctx.label.name,
   110              ))
   111  
   112          # Construct the header.
   113          header = """\
   114  description: {description}
   115  permalink: {permalink}
   116  category: {category}
   117  subcategory: {subcategory}
   118  weight: {weight}
   119  editpath: {editpath}
   120  authors: {authors}
   121  layout: {layout}
   122  include_in_menu: {include_in_menu}"""
   123  
   124          for f in dep.files.to_list():
   125              # Is this a markdown file? If not, then we ensure that it ends up
   126              # in the same path as the permalink for relative addressing.
   127              if not f.basename.endswith(".md"):
   128                  builder_content.append("mkdir -p $T/%s" % doc.permalink)
   129                  builder_content.append("cp %s $T/%s" % (f.path, doc.permalink))
   130                  continue
   131  
   132              # Is this a post? If yes, then we must put this in the _posts
   133              # directory. This directory is treated specially with respect to
   134              # pagination and page generation.
   135              dest = f.short_path
   136              if doc.layout == "post":
   137                  dest = "_posts/" + f.basename
   138              builder_content.append("echo Processing %s... >&2" % f.short_path)
   139              builder_content.append("mkdir -p $T/$(dirname %s)" % dest)
   140  
   141              # Construct the header dynamically. We include the title field from
   142              # the markdown itself, as this is the g3doc format required. The
   143              # title will be injected by the web layout however, so we don't
   144              # want this to appear in the document.
   145              args = dict([(k, getattr(doc, k)) for k in dir(doc)])
   146              builder_content.append("title=\"$(grep -E '^# ' %s | head -n 1 | cut -d'#' -f2- || true)\"" % f.path)
   147              builder_content.append("cat >$T/%s <<EOF" % dest)
   148              builder_content.append("---")
   149              builder_content.append("title: $title")
   150              builder_content.append(header.format(**args))
   151              builder_content.append("---")
   152              builder_content.append("EOF")
   153  
   154              # To generate the final page, we need to strip out the title (which
   155              # was pulled above to generate the annotation in the frontmatter,
   156              # and substitute the [TOC] tag with the {% toc %} plugin tag. Note
   157              # that the pipeline here is almost important, as the grep will
   158              # return non-zero if the file is empty, but we ignore that within
   159              # the pipeline.
   160              builder_content.append("grep -v -E '^# ' %s | sed -e 's|^\\[TOC\\]$|- TOC\\n{:toc}|' >>$T/%s" %
   161                                     (f.path, dest))
   162  
   163      builder_content.append("declare -r filename=$(readlink -m %s)" % tarball.path)
   164      builder_content.append("(cd $T && tar -zcf \"${filename}\" .)\n")
   165      ctx.actions.write(builder, "\n".join(builder_content), is_executable = True)
   166  
   167      # Generate the tarball.
   168      ctx.actions.run(
   169          inputs = depset(ctx.files.deps),
   170          outputs = [tarball],
   171          progress_message = "Generating %s" % ctx.label,
   172          executable = builder,
   173          toolchain = None,
   174      )
   175      return [DefaultInfo(
   176          files = depset([tarball]),
   177      )]
   178  
   179  docs = rule(
   180      implementation = _docs_impl,
   181      doc = "Construct a site tarball from doc dependencies.",
   182      attrs = {
   183          "deps": attr.label_list(
   184              doc = "All document dependencies.",
   185          ),
   186      },
   187  )