Skip to content

Commit

Permalink
Add Bzlmod support for nogo
Browse files Browse the repository at this point in the history
See `bzlmod.md` for the details. This change is backwards compatible for
WORKSPACE setups, but allows finer control over the set of targets nogo
is applied to when using Bzlmod.
  • Loading branch information
fmeum committed Dec 11, 2023
1 parent 43e644e commit 42a0fdd
Show file tree
Hide file tree
Showing 14 changed files with 242 additions and 39 deletions.
48 changes: 37 additions & 11 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,56 @@ module(
repo_name = "io_bazel_rules_go",
)

bazel_dep(name = "bazel_features", version = "1.1.1")
bazel_dep(name = "bazel_skylib", version = "1.2.0")
bazel_dep(name = "platforms", version = "0.0.4")
bazel_dep(name = "rules_proto", version = "4.0.0")
bazel_dep(name = "protobuf", version = "3.19.2", repo_name = "com_google_protobuf")
bazel_dep(
name = "bazel_features",
version = "1.1.1",
)

non_module_dependencies = use_extension("//go/private:extensions.bzl", "non_module_dependencies")
use_repo(
non_module_dependencies,
"io_bazel_rules_nogo",
bazel_dep(
name = "bazel_skylib",
version = "1.2.0",
)

bazel_dep(
name = "platforms",
version = "0.0.4",
)

bazel_dep(
name = "rules_proto",
version = "4.0.0",
)

bazel_dep(
name = "protobuf",
version = "3.19.2",
repo_name = "com_google_protobuf",
)

go_sdk = use_extension("//go:extensions.bzl", "go_sdk")

go_sdk.download(
name = "go_default_sdk",
version = "1.21.1",
)
use_repo(go_sdk, "go_toolchains")

use_repo(
go_sdk,
"go_toolchains",
"io_bazel_rules_nogo",
)

register_toolchains("@go_toolchains//:all")

bazel_dep(name = "gazelle", version = "0.34.0")
bazel_dep(
name = "gazelle",
version = "0.34.0",
)

go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")

go_deps.from_file(go_mod = "//:go.mod")

use_repo(
go_deps,
"com_github_gogo_protobuf",
Expand All @@ -39,4 +64,5 @@ use_repo(
"org_golang_google_grpc",
"org_golang_google_protobuf",
"org_golang_x_net",
"org_golang_x_tools",
)
32 changes: 30 additions & 2 deletions docs/go/core/bzlmod.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ bazel_dep(name = "rules_go", version = "0.39.1", repo_name = "io_bazel_rules_go"
bazel_dep(name = "gazelle", version = "0.31.0", repo_name = "bazel_gazelle")
```

## Registering Go SDKs
## Go SDKs

rules_go automatically downloads and registers a recent Go SDK, so unless a particular version is required, no manual steps are required.

Expand Down Expand Up @@ -67,11 +67,39 @@ If you really do need direct access to a Go SDK, you can provide the `name` attr
Note that modules using this attribute cannot be added to registries such as the Bazel Central Registry (BCR).
If you have a use case that would require this, please explain it in an issue.

### Configuring `nogo`

The `nogo` tool is a static analyzer for Go code that is run as part of compilation.
It is configured via an instance of the [`nogo`](/go/nogo.rst) rule, which can then be registered with the `go_sdk` extension:

```starlark
go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
go_sdk.nogo(nogo = "//:my_nogo")
```

By default, the `nogo` tool is executed for all Go targets in the main repository, but not any external repositories.
Each module can only provide at most one `go_sdk.nogo` tag and only the tag of the root module is honored.

It is also possible to include only or exclude particular packages from `nogo` analysis, using syntax that matches the `visibility` attribute on rules:

```starlark
go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
go_sdk.nogo(
nogo = "//:my_nogo",
includes = [
"//:__subpackages__",
"@my_own_go_dep//logic:__pkg__",
],
excludes = [
"//third_party:__subpackages__",
],
)
```

### Not yet supported

* `go_local_sdk`
* `go_wrap_sdk`
* nogo ([#3529](https://github.com/bazelbuild/rules_go/issues/3529))

## Generating BUILD files

Expand Down
12 changes: 7 additions & 5 deletions go/def.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,7 @@ load(
_go_cross_binary = "go_cross_binary",
)

# TOOLS_NOGO is a list of all analysis passes in
# golang.org/x/tools/go/analysis/passes.
# This is not backward compatible, so use caution when depending on this --
# new analyses may discover issues in existing builds.
TOOLS_NOGO = [
_TOOLS_NOGO = [
"@org_golang_x_tools//go/analysis/passes/asmdecl:go_default_library",
"@org_golang_x_tools//go/analysis/passes/assign:go_default_library",
"@org_golang_x_tools//go/analysis/passes/atomic:go_default_library",
Expand Down Expand Up @@ -117,6 +113,12 @@ TOOLS_NOGO = [
"@org_golang_x_tools//go/analysis/passes/unusedresult:go_default_library",
]

# TOOLS_NOGO is a list of all analysis passes in
# golang.org/x/tools/go/analysis/passes.
# This is not backward compatible, so use caution when depending on this --
# new analyses may discover issues in existing builds.
TOOLS_NOGO = [str(Label(l)) for l in _TOOLS_NOGO]

# Current version or next version to be tagged. Gazelle and other tools may
# check this to determine compatibility.
RULES_GO_VERSION = "0.43.0"
Expand Down
1 change: 1 addition & 0 deletions go/private/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ bzl_library(
"@bazel_skylib//rules:common_settings",
"@bazel_tools//tools/build_defs/cc:action_names.bzl",
"@bazel_tools//tools/cpp:toolchain_utils.bzl",
"@io_bazel_rules_nogo//:scope.bzl",
],
)

Expand Down
2 changes: 2 additions & 0 deletions go/private/BUILD.nogo.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ alias(
name = "nogo",
actual = "{{nogo}}",
)

exports_files(["scope.bzl"])
7 changes: 4 additions & 3 deletions go/private/actions/compilepkg.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,10 @@ def emit_compilepkg(

args.add("-o", out_lib)
args.add("-x", out_export)
if go.nogo:
args.add("-nogo", go.nogo)
inputs.append(go.nogo)
nogo = go.get_nogo(go)
if nogo:
args.add("-nogo", nogo)
inputs.append(nogo)
if out_cgo_export_h:
args.add("-cgoexport", out_cgo_export_h)
outputs.append(out_cgo_export_h)
Expand Down
30 changes: 30 additions & 0 deletions go/private/context.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ load(
"//go/private/rules:transition.bzl",
"request_nogo_transition",
)
load(
"@io_bazel_rules_nogo//:scope.bzl",
NOGO_EXCLUDES = "EXCLUDES",
NOGO_INCLUDES = "INCLUDES",
)

# cgo requires a gcc/clang style compiler.
# We use a denylist instead of an allowlist:
Expand Down Expand Up @@ -392,6 +397,30 @@ def _infer_importpath(ctx, attr):
importpath = importpath[1:]
return importpath, importpath, INFERRED_PATH

def _matches_scope(label, scope):
if scope == "all":
return True
if scope.workspace_name != label.workspace_name:
return False
if scope.name == "__pkg__":
return scope.package == label.package
if scope.name == "__subpackages__":
if not scope.package:
return True
return scope.package == label.package or label.package.startswith(scope.package + "/")
fail("invalid scope '%s'" % scope.name)

def _matches_scopes(label, scopes):
return any([_matches_scope(label, scope) for scope in scopes])

def _get_nogo(go):
"""Returns the nogo file for this target, if enabled and in scope."""
label = go._ctx.label
if _matches_scopes(label, NOGO_INCLUDES) and not _matches_scopes(label, NOGO_EXCLUDES):
return go.nogo
else:
return None

def go_context(ctx, attr = None):
"""Returns an API used to build Go code.
Expand Down Expand Up @@ -551,6 +580,7 @@ def go_context(ctx, attr = None):
library_to_source = _library_to_source,
declare_file = _declare_file,
declare_directory = _declare_directory,
get_nogo = _get_nogo,

# Private
# TODO: All uses of this should be removed
Expand Down
67 changes: 60 additions & 7 deletions go/private/extensions.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
load("@bazel_features//:features.bzl", "bazel_features")
load("//go/private:sdk.bzl", "detect_host_platform", "go_download_sdk_rule", "go_host_sdk_rule", "go_multiple_toolchains")
load("//go/private:repositories.bzl", "go_rules_dependencies")
load("//go/private:nogo.bzl", "DEFAULT_NOGO", "go_register_nogo")

def host_compatible_toolchain_impl(ctx):
ctx.file("BUILD.bazel")
Expand Down Expand Up @@ -67,12 +68,70 @@ _host_tag = tag_class(
},
)

_NOGO_DEFAULT_INCLUDES = ["@@//:__subpackages__"]
_NOGO_DEFAULT_EXCLUDES = []

_nogo_tag = tag_class(
attrs = {
"nogo": attr.label(
doc = "The nogo target to use when this module is the root module.",
),
"includes": attr.label_list(
default = _NOGO_DEFAULT_INCLUDES,
doc = """
A Go target is checked with nogo if its package matches at least one of the entries in 'includes'
and none of the entries in 'excludes'. By default, nogo is applied to all targets in the main
repository.
Uses the same format as 'visibility', i.e., every entry must be a label that ends with ':__pkg__' or
':__subpackages__'.
""",
),
"excludes": attr.label_list(
default = _NOGO_DEFAULT_EXCLUDES,
doc = "See 'includes'.",
),
},
)

# This limit can be increased essentially arbitrarily, but doing so will cause a rebuild of all
# targets using any of these toolchains due to the changed repository name.
_MAX_NUM_TOOLCHAINS = 9999
_TOOLCHAIN_INDEX_PAD_LENGTH = len(str(_MAX_NUM_TOOLCHAINS))

def _go_sdk_impl(ctx):
nogo_tag = struct(
nogo = DEFAULT_NOGO,
includes = _NOGO_DEFAULT_INCLUDES,
excludes = _NOGO_DEFAULT_EXCLUDES,
)
for module in ctx.modules:
if not module.is_root or not module.tags.nogo:
continue
if len(module.tags.nogo) > 1:
# Make use of the special formatting applied to tags by fail.
fail(
"go_sdk.nogo: only one tag can be specified per module, got:\n",
*[t for p in zip(module.tags.nogo, len(module.tags.nogo) * ["\n"]) for t in p]
)
nogo_tag = module.tags.nogo[0]
for scope in nogo_tag.includes + nogo_tag.excludes:
# Validate that the scope references a valid, visible repository.
_ = scope.workspace_name
if scope.name != "__pkg__" and scope.name != "__subpackages__":
fail(
"go_sdk.nogo: all entries in includes and excludes must end with ':__pkg__' or ':__subpackages__', got '{}' in".format(scope.name),
nogo_tag,
)
go_register_nogo(
name = "io_bazel_rules_nogo",
nogo = str(nogo_tag.nogo),
# Go through canonical label literals to avoid a dependency edge on the packages in the
# scope.
includes = [str(l) for l in nogo_tag.includes],
excludes = [str(l) for l in nogo_tag.excludes],
)

multi_version_module = {}
for module in ctx.modules:
if module.name in multi_version_module:
Expand Down Expand Up @@ -220,13 +279,7 @@ go_sdk = module_extension(
tag_classes = {
"download": _download_tag,
"host": _host_tag,
"nogo": _nogo_tag,
},
**go_sdk_extra_kwargs
)

def _non_module_dependencies_impl(_ctx):
go_rules_dependencies(force = True)

non_module_dependencies = module_extension(
implementation = _non_module_dependencies_impl,
)
25 changes: 25 additions & 0 deletions go/private/nogo.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@

DEFAULT_NOGO = "@io_bazel_rules_go//:default_nogo"

# repr(Label(...)) does not emit a canonical label literal.
def _label_repr(label):
return "Label(\"{}\")".format(label)

def _scope_list_repr(scopes):
if scopes == ["all"]:
return repr(["all"])
return "[" + ", ".join([_label_repr(Label(l)) for l in scopes]) + "]"

def _go_register_nogo_impl(ctx):
ctx.template(
"BUILD.bazel",
Expand All @@ -23,14 +32,30 @@ def _go_register_nogo_impl(ctx):
},
executable = False,
)
ctx.file(
"scope.bzl",
"""
INCLUDES = {includes}
EXCLUDES = {excludes}
""".format(
includes = _scope_list_repr(ctx.attr.includes),
excludes = _scope_list_repr(ctx.attr.excludes),
),
executable = False,
)

# go_register_nogo creates a repository with an alias that points
# to the nogo rule that should be used globally by go rules in the workspace.
# This may be called automatically by go_rules_dependencies or by
# go_register_toolchains.
# With Bzlmod, it is created by the go_sdk extension.
go_register_nogo = repository_rule(
_go_register_nogo_impl,
attrs = {
"nogo": attr.string(mandatory = True),
# Special sentinel value used to let nogo run on all targets when using
# WORKSPACE, for backwards compatibility.
"includes": attr.string_list(default = ["all"]),
"excludes": attr.string_list(),
},
)
12 changes: 6 additions & 6 deletions go/private/rules/nogo.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def nogo(name, visibility = None, **kwargs):
native.alias(
name = name,
actual = select({
"@io_bazel_rules_go//go/private:nogo_active": actual_name,
str(Label("//go/private:nogo_active")): actual_name,
"//conditions:default": Label("//:default_nogo"),
}),
visibility = visibility,
Expand All @@ -140,11 +140,11 @@ def nogo(name, visibility = None, **kwargs):
def nogo_wrapper(**kwargs):
if kwargs.get("vet"):
kwargs["deps"] = kwargs.get("deps", []) + [
"@org_golang_x_tools//go/analysis/passes/atomic:go_default_library",
"@org_golang_x_tools//go/analysis/passes/bools:go_default_library",
"@org_golang_x_tools//go/analysis/passes/buildtag:go_default_library",
"@org_golang_x_tools//go/analysis/passes/nilfunc:go_default_library",
"@org_golang_x_tools//go/analysis/passes/printf:go_default_library",
Label("@org_golang_x_tools//go/analysis/passes/atomic:go_default_library"),
Label("@org_golang_x_tools//go/analysis/passes/bools:go_default_library"),
Label("@org_golang_x_tools//go/analysis/passes/buildtag:go_default_library"),
Label("@org_golang_x_tools//go/analysis/passes/nilfunc:go_default_library"),
Label("@org_golang_x_tools//go/analysis/passes/printf:go_default_library"),
]
kwargs = {k: v for k, v in kwargs.items() if k != "vet"}
nogo(**kwargs)
Loading

0 comments on commit 42a0fdd

Please sign in to comment.