github.com/SagerNet/gvisor@v0.0.0-20210707092255-7731c139d75c/tools/nogo/defs.bzl (about)

     1  """Nogo rules."""
     2  
     3  load("//tools/bazeldefs:go.bzl", "go_context", "go_embed_libraries", "go_importpath", "go_rule")
     4  
     5  NogoConfigInfo = provider(
     6      "information about a nogo configuration",
     7      fields = {
     8          "srcs": "the collection of configuration files",
     9      },
    10  )
    11  
    12  def _nogo_config_impl(ctx):
    13      return [NogoConfigInfo(
    14          srcs = ctx.files.srcs,
    15      )]
    16  
    17  nogo_config = rule(
    18      implementation = _nogo_config_impl,
    19      attrs = {
    20          "srcs": attr.label_list(
    21              doc = "a list of yaml files (schema defined by tool/nogo/config.go).",
    22              allow_files = True,
    23          ),
    24      },
    25  )
    26  
    27  NogoTargetInfo = provider(
    28      "information about the Go target",
    29      fields = {
    30          "goarch": "the build architecture (GOARCH)",
    31          "goos": "the build OS target (GOOS)",
    32          "worker_debug": "transitive debugging",
    33      },
    34  )
    35  
    36  def _nogo_target_impl(ctx):
    37      return [NogoTargetInfo(
    38          goarch = ctx.attr.goarch,
    39          goos = ctx.attr.goos,
    40          worker_debug = ctx.attr.worker_debug,
    41      )]
    42  
    43  nogo_target = go_rule(
    44      rule,
    45      implementation = _nogo_target_impl,
    46      attrs = {
    47          "goarch": attr.string(
    48              doc = "the Go build architecture (propagated to other rules).",
    49              mandatory = True,
    50          ),
    51          "goos": attr.string(
    52              doc = "the Go OS target (propagated to other rules).",
    53              mandatory = True,
    54          ),
    55          "worker_debug": attr.bool(
    56              doc = "whether worker debugging should be enabled.",
    57              default = False,
    58          ),
    59      },
    60  )
    61  
    62  def _nogo_objdump_tool_impl(ctx):
    63      # Construct the magic dump command.
    64      #
    65      # Note that in some cases, the input is being fed into the tool via stdin.
    66      # Unfortunately, the Go objdump tool expects to see a seekable file [1], so
    67      # we need the tool to handle this case by creating a temporary file.
    68      #
    69      # [1] https://github.com/golang/go/issues/41051
    70      nogo_target_info = ctx.attr._target[NogoTargetInfo]
    71      go_ctx = go_context(ctx, goos = nogo_target_info.goos, goarch = nogo_target_info.goarch)
    72      env_prefix = " ".join(["%s=%s" % (key, value) for (key, value) in go_ctx.env.items()])
    73      dumper = ctx.actions.declare_file(ctx.label.name)
    74      ctx.actions.write(dumper, "\n".join([
    75          "#!/bin/bash",
    76          "set -euo pipefail",
    77          "if [[ $# -eq 0 ]]; then",
    78          " T=$(mktemp -u -t libXXXXXX.a)",
    79          " cat /dev/stdin > ${T}",
    80          "else",
    81          " T=$1;",
    82          "fi",
    83          "%s %s tool objdump ${T}" % (
    84              env_prefix,
    85              go_ctx.go.path,
    86          ),
    87          "if [[ $# -eq 0 ]]; then",
    88          " rm -rf ${T}",
    89          "fi",
    90          "",
    91      ]), is_executable = True)
    92  
    93      # Include the full runfiles.
    94      return [DefaultInfo(
    95          runfiles = ctx.runfiles(files = go_ctx.runfiles.to_list()),
    96          executable = dumper,
    97      )]
    98  
    99  nogo_objdump_tool = go_rule(
   100      rule,
   101      implementation = _nogo_objdump_tool_impl,
   102      attrs = {
   103          "_target": attr.label(
   104              default = "//tools/nogo:target",
   105              cfg = "target",
   106          ),
   107      },
   108  )
   109  
   110  # NogoStdlibInfo is the set of standard library facts.
   111  NogoStdlibInfo = provider(
   112      "information for nogo analysis (standard library facts)",
   113      fields = {
   114          "facts": "serialized standard library facts",
   115          "raw_findings": "raw package findings (if relevant)",
   116      },
   117  )
   118  
   119  def _nogo_stdlib_impl(ctx):
   120      # Build the standard library facts.
   121      nogo_target_info = ctx.attr._target[NogoTargetInfo]
   122      go_ctx = go_context(ctx, goos = nogo_target_info.goos, goarch = nogo_target_info.goarch)
   123      facts = ctx.actions.declare_file(ctx.label.name + ".facts")
   124      raw_findings = ctx.actions.declare_file(ctx.label.name + ".raw_findings")
   125      config = struct(
   126          Srcs = [f.path for f in go_ctx.stdlib_srcs],
   127          GOOS = go_ctx.goos,
   128          GOARCH = go_ctx.goarch,
   129          Tags = go_ctx.gotags,
   130      )
   131      config_file = ctx.actions.declare_file(ctx.label.name + ".cfg")
   132      ctx.actions.write(config_file, config.to_json())
   133      args_file = ctx.actions.declare_file(ctx.label.name + "_args_file")
   134      ctx.actions.write(
   135          output = args_file,
   136          content = "\n".join(go_ctx.nogo_args + [
   137              "-objdump_tool=%s" % ctx.files._objdump_tool[0].path,
   138              "-stdlib=%s" % config_file.path,
   139              "-findings=%s" % raw_findings.path,
   140              "-facts=%s" % facts.path,
   141          ]),
   142      )
   143      ctx.actions.run(
   144          inputs = [config_file] + go_ctx.stdlib_srcs + [args_file],
   145          outputs = [facts, raw_findings],
   146          tools = depset(go_ctx.runfiles.to_list() + ctx.files._objdump_tool),
   147          executable = ctx.files._check[0],
   148          mnemonic = "GoStandardLibraryAnalysis",
   149          # Note that this does not support work execution currently. There is an
   150          # issue with stdout pollution that is not yet resolved, so this is kept
   151          # as a separate menomic.
   152          progress_message = "Analyzing Go Standard Library",
   153          arguments = [
   154              "--worker_debug=%s" % nogo_target_info.worker_debug,
   155              "@%s" % args_file.path,
   156          ],
   157      )
   158  
   159      # Return the stdlib facts as output.
   160      return [NogoStdlibInfo(
   161          facts = facts,
   162          raw_findings = raw_findings,
   163      )]
   164  
   165  nogo_stdlib = go_rule(
   166      rule,
   167      implementation = _nogo_stdlib_impl,
   168      attrs = {
   169          "_check": attr.label(
   170              default = "//tools/nogo/check:check",
   171              cfg = "host",
   172          ),
   173          "_objdump_tool": attr.label(
   174              default = "//tools/nogo:objdump_tool",
   175              cfg = "host",
   176          ),
   177          "_target": attr.label(
   178              default = "//tools/nogo:target",
   179              cfg = "target",
   180          ),
   181      },
   182  )
   183  
   184  # NogoInfo is the serialized set of package facts for a nogo analysis.
   185  #
   186  # Each go_library rule will generate a corresponding nogo rule, which will run
   187  # with the source files as input. Note however, that the individual nogo rules
   188  # are simply stubs that enter into the shadow dependency tree (the "aspect").
   189  NogoInfo = provider(
   190      "information for nogo analysis",
   191      fields = {
   192          "facts": "serialized package facts",
   193          "raw_findings": "raw package findings (if relevant)",
   194          "importpath": "package import path",
   195          "binaries": "package binary files",
   196          "srcs": "srcs (for go_test support)",
   197          "deps": "deps (for go_test support)",
   198      },
   199  )
   200  
   201  def _select_objfile(files):
   202      """Returns (.a file, .x file, is_archive).
   203  
   204      If no .a file is available, then the first .x file will be returned
   205      instead, and vice versa. If neither are available, then the first provided
   206      file will be returned."""
   207      a_files = [f for f in files if f.path.endswith(".a")]
   208      x_files = [f for f in files if f.path.endswith(".x")]
   209      if not len(x_files) and not len(a_files):
   210          return (files[0], files[0], False)
   211      if not len(x_files):
   212          x_files = a_files
   213      if not len(a_files):
   214          a_files = x_files
   215      return a_files[0], x_files[0], True
   216  
   217  def _nogo_aspect_impl(target, ctx):
   218      # If this is a nogo rule itself (and not the shadow of a go_library or
   219      # go_binary rule created by such a rule), then we simply return nothing.
   220      # All work is done in the shadow properties for go rules. For a proto
   221      # library, we simply skip the analysis portion but still need to return a
   222      # valid NogoInfo to reference the generated binary.
   223      #
   224      # Note that we almost exclusively use go_library, not go_tool_library.
   225      # This is because nogo is manually annotated, so the go_tool_library kind
   226      # is not needed to avoid dependency loops. Unfortunately, bazel coverdata
   227      # is exported *only* as a go_tool_library. This does not cause a problem,
   228      # since there is guaranteed to be no conflict. However for consistency,
   229      # we should not introduce new go_tool_library dependencies unless strictly
   230      # necessary.
   231      if ctx.rule.kind in ("go_library", "go_tool_library", "go_binary", "go_test"):
   232          srcs = ctx.rule.files.srcs
   233          deps = ctx.rule.attr.deps
   234      elif ctx.rule.kind in ("go_proto_library", "go_wrap_cc"):
   235          srcs = []
   236          deps = ctx.rule.attr.deps
   237      else:
   238          return [NogoInfo()]
   239  
   240      # If we're using the "library" attribute, then we need to aggregate the
   241      # original library sources and dependencies into this target to perform
   242      # proper type analysis.
   243      for embed in go_embed_libraries(ctx.rule):
   244          info = embed[NogoInfo]
   245          if hasattr(info, "srcs"):
   246              srcs = srcs + info.srcs
   247          if hasattr(info, "deps"):
   248              deps = deps + info.deps
   249  
   250      # Start with all target files and srcs as input.
   251      binaries = target.files.to_list()
   252      inputs = binaries + srcs
   253  
   254      # Generate a shell script that dumps the binary. Annoyingly, this seems
   255      # necessary as the context in which a run_shell command runs does not seem
   256      # to cleanly allow us redirect stdout to the actual output file. Perhaps
   257      # I'm missing something here, but the intermediate script does work.
   258      target_objfile, target_xfile, has_objfile = _select_objfile(binaries)
   259      inputs.append(target_objfile)
   260  
   261      # Extract the importpath for this package.
   262      if ctx.rule.kind == "go_test":
   263          # If this is a test, then it will not be imported by anything else.
   264          # We can safely set the importapth to just "test". Note that this
   265          # is necessary if the library also imports the core library (in
   266          # addition to including the sources directly), which happens in
   267          # some complex cases (seccomp_victim).
   268          importpath = "test"
   269      else:
   270          importpath = go_importpath(target)
   271  
   272      # Collect all info from shadow dependencies.
   273      fact_map = dict()
   274      import_map = dict()
   275      all_raw_findings = []
   276      for dep in deps:
   277          # There will be no file attribute set for all transitive dependencies
   278          # that are not go_library or go_binary rules, such as a proto rules.
   279          # This is handled by the ctx.rule.kind check above.
   280          info = dep[NogoInfo]
   281          if not hasattr(info, "facts"):
   282              continue
   283  
   284          # Configure where to find the binary & fact files. Note that this will
   285          # use .x and .a regardless of whether this is a go_binary rule, since
   286          # these dependencies must be go_library rules.
   287          _, x_file, _ = _select_objfile(info.binaries)
   288          import_map[info.importpath] = x_file.path
   289          fact_map[info.importpath] = info.facts.path
   290  
   291          # Collect all findings; duplicates are resolved at the end.
   292          all_raw_findings.extend(info.raw_findings)
   293  
   294          # Ensure the above are available as inputs.
   295          inputs.append(info.facts)
   296          inputs += info.binaries
   297  
   298      # Add the module itself, for the type sanity check. This applies only to
   299      # the libraries, and not binaries or tests.
   300      if has_objfile:
   301          import_map[importpath] = target_xfile.path
   302  
   303      # Add the standard library facts.
   304      stdlib_info = ctx.attr._nogo_stdlib[NogoStdlibInfo]
   305      stdlib_facts = stdlib_info.facts
   306      inputs.append(stdlib_facts)
   307  
   308      # The nogo tool operates on a configuration serialized in JSON format.
   309      nogo_target_info = ctx.attr._target[NogoTargetInfo]
   310      go_ctx = go_context(ctx, goos = nogo_target_info.goos, goarch = nogo_target_info.goarch)
   311      facts = ctx.actions.declare_file(target.label.name + ".facts")
   312      raw_findings = ctx.actions.declare_file(target.label.name + ".raw_findings")
   313      config = struct(
   314          ImportPath = importpath,
   315          GoFiles = [src.path for src in srcs if src.path.endswith(".go")],
   316          NonGoFiles = [src.path for src in srcs if not src.path.endswith(".go")],
   317          GOOS = go_ctx.goos,
   318          GOARCH = go_ctx.goarch,
   319          Tags = go_ctx.gotags,
   320          FactMap = fact_map,
   321          ImportMap = import_map,
   322          StdlibFacts = stdlib_facts.path,
   323      )
   324      config_file = ctx.actions.declare_file(target.label.name + ".cfg")
   325      ctx.actions.write(config_file, config.to_json())
   326      inputs.append(config_file)
   327      args_file = ctx.actions.declare_file(ctx.label.name + "_args_file")
   328      ctx.actions.write(
   329          output = args_file,
   330          content = "\n".join(go_ctx.nogo_args + [
   331              "-binary=%s" % target_objfile.path,
   332              "-objdump_tool=%s" % ctx.files._objdump_tool[0].path,
   333              "-package=%s" % config_file.path,
   334              "-findings=%s" % raw_findings.path,
   335              "-facts=%s" % facts.path,
   336          ]),
   337      )
   338      ctx.actions.run(
   339          inputs = inputs + [args_file],
   340          outputs = [facts, raw_findings],
   341          tools = depset(go_ctx.runfiles.to_list() + ctx.files._objdump_tool),
   342          executable = ctx.files._check[0],
   343          mnemonic = "GoStaticAnalysis",
   344          progress_message = "Analyzing %s" % target.label,
   345          execution_requirements = {"supports-workers": "1"},
   346          arguments = [
   347              "--worker_debug=%s" % nogo_target_info.worker_debug,
   348              "@%s" % args_file.path,
   349          ],
   350      )
   351  
   352      # Flatten all findings from all dependencies.
   353      #
   354      # This is done because all the filtering must be done at the
   355      # top-level nogo_test to dynamically apply a configuration.
   356      # This does not actually add any additional work here, but
   357      # will simply propagate the full list of files.
   358      all_raw_findings = [stdlib_info.raw_findings] + depset(all_raw_findings).to_list() + [raw_findings]
   359  
   360      # Return the package facts as output.
   361      return [
   362          NogoInfo(
   363              facts = facts,
   364              raw_findings = all_raw_findings,
   365              importpath = importpath,
   366              binaries = binaries,
   367              srcs = srcs,
   368              deps = deps,
   369          ),
   370      ]
   371  
   372  nogo_aspect = go_rule(
   373      aspect,
   374      implementation = _nogo_aspect_impl,
   375      attr_aspects = [
   376          "deps",
   377          "library",
   378          "embed",
   379      ],
   380      attrs = {
   381          "_check": attr.label(
   382              default = "//tools/nogo/check:check",
   383              cfg = "host",
   384          ),
   385          "_objdump_tool": attr.label(
   386              default = "//tools/nogo:objdump_tool",
   387              cfg = "host",
   388          ),
   389          "_target": attr.label(
   390              default = "//tools/nogo:target",
   391              cfg = "target",
   392          ),
   393          # The name of this attribute must not be _stdlib, since that
   394          # appears to be reserved for some internal bazel use.
   395          "_nogo_stdlib": attr.label(
   396              default = "//tools/nogo:stdlib",
   397              cfg = "host",
   398          ),
   399      },
   400  )
   401  
   402  def _nogo_test_impl(ctx):
   403      """Check nogo findings."""
   404      nogo_target_info = ctx.attr._target[NogoTargetInfo]
   405  
   406      # Ensure there's a single dependency.
   407      if len(ctx.attr.deps) != 1:
   408          fail("nogo_test requires exactly one dep.")
   409      raw_findings = ctx.attr.deps[0][NogoInfo].raw_findings
   410  
   411      # Build a step that applies the configuration.
   412      config_srcs = ctx.attr.config[NogoConfigInfo].srcs
   413      findings = ctx.actions.declare_file(ctx.label.name + ".findings")
   414      args_file = ctx.actions.declare_file(ctx.label.name + "_args_file")
   415      ctx.actions.write(
   416          output = args_file,
   417          content = "\n".join(
   418              ["-input=%s" % f.path for f in raw_findings] +
   419              ["-config=%s" % f.path for f in config_srcs] +
   420              ["-output=%s" % findings.path],
   421          ),
   422      )
   423      ctx.actions.run(
   424          inputs = raw_findings + ctx.files.srcs + config_srcs + [args_file],
   425          outputs = [findings],
   426          tools = depset(ctx.files._filter),
   427          executable = ctx.files._filter[0],
   428          mnemonic = "GoStaticAnalysis",
   429          progress_message = "Generating %s" % ctx.label,
   430          execution_requirements = {"supports-workers": "1"},
   431          arguments = [
   432              "--worker_debug=%s" % nogo_target_info.worker_debug,
   433              "@%s" % args_file.path,
   434          ],
   435      )
   436  
   437      # Build a runner that checks the filtered facts.
   438      #
   439      # Note that this calls the filter binary without any configuration, so all
   440      # findings will be included. But this is expected, since we've already
   441      # filtered out everything that should not be included.
   442      runner = ctx.actions.declare_file(ctx.label.name)
   443      runner_content = [
   444          "#!/bin/bash",
   445          "exec %s -check -input=%s" % (ctx.files._filter[0].short_path, findings.short_path),
   446          "",
   447      ]
   448      ctx.actions.write(runner, "\n".join(runner_content), is_executable = True)
   449  
   450      return [DefaultInfo(
   451          # The runner just executes the filter again, on the
   452          # newly generated filtered findings. We still need
   453          # the filter tool as part of our runfiles, however.
   454          runfiles = ctx.runfiles(files = ctx.files._filter + [findings]),
   455          executable = runner,
   456      ), OutputGroupInfo(
   457          # Propagate the filtered filters, for consumption by
   458          # build tooling. Note that the build tooling typically
   459          # pays attention to the mnemoic above, so this must be
   460          # what is expected by the tooling.
   461          nogo_findings = depset([findings]),
   462      )]
   463  
   464  nogo_test = rule(
   465      implementation = _nogo_test_impl,
   466      attrs = {
   467          "config": attr.label(
   468              mandatory = True,
   469              doc = "A rule of kind nogo_config.",
   470          ),
   471          "deps": attr.label_list(
   472              aspects = [nogo_aspect],
   473              doc = "Exactly one Go dependency to be analyzed.",
   474          ),
   475          "srcs": attr.label_list(
   476              allow_files = True,
   477              doc = "Relevant src files. This is ignored except to make the nogo_test directly affected by the files.",
   478          ),
   479          "_target": attr.label(
   480              default = "//tools/nogo:target",
   481              cfg = "target",
   482          ),
   483          "_filter": attr.label(default = "//tools/nogo/filter:filter"),
   484      },
   485      test = True,
   486  )
   487  
   488  def _nogo_aspect_tricorder_impl(target, ctx):
   489      if ctx.rule.kind != "nogo_test" or OutputGroupInfo not in target:
   490          return []
   491      if not hasattr(target[OutputGroupInfo], "nogo_findings"):
   492          return []
   493      return [
   494          OutputGroupInfo(tricorder = target[OutputGroupInfo].nogo_findings),
   495      ]
   496  
   497  # Trivial aspect that forwards the findings from a nogo_test rule to
   498  # go/tricorder, which reads from the `tricorder` output group.
   499  nogo_aspect_tricorder = aspect(
   500      implementation = _nogo_aspect_tricorder_impl,
   501  )