1# Copyright 2023 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15load("@io_bazel_rules_go_bazel_features//:features.bzl", "bazel_features")
16load("//go/private:sdk.bzl", "detect_host_platform", "go_download_sdk_rule", "go_host_sdk_rule", "go_multiple_toolchains")
17load("//go/private:nogo.bzl", "DEFAULT_NOGO", "NOGO_DEFAULT_EXCLUDES", "NOGO_DEFAULT_INCLUDES", "go_register_nogo")
18
19def host_compatible_toolchain_impl(ctx):
20 ctx.file("BUILD.bazel")
21 ctx.file("defs.bzl", content = """
22HOST_COMPATIBLE_SDK = Label({})
23""".format(repr(ctx.attr.toolchain)))
24
25host_compatible_toolchain = repository_rule(
26 implementation = host_compatible_toolchain_impl,
27 attrs = {
28 # We cannot use attr.label for the `toolchain` attribute since the module extension cannot
29 # refer to the repositories it creates by their apparent repository names.
30 "toolchain": attr.string(
31 doc = "The apparent label of a `ROOT` file in the repository of a host compatible toolchain created by the `go_sdk` extension",
32 mandatory = True,
33 ),
34 },
35 doc = "An external repository to expose the first host compatible toolchain",
36)
37
38_download_tag = tag_class(
39 attrs = {
40 "name": attr.string(),
41 "goos": attr.string(),
42 "goarch": attr.string(),
43 "sdks": attr.string_list_dict(),
44 "experiments": attr.string_list(
45 doc = "Go experiments to enable via GOEXPERIMENT",
46 ),
47 "urls": attr.string_list(default = ["https://dl.google.com/go/{}"]),
48 "version": attr.string(),
49 "patches": attr.label_list(
50 doc = "A list of patches to apply to the SDK after downloading it",
51 ),
52 "patch_strip": attr.int(
53 default = 0,
54 doc = "The number of leading path segments to be stripped from the file name in the patches.",
55 ),
56 "strip_prefix": attr.string(default = "go"),
57 },
58)
59
60_host_tag = tag_class(
61 attrs = {
62 "name": attr.string(),
63 "version": attr.string(),
64 "experiments": attr.string_list(
65 doc = "Go experiments to enable via GOEXPERIMENT",
66 ),
67 },
68)
69
70_nogo_tag = tag_class(
71 attrs = {
72 "nogo": attr.label(
73 doc = "The nogo target to use when this module is the root module.",
74 ),
75 "includes": attr.label_list(
76 default = NOGO_DEFAULT_INCLUDES,
77 # The special include "all" is undocumented on purpose: With it, adding a new transitive
78 # dependency to a Go module can cause a build failure if the new dependency has lint
79 # issues.
80 doc = """
81A Go target is checked with nogo if its package matches at least one of the entries in 'includes'
82and none of the entries in 'excludes'. By default, nogo is applied to all targets in the main
83repository.
84
85Uses the same format as 'visibility', i.e., every entry must be a label that ends with ':__pkg__' or
86':__subpackages__'.
87""",
88 ),
89 "excludes": attr.label_list(
90 default = NOGO_DEFAULT_EXCLUDES,
91 doc = "See 'includes'.",
92 ),
93 },
94)
95
96# A list of (goos, goarch) pairs that are commonly used for remote executors in cross-platform
97# builds (where host != exec platform). By default, we register toolchains for all of these
98# platforms in addition to the host platform.
99_COMMON_EXEC_PLATFORMS = [
100 ("darwin", "amd64"),
101 ("darwin", "arm64"),
102 ("linux", "amd64"),
103 ("linux", "arm64"),
104 ("windows", "amd64"),
105 ("windows", "arm64"),
106]
107
108# This limit can be increased essentially arbitrarily, but doing so will cause a rebuild of all
109# targets using any of these toolchains due to the changed repository name.
110_MAX_NUM_TOOLCHAINS = 9999
111_TOOLCHAIN_INDEX_PAD_LENGTH = len(str(_MAX_NUM_TOOLCHAINS))
112
113def _go_sdk_impl(ctx):
114 nogo_tag = struct(
115 nogo = DEFAULT_NOGO,
116 includes = NOGO_DEFAULT_INCLUDES,
117 excludes = NOGO_DEFAULT_EXCLUDES,
118 )
119 for module in ctx.modules:
120 if not module.is_root or not module.tags.nogo:
121 continue
122 if len(module.tags.nogo) > 1:
123 # Make use of the special formatting applied to tags by fail.
124 fail(
125 "go_sdk.nogo: only one tag can be specified per module, got:\n",
126 *[t for p in zip(module.tags.nogo, len(module.tags.nogo) * ["\n"]) for t in p]
127 )
128 nogo_tag = module.tags.nogo[0]
129 for scope in nogo_tag.includes + nogo_tag.excludes:
130 # Validate that the scope references a valid, visible repository.
131 # buildifier: disable=no-effect
132 scope.workspace_name
133 if scope.name != "__pkg__" and scope.name != "__subpackages__":
134 fail(
135 "go_sdk.nogo: all entries in includes and excludes must end with ':__pkg__' or ':__subpackages__', got '{}' in".format(scope.name),
136 nogo_tag,
137 )
138 go_register_nogo(
139 name = "io_bazel_rules_nogo",
140 nogo = str(nogo_tag.nogo),
141 # Go through canonical label literals to avoid a dependency edge on the packages in the
142 # scope.
143 includes = [str(l) for l in nogo_tag.includes],
144 excludes = [str(l) for l in nogo_tag.excludes],
145 )
146
147 multi_version_module = {}
148 for module in ctx.modules:
149 if module.name in multi_version_module:
150 multi_version_module[module.name] = True
151 else:
152 multi_version_module[module.name] = False
153
154 # We remember the first host compatible toolchain declared by the download and host tags.
155 # The order follows bazel's iteration over modules (the toolchains declared by the root module are considered first).
156 # We know that at least `go_default_sdk` (which is declared by the `rules_go` module itself) is host compatible.
157 first_host_compatible_toolchain = None
158 host_detected_goos, host_detected_goarch = detect_host_platform(ctx)
159 toolchains = []
160 for module in ctx.modules:
161 for index, download_tag in enumerate(module.tags.download):
162 # SDKs without an explicit version are fetched even when not selected by toolchain
163 # resolution. This is acceptable if brought in by the root module, but transitive
164 # dependencies should not slow down the build in this way.
165 if not module.is_root and not download_tag.version:
166 fail("go_sdk.download: version must be specified in non-root module " + module.name)
167
168 # SDKs with an explicit name are at risk of colliding with those from other modules.
169 # This is acceptable if brought in by the root module as the user is responsible for any
170 # conflicts that arise. rules_go itself provides "go_default_sdk".
171 # TODO: Now that Gazelle relies on the go_host_compatible_sdk_label repo, remove the
172 # special case for "go_default_sdk". Users should migrate to @rules_go//go.
173 if (not module.is_root and not module.name == "rules_go") and download_tag.name:
174 fail("go_sdk.download: name must not be specified in non-root module " + module.name)
175
176 name = download_tag.name or _default_go_sdk_name(
177 module = module,
178 multi_version = multi_version_module[module.name],
179 tag_type = "download",
180 index = index,
181 )
182 go_download_sdk_rule(
183 name = name,
184 goos = download_tag.goos,
185 goarch = download_tag.goarch,
186 sdks = download_tag.sdks,
187 experiments = download_tag.experiments,
188 patches = download_tag.patches,
189 patch_strip = download_tag.patch_strip,
190 urls = download_tag.urls,
191 version = download_tag.version,
192 strip_prefix = download_tag.strip_prefix,
193 )
194
195 if (not download_tag.goos or download_tag.goos == host_detected_goos) and (not download_tag.goarch or download_tag.goarch == host_detected_goarch):
196 first_host_compatible_toolchain = first_host_compatible_toolchain or "@{}//:ROOT".format(name)
197
198 toolchains.append(struct(
199 goos = download_tag.goos,
200 goarch = download_tag.goarch,
201 sdk_repo = name,
202 sdk_type = "remote",
203 sdk_version = download_tag.version,
204 ))
205
206 # Additionally register SDKs for all common execution platforms, but only if the user
207 # specified a version to prevent eager fetches.
208 if download_tag.version and not download_tag.goos and not download_tag.goarch:
209 for goos, goarch in _COMMON_EXEC_PLATFORMS:
210 if goos == host_detected_goos and goarch == host_detected_goarch:
211 # We already added the host-compatible toolchain above.
212 continue
213
214 if download_tag.sdks and not "{}_{}".format(goos, goarch) in download_tag.sdks:
215 # The user supplied custom download links, but not for this tuple.
216 continue
217
218 default_name = _default_go_sdk_name(
219 module = module,
220 multi_version = multi_version_module[module.name],
221 tag_type = "download",
222 index = index,
223 suffix = "_{}_{}".format(goos, goarch),
224 )
225 go_download_sdk_rule(
226 name = default_name,
227 goos = download_tag.goos,
228 goarch = download_tag.goarch,
229 sdks = download_tag.sdks,
230 urls = download_tag.urls,
231 version = download_tag.version,
232 )
233
234 toolchains.append(struct(
235 goos = goos,
236 goarch = goarch,
237 sdk_repo = default_name,
238 sdk_type = "remote",
239 sdk_version = download_tag.version,
240 ))
241
242 for index, host_tag in enumerate(module.tags.host):
243 # Dependencies can rely on rules_go providing a default remote SDK. They can also
244 # configure a specific version of the SDK to use. However, they should not add a
245 # dependency on the host's Go SDK.
246 if not module.is_root:
247 fail("go_sdk.host: cannot be used in non-root module " + module.name)
248
249 name = host_tag.name or _default_go_sdk_name(
250 module = module,
251 multi_version = multi_version_module[module.name],
252 tag_type = "host",
253 index = index,
254 )
255 go_host_sdk_rule(
256 name = name,
257 version = host_tag.version,
258 experiments = host_tag.experiments,
259 )
260
261 toolchains.append(struct(
262 goos = "",
263 goarch = "",
264 sdk_repo = name,
265 sdk_type = "host",
266 sdk_version = host_tag.version,
267 ))
268 first_host_compatible_toolchain = first_host_compatible_toolchain or "@{}//:ROOT".format(name)
269
270 host_compatible_toolchain(name = "go_host_compatible_sdk_label", toolchain = first_host_compatible_toolchain)
271 if len(toolchains) > _MAX_NUM_TOOLCHAINS:
272 fail("more than {} go_sdk tags are not supported".format(_MAX_NUM_TOOLCHAINS))
273
274 # Toolchains in a BUILD file are registered in the order given by name, not in the order they
275 # are declared:
276 # https://cs.opensource.google/bazel/bazel/+/master:src/main/java/com/google/devtools/build/lib/packages/Package.java;drc=8e41dce65b97a3d466d6b1e65005abc52a07b90b;l=156
277 # We pad with an index that lexicographically sorts in the same order as if these toolchains
278 # were registered using register_toolchains in their MODULE.bazel files.
279 go_multiple_toolchains(
280 name = "go_toolchains",
281 prefixes = [
282 _toolchain_prefix(index, toolchain.sdk_repo)
283 for index, toolchain in enumerate(toolchains)
284 ],
285 geese = [toolchain.goos for toolchain in toolchains],
286 goarchs = [toolchain.goarch for toolchain in toolchains],
287 sdk_repos = [toolchain.sdk_repo for toolchain in toolchains],
288 sdk_types = [toolchain.sdk_type for toolchain in toolchains],
289 sdk_versions = [toolchain.sdk_version for toolchain in toolchains],
290 )
291
292def _default_go_sdk_name(*, module, multi_version, tag_type, index, suffix = ""):
293 # Keep the version out of the repository name if possible to prevent unnecessary rebuilds when
294 # it changes.
295 return "{name}_{version}_{tag_type}_{index}{suffix}".format(
296 # "main_" is not a valid module name and thus can't collide.
297 name = module.name or "main_",
298 version = module.version if multi_version else "",
299 tag_type = tag_type,
300 index = index,
301 suffix = suffix,
302 )
303
304def _toolchain_prefix(index, name):
305 """Prefixes the given name with the index, padded with zeros to ensure lexicographic sorting.
306
307 Examples:
308 _toolchain_prefix( 2, "foo") == "_0002_foo_"
309 _toolchain_prefix(2000, "foo") == "_2000_foo_"
310 """
311 return "_{}_{}_".format(_left_pad_zero(index, _TOOLCHAIN_INDEX_PAD_LENGTH), name)
312
313def _left_pad_zero(index, length):
314 if index < 0:
315 fail("index must be non-negative")
316 return ("0" * length + str(index))[-length:]
317
318go_sdk_extra_kwargs = {
319 # The choice of a host-compatible SDK is expressed in repository rule attribute values and
320 # depends on host OS and architecture.
321 "os_dependent": True,
322 "arch_dependent": True,
323} if bazel_features.external_deps.module_extension_has_os_arch_dependent else {}
324
325go_sdk = module_extension(
326 implementation = _go_sdk_impl,
327 tag_classes = {
328 "download": _download_tag,
329 "host": _host_tag,
330 "nogo": _nogo_tag,
331 },
332 **go_sdk_extra_kwargs
333)
View as plain text