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 )