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

     1  """Nogo rules."""
     2  
     3  load("//tools:arch.bzl", "arch_transition", "transition_allowlist")
     4  load("//tools/bazeldefs:go.bzl", "go_context", "go_embed_libraries", "go_importpath", "go_rule")
     5  
     6  NogoConfigInfo = provider(
     7      "information about a nogo configuration",
     8      fields = {
     9          "srcs": "the collection of configuration files",
    10      },
    11  )
    12  
    13  def _nogo_config_impl(ctx):
    14      return [NogoConfigInfo(
    15          srcs = ctx.files.srcs,
    16      )]
    17  
    18  nogo_config = rule(
    19      implementation = _nogo_config_impl,
    20      attrs = {
    21          "srcs": attr.label_list(
    22              doc = "a list of yaml files (schema defined by tool/nogo/config.go).",
    23              allow_files = True,
    24          ),
    25      },
    26  )
    27  
    28  NogoTargetInfo = provider(
    29      "information about the Go target",
    30      fields = {
    31          "goarch": "the build architecture (GOARCH)",
    32          "goos": "the build OS target (GOOS)",
    33      },
    34  )
    35  
    36  def _nogo_target_impl(ctx):
    37      return [NogoTargetInfo(
    38          goarch = ctx.attr.goarch,
    39          goos = ctx.attr.goos,
    40      )]
    41  
    42  nogo_target = go_rule(
    43      rule,
    44      implementation = _nogo_target_impl,
    45      attrs = {
    46          "goarch": attr.string(
    47              doc = "the Go build architecture (propagated to other rules).",
    48              mandatory = True,
    49          ),
    50          "goos": attr.string(
    51              doc = "the Go OS target (propagated to other rules).",
    52              mandatory = True,
    53          ),
    54      },
    55  )
    56  
    57  # NogoStdlibInfo is the set of standard library facts.
    58  NogoStdlibInfo = provider(
    59      "information for nogo analysis (standard library facts)",
    60      fields = {
    61          "facts": "serialized standard library facts",
    62          "raw_findings": "raw package findings (if relevant)",
    63      },
    64  )
    65  
    66  def _nogo_stdlib_impl(ctx):
    67      # Build the configuration for the stdlib.
    68      go_ctx, args, inputs, raw_findings = _nogo_config(ctx, deps = [])
    69  
    70      # Build the analyzer command.
    71      facts_file = ctx.actions.declare_file(ctx.label.name + ".facts")
    72      findings_file = ctx.actions.declare_file(ctx.label.name + ".raw_findings")
    73      ctx.actions.run(
    74          # For the standard library, we need to include the full set of Go
    75          # sources in the inputs.
    76          inputs = inputs + go_ctx.stdlib_srcs,
    77          outputs = [facts_file, findings_file],
    78          tools = depset(go_ctx.runfiles.to_list() + ctx.files._nogo),
    79          executable = ctx.files._nogo[0],
    80          env = go_ctx.env,
    81          mnemonic = "GoStandardLibraryAnalysis",
    82          progress_message = "Analyzing Go Standard Library",
    83          # Since these actions are generally I/O bound, reading source files,
    84          # facts, binaries and serializing results, disable sandboxing. This can
    85          # be enabled without any issues for correctness, but we want to avoid
    86          # paying the FUSE penalty.
    87          execution_requirements = {"no-sandbox": "1"},
    88          arguments = args + [
    89              "bundle",
    90              "-findings=%s" % findings_file.path,
    91              "-facts=%s" % facts_file.path,
    92              "-root=.*?/src/",
    93          ] + [f.path for f in go_ctx.stdlib_srcs],
    94          toolchain = None,
    95      )
    96  
    97      # Return the stdlib facts as output.
    98      return [NogoStdlibInfo(
    99          facts = facts_file,
   100          raw_findings = raw_findings + [findings_file],
   101      ), DefaultInfo(
   102          # Declare the facts and findings as default outputs. This is not
   103          # strictly required, but ensures that the target still perform analysis
   104          # when built directly rather than just indirectly via a nogo_test.
   105          files = depset([facts_file, findings_file]),
   106      )]
   107  
   108  nogo_stdlib = go_rule(
   109      rule,
   110      implementation = _nogo_stdlib_impl,
   111      attrs = {
   112          "_nogo": attr.label(
   113              default = "//tools/nogo:nogo",
   114              cfg = "exec",
   115          ),
   116          "_target": attr.label(
   117              default = "//tools/nogo:target",
   118              cfg = "target",
   119          ),
   120      },
   121  )
   122  
   123  # NogoInfo is the serialized set of package facts for a nogo analysis.
   124  #
   125  # Each go_library rule will generate a corresponding nogo rule, which will run
   126  # with the source files as input. Note however, that the individual nogo rules
   127  # are simply stubs that enter into the shadow dependency tree (the "aspect").
   128  NogoInfo = provider(
   129      "information for nogo analysis",
   130      fields = {
   131          "facts": "serialized package facts",
   132          "raw_findings": "raw package findings (if relevant)",
   133          "importpath": "package import path",
   134          "binaries": "package binary files",
   135          "srcs": "srcs (for go_test support)",
   136          "deps": "deps (for go_test support)",
   137      },
   138  )
   139  
   140  def _select_objfile(files):
   141      """Returns (.a file, .x file).
   142  
   143      If no .x file is available, then the first .x file will be returned
   144      instead, and vice versa. If neither are available, then the first provided
   145      file will be returned."""
   146      a_files = [f for f in files if f.path.endswith(".a")]
   147      x_files = [f for f in files if f.path.endswith(".x")]
   148      if not len(x_files) and not len(a_files):
   149          if not len(files):
   150              return (None, None)
   151          return (files[0], files[0])
   152      if not len(x_files):
   153          x_files = a_files
   154      if not len(a_files):
   155          a_files = x_files
   156      return a_files[0], x_files[0]
   157  
   158  def _nogo_config(ctx, deps):
   159      # Build a configuration for the given set of deps. This is most basic
   160      # configuration and is used by the stdlib. For a more complete config, the
   161      # _nogo_package_config function may be used.
   162      #
   163      # Returns (go_ctx, args, inputs, raw_findings).
   164      nogo_target_info = ctx.attr._target[NogoTargetInfo]
   165      go_ctx = go_context(ctx, goos = nogo_target_info.goos, goarch = nogo_target_info.goarch)
   166      args = go_ctx.nogo_args + [
   167          "-go=%s" % go_ctx.go.path,
   168          "-GOOS=%s" % go_ctx.goos,
   169          "-GOARCH=%s" % go_ctx.goarch,
   170          "-tags=%s" % (",".join(go_ctx.gotags)),
   171      ]
   172      inputs = []
   173      raw_findings = []
   174      for dep in deps:
   175          # There will be no file attribute set for all transitive dependencies
   176          # that are not go_library or go_binary rules, such as a proto rules.
   177          # This is handled by the ctx.rule.kind check above.
   178          info = dep[NogoInfo]
   179          if not hasattr(info, "facts"):
   180              continue
   181  
   182          # Configure where to find the binary & fact files. Note that this will
   183          # use .x and .a regardless of whether this is a go_binary rule, since
   184          # these dependencies must be go_library rules.
   185          a_file, x_file = _select_objfile(info.binaries)
   186          args.append("-archive=%s=%s" % (info.importpath, a_file.path))
   187          args.append("-import=%s=%s" % (info.importpath, x_file.path))
   188          args.append("-facts=%s=%s" % (info.importpath, info.facts.path))
   189  
   190          # Collect all findings; duplicates are resolved at the end.
   191          raw_findings.extend(info.raw_findings)
   192  
   193          # Ensure the above are available as inputs.
   194          inputs.append(a_file)
   195          inputs.append(x_file)
   196          inputs.append(info.facts)
   197  
   198      return (go_ctx, args, inputs, raw_findings)
   199  
   200  def _nogo_package_config(ctx, deps, importpath = None, target = None):
   201      # See _nogo_config. This includes package details.
   202      #
   203      # Returns (go_ctx, args, inputs, raw_findings).
   204      go_ctx, args, inputs, raw_findings = _nogo_config(ctx, deps)
   205  
   206      # Add the module itself, for the type sanity check. This applies only to
   207      # the libraries, and not binaries or tests.
   208      binaries = []
   209      if target != None:
   210          binaries.extend(target.files.to_list())
   211      target_afile, target_xfile = _select_objfile(binaries)
   212      if target_xfile != None:
   213          args.append("-archive=%s=%s" % (importpath, target_afile.path))
   214          args.append("-import=%s=%s" % (importpath, target_xfile.path))
   215          inputs.append(target_afile)
   216          inputs.append(target_xfile)
   217  
   218      # Add the standard library facts.
   219      stdlib_info = ctx.attr._nogo_stdlib[NogoStdlibInfo]
   220      stdlib_facts = stdlib_info.facts
   221      if stdlib_facts:
   222          inputs.append(stdlib_facts)
   223          args.append("-bundle=%s" % stdlib_facts.path)
   224  
   225      # Flatten all findings from all dependencies.
   226      #
   227      # This is done because all the filtering must be done at the
   228      # top-level nogo_test to dynamically apply a configuration.
   229      # This does not actually add any additional work here, but
   230      # will simply propagate the full list of files.
   231      raw_findings = stdlib_info.raw_findings + depset(raw_findings).to_list()
   232      return go_ctx, args, inputs, raw_findings
   233  
   234  def _nogo_aspect_impl(target, ctx):
   235      # If this is a nogo rule itself (and not the shadow of a go_library or
   236      # go_binary rule created by such a rule), then we simply return nothing.
   237      # All work is done in the shadow properties for go rules. For a proto
   238      # library, we simply skip the analysis portion but still need to return a
   239      # valid NogoInfo to reference the generated binary.
   240      #
   241      # Note that we almost exclusively use go_library, not go_tool_library.
   242      # This is because nogo is manually annotated, so the go_tool_library kind
   243      # is not needed to avoid dependency loops. Unfortunately, bazel coverdata
   244      # is exported *only* as a go_tool_library. This does not cause a problem,
   245      # since there is guaranteed to be no conflict. However for consistency,
   246      # we should not introduce new go_tool_library dependencies unless strictly
   247      # necessary.
   248      if ctx.rule.kind in ("go_library", "go_tool_library", "go_binary", "go_test"):
   249          srcs = ctx.rule.files.srcs
   250          deps = ctx.rule.attr.deps
   251      elif ctx.rule.kind in ("go_proto_library", "go_wrap_cc"):
   252          srcs = []
   253          deps = ctx.rule.attr.deps
   254      else:
   255          return [NogoInfo()]
   256  
   257      # If we're using the "library" attribute, then we need to aggregate the
   258      # original library sources and dependencies into this target to perform
   259      # proper type analysis.
   260      for embed in go_embed_libraries(ctx.rule):
   261          info = embed[NogoInfo]
   262          if hasattr(info, "srcs"):
   263              srcs = srcs + info.srcs
   264          if hasattr(info, "deps"):
   265              deps = deps + info.deps
   266  
   267      # Extract the importpath for this package.
   268      if ctx.rule.kind == "go_test":
   269          importpath = "test"
   270      else:
   271          importpath = go_importpath(target)
   272  
   273      # Build a complete configuration, referring to the library rule.
   274      go_ctx, args, inputs, raw_findings = _nogo_package_config(ctx, deps, importpath = importpath, target = target)
   275  
   276      # Build the argument file, and the runner.
   277      facts_file = ctx.actions.declare_file(ctx.label.name + ".facts")
   278      findings_file = ctx.actions.declare_file(ctx.label.name + ".findings")
   279      ctx.actions.run(
   280          inputs = inputs + srcs,
   281          outputs = [findings_file, facts_file],
   282          tools = depset(go_ctx.runfiles.to_list() + ctx.files._nogo),
   283          executable = ctx.files._nogo[0],
   284          env = go_ctx.env,
   285          mnemonic = "GoStaticAnalysis",
   286          progress_message = "Analyzing %s" % target.label,
   287          # See above.
   288          execution_requirements = {"no-sandbox": "1"},
   289          arguments = args + [
   290              "check",
   291              "-findings=%s" % findings_file.path,
   292              "-facts=%s" % facts_file.path,
   293              "-package=%s" % importpath,
   294          ] + [src.path for src in srcs],
   295          toolchain = None,
   296      )
   297  
   298      # Return the package facts as output.
   299      return [
   300          NogoInfo(
   301              facts = facts_file,
   302              raw_findings = raw_findings + [findings_file],
   303              importpath = importpath,
   304              binaries = target.files.to_list(),
   305              srcs = srcs,
   306              deps = deps,
   307          ),
   308      ]
   309  
   310  nogo_aspect = go_rule(
   311      aspect,
   312      implementation = _nogo_aspect_impl,
   313      attr_aspects = [
   314          "deps",
   315          "library",
   316          "embed",
   317      ],
   318      attrs = {
   319          "_nogo": attr.label(
   320              default = "//tools/nogo:nogo",
   321              cfg = "exec",
   322          ),
   323          "_target": attr.label(
   324              default = "//tools/nogo:target",
   325              cfg = "target",
   326          ),
   327          # The name of this attribute must not be _stdlib, since that
   328          # appears to be reserved for some internal bazel use.
   329          "_nogo_stdlib": attr.label(
   330              default = "//tools/nogo:stdlib",
   331              cfg = "target",
   332          ),
   333      },
   334  )
   335  
   336  def _nogo_test_impl(ctx):
   337      """Check nogo findings."""
   338  
   339      # Build a runner that checks the filtered facts.
   340      runner = ctx.actions.declare_file(ctx.label.name)
   341      runner_content = ["#!/bin/bash"]
   342      runner_footer = list()
   343      all_findings = list()
   344  
   345      # Collect all architecture-targets.
   346      for (arch, deps) in ctx.split_attr.deps.items():
   347          # Ensure there's a single dependency.
   348          if len(deps) != 1:
   349              fail("nogo_test requires exactly one dep.")
   350          raw_findings = deps[0][NogoInfo].raw_findings
   351  
   352          # Build a step that applies the configuration.
   353          config_srcs = ctx.attr.config[NogoConfigInfo].srcs
   354          findings = ctx.actions.declare_file(ctx.label.name + "." + arch + ".findings")
   355          ctx.actions.run(
   356              inputs = raw_findings + ctx.files.srcs + config_srcs,
   357              outputs = [findings],
   358              tools = depset(ctx.files._nogo),
   359              executable = ctx.files._nogo[0],
   360              mnemonic = "GoStaticAnalysis",
   361              progress_message = "Generating %s" % ctx.label,
   362              # See above.
   363              execution_requirements = {"no-sandbox": "1"},
   364              arguments = ["filter"] +
   365                          ["-config=%s" % f.path for f in config_srcs] +
   366                          ["-output=%s" % findings.path] +
   367                          [f.path for f in raw_findings],
   368              toolchain = None,
   369          )
   370  
   371          # Note that this calls the filter binary without any configuration, so
   372          # all findings will be included. But this is expected, since we've
   373          # already filtered out everything that should not be included. The
   374          # runner will always run all tests, and then exit if any have failed.
   375          runner_content.append("echo -n %s..." % arch)
   376          runner_content.append("%s filter -test -text %s" % (ctx.files._nogo[0].short_path, findings.short_path))
   377          runner_content.append("rc_%s=$?" % arch)
   378          runner_footer.append("if [[ $rc_%s -ne 0 ]]; then exit $rc_%s; fi" % (arch, arch))
   379          all_findings.append(findings)
   380      runner_content.extend(runner_footer)
   381      runner_content.append("")  # Ensure empty line.
   382      ctx.actions.write(runner, "\n".join(runner_content), is_executable = True)
   383      return [DefaultInfo(
   384          # The runner just executes the filter again, on the
   385          # newly generated filtered findings. We still need
   386          # the filter tool as part of our runfiles, however.
   387          runfiles = ctx.runfiles(files = ctx.files._nogo + all_findings),
   388          executable = runner,
   389      ), OutputGroupInfo(
   390          # Propagate the filtered filters, for consumption by
   391          # build tooling. Note that the build tooling typically
   392          # pays attention to the mnemoic above, so this must be
   393          # what is expected by the tooling.
   394          nogo_findings = depset(all_findings),
   395      )]
   396  
   397  nogo_test = rule(
   398      implementation = _nogo_test_impl,
   399      attrs = {
   400          "config": attr.label(
   401              mandatory = True,
   402              doc = "A rule of kind nogo_config.",
   403          ),
   404          "deps": attr.label_list(
   405              aspects = [nogo_aspect],
   406              doc = "Exactly one Go dependency to be analyzed.",
   407              cfg = arch_transition,
   408          ),
   409          "srcs": attr.label_list(
   410              allow_files = True,
   411              doc = "Relevant src files. This is ignored except to make the nogo_test directly affected by the files.",
   412          ),
   413          "_nogo": attr.label(
   414              default = "//tools/nogo:nogo",
   415              cfg = "exec",
   416          ),
   417          "_target": attr.label(
   418              default = "//tools/nogo:target",
   419              cfg = arch_transition,
   420          ),
   421          "_allowlist_function_transition": attr.label(
   422              default = transition_allowlist,
   423          ),
   424      },
   425      test = True,
   426  )
   427  
   428  def _nogo_aspect_tricorder_impl(target, ctx):
   429      if ctx.rule.kind != "nogo_test" or OutputGroupInfo not in target:
   430          return []
   431      if not hasattr(target[OutputGroupInfo], "nogo_findings"):
   432          return []
   433      return [
   434          OutputGroupInfo(tricorder = target[OutputGroupInfo].nogo_findings),
   435      ]
   436  
   437  # Trivial aspect that forwards the findings from a nogo_test rule to
   438  # go/tricorder, which reads from the `tricorder` output group.
   439  nogo_aspect_tricorder = aspect(
   440      implementation = _nogo_aspect_tricorder_impl,
   441  )