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 )