Skip to content

Commit 3d7141d

Browse files
authored
Add go_sdk.from_file to read the SDK version from go.mod. (#4305)
**What type of PR is this?** Feature **What does this PR do? Why is it needed?** Adds a `go_sdk.from_file` rule to the extension that allows modules to say that their SDK version should be read from their `go.mod` file. This allows callers to avoid repeating themselves and stay within the Go tool environment for version specification, allowing better integration with other tools. **Which issues(s) does this PR fix?** Fixes #4292 **Other notes for review** The toolchain version is selected based on the rules in https://go.dev/doc/toolchain This change copies parts of the `go_mod.bzl` parser from the Gazelle repository: https://github.com/bazel-contrib/bazel-gazelle/blob/master/internal/bzlmod/go_mod.bzl A test based on the `go_download_sdk` test sets up a module-under-test with a `go.mod` and checks that the version specified in it is used to run the test in that repo-under-test.
1 parent b1f572a commit 3d7141d

File tree

7 files changed

+359
-22
lines changed

7 files changed

+359
-22
lines changed

MODULE.bazel

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ bazel_dep(name = "protobuf", version = "3.19.2", repo_name = "com_google_protobu
1616
bazel_dep(name = "rules_shell", version = "0.3.0")
1717

1818
go_sdk = use_extension("//go:extensions.bzl", "go_sdk")
19-
go_sdk.download(
19+
go_sdk.from_file(
2020
name = "go_default_sdk",
21-
version = "1.23.6",
21+
go_mod = "//:go.mod",
2222
)
2323
use_repo(
2424
go_sdk,

docs/go/core/bzlmod.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@ To register a particular version of the Go SDK, use the `go_sdk` module extensio
3030
```starlark
3131
go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
3232

33-
# Download an SDK for the host OS & architecture as well as common remote execution platforms.
33+
# Download an SDK for the host OS & architecture as well as common remote execution
34+
# platforms, using the version given from the `go.mod` file.
35+
go_sdk.from_file(go_mod = "//:go.mod")
36+
37+
# Download an SDK for the host OS & architecture as well as common remote execution
38+
# platforms, with a specific version.
3439
go_sdk.download(version = "1.23.1")
3540

3641
# Alternatively, download an SDK for a fixed OS/architecture.

go/private/extensions.bzl

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
load("@io_bazel_rules_go_bazel_features//:features.bzl", "bazel_features")
16+
load("//go/private:go_mod.bzl", "version_from_go_mod")
1617
load("//go/private:nogo.bzl", "DEFAULT_NOGO", "NOGO_DEFAULT_EXCLUDES", "NOGO_DEFAULT_INCLUDES", "go_register_nogo")
1718
load("//go/private:sdk.bzl", "detect_host_platform", "go_download_sdk_rule", "go_host_sdk_rule", "go_multiple_toolchains", "go_wrap_sdk_rule")
1819

@@ -35,25 +36,29 @@ host_compatible_toolchain = repository_rule(
3536
doc = "An external repository to expose the first host compatible toolchain",
3637
)
3738

39+
_COMMON_TAG_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+
"patches": attr.label_list(
49+
doc = "A list of patches to apply to the SDK after downloading it",
50+
),
51+
"patch_strip": attr.int(
52+
default = 0,
53+
doc = "The number of leading path segments to be stripped from the file name in the patches.",
54+
),
55+
"strip_prefix": attr.string(default = "go"),
56+
}
57+
3858
_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/{}"]),
59+
doc = """Download a specific Go SDK at the optional GOOS, GOARCH, and version, from a customisable URL. Optionally apply local customisations to the SDK by applying patches and setting experiments.""",
60+
attrs = _COMMON_TAG_ATTRS | {
4861
"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"),
5762
},
5863
)
5964

@@ -112,6 +117,15 @@ _wrap_tag = tag_class(
112117
},
113118
)
114119

120+
_from_file_tag = tag_class(
121+
doc = """Use a specific Go SDK version described by a `go.mod` file. Optionally supply GOOS, GOARCH, and download from a customisable URL, and apply local patches or set experiments.""",
122+
attrs = _COMMON_TAG_ATTRS | {
123+
"go_mod": attr.label(
124+
doc = "The go.mod file to read the SDK version from.",
125+
),
126+
},
127+
)
128+
115129
# A list of (goos, goarch) pairs that are commonly used for remote executors in cross-platform
116130
# builds (where host != exec platform). By default, we register toolchains for all of these
117131
# platforms in addition to the host platform.
@@ -201,7 +215,22 @@ def _go_sdk_impl(ctx):
201215
sdk_version = wrap_tag.version,
202216
))
203217

204-
for index, download_tag in enumerate(module.tags.download):
218+
additional_download_tags = []
219+
220+
# If the module suggests to read the toolchain version from a `go.mod` file, use that.
221+
for index, from_file_tag in enumerate(module.tags.from_file):
222+
version = version_from_go_mod(ctx, from_file_tag.go_mod)
223+
224+
# Synthesize a `download` tag so we can reuse the selection logic below.
225+
download_tag = {
226+
key: getattr(from_file_tag, key)
227+
for key in dir(from_file_tag)
228+
if key not in ["go_mod"]
229+
}
230+
download_tag["version"] = version
231+
additional_download_tags += [struct(**download_tag)]
232+
233+
for index, download_tag in enumerate(module.tags.download + additional_download_tags):
205234
# SDKs without an explicit version are fetched even when not selected by toolchain
206235
# resolution. This is acceptable if brought in by the root module, but transitive
207236
# dependencies should not slow down the build in this way.
@@ -223,7 +252,7 @@ def _go_sdk_impl(ctx):
223252
index = index,
224253
)
225254

226-
# Keep in sync with the other call to go_download_sdk_rule below.
255+
# Keep in sync with the other calls to `go_download_sdk_rule` above and below.
227256
go_download_sdk_rule(
228257
name = name,
229258
goos = download_tag.goos,
@@ -267,6 +296,8 @@ def _go_sdk_impl(ctx):
267296
index = index,
268297
suffix = "_{}_{}".format(goos, goarch),
269298
)
299+
300+
# Keep in sync with the other calls to `go_download_sdk_rule` above.
270301
go_download_sdk_rule(
271302
name = default_name,
272303
goos = goos,
@@ -383,6 +414,7 @@ go_sdk = module_extension(
383414
"host": _host_tag,
384415
"nogo": _nogo_tag,
385416
"wrap": _wrap_tag,
417+
"from_file": _from_file_tag,
386418
},
387419
**go_sdk_extra_kwargs
388420
)

go/private/go_mod.bzl

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Copyright 2025 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+
15+
def version_from_go_mod(module_ctx, go_mod_label):
16+
"""Returns a version string from a go.mod file.
17+
18+
Args:
19+
module_ctx: a https://bazel.build/rules/lib/module_ctx object passed
20+
from the MODULE.bazel call.
21+
go_mod_label: a Label for a `go.mod` file.
22+
23+
Returns:
24+
a string containing the version of the Go SDK defined in go.mod
25+
"""
26+
_check_go_mod_name(go_mod_label.name)
27+
go_mod_path = module_ctx.path(go_mod_label)
28+
go_mod_content = module_ctx.read(go_mod_path)
29+
30+
state = {
31+
"toolchain": None,
32+
"go": None,
33+
}
34+
35+
current_directive = None
36+
for line_no, line in enumerate(go_mod_content.splitlines(), 1):
37+
tokens, _ = _tokenize_line(line, go_mod_path, line_no)
38+
if not tokens:
39+
continue
40+
41+
if not current_directive:
42+
if tokens[0] == "go":
43+
_validate_go_version(go_mod_path, state, tokens, line_no)
44+
state["go"] = tokens[1]
45+
46+
if tokens[0] == "toolchain":
47+
_validate_toolchain_version(go_mod_path, state, tokens, line_no)
48+
state["toolchain"] = tokens[1][len("go"):].strip()
49+
50+
if tokens[1] == "(":
51+
current_directive = tokens[0]
52+
if len(tokens) > 2:
53+
fail("{}:{}: unexpected token '{}' after '('".format(go_mod_path, line_no, tokens[2]))
54+
continue
55+
elif tokens[0] == ")":
56+
current_directive = None
57+
if len(tokens) > 1:
58+
fail("{}:{}: unexpected token '{}' after ')'".format(go_mod_path, line_no, tokens[1]))
59+
continue
60+
61+
version = state["toolchain"]
62+
if not version:
63+
# https://go.dev/doc/toolchain: "a go.mod that says go 1.21.0 with no toolchain line is interpreted as if it had a toolchain go1.21.0 line."
64+
version = state["go"]
65+
if not version:
66+
# "As of the Go 1.17 release, if the go directive is missing, go 1.16 is assumed."
67+
version = "1.16"
68+
69+
return version
70+
71+
def _tokenize_line(line, path, line_no):
72+
tokens = []
73+
r = line
74+
for _ in range(len(line)):
75+
r = r.strip()
76+
if not r:
77+
break
78+
79+
if r[0] == "`":
80+
end = r.find("`", 1)
81+
if end == -1:
82+
fail("{}:{}: unterminated raw string".format(path, line_no))
83+
84+
tokens.append(r[1:end])
85+
r = r[end + 1:]
86+
87+
elif r[0] == "\"":
88+
value = ""
89+
escaped = False
90+
found_end = False
91+
pos = 0
92+
for pos in range(1, len(r)):
93+
c = r[pos]
94+
95+
if escaped:
96+
value += c
97+
escaped = False
98+
continue
99+
100+
if c == "\\":
101+
escaped = True
102+
continue
103+
104+
if c == "\"":
105+
found_end = True
106+
break
107+
108+
value += c
109+
110+
if not found_end:
111+
fail("{}:{}: unterminated interpreted string".format(path, line_no))
112+
113+
tokens.append(value)
114+
r = r[pos + 1:]
115+
116+
elif r.startswith("//"):
117+
# A comment always ends the current line
118+
return tokens, r[len("//"):].strip()
119+
120+
else:
121+
token, _, r = r.partition(" ")
122+
tokens.append(token)
123+
124+
return tokens, None
125+
126+
def _check_go_mod_name(name):
127+
if name != "go.mod":
128+
fail("go_sdk.from_file requires a 'go.mod' file, not '{}'".format(name))
129+
130+
def _validate_go_version(path, state, tokens, line_no):
131+
if len(tokens) == 1:
132+
fail("{}:{}: expected another token after 'go'".format(path, line_no))
133+
if state["go"] != None:
134+
fail("{}:{}: unexpected second 'go' directive".format(path, line_no))
135+
if len(tokens) > 2:
136+
fail("{}:{}: unexpected token '{}' after '{}'".format(path, line_no, tokens[2], tokens[1]))
137+
138+
def _validate_toolchain_version(path, state, tokens, line_no):
139+
if len(tokens) == 1:
140+
fail("{}:{}: expected another token after 'toolchain'".format(path, line_no))
141+
if state["toolchain"] != None:
142+
fail("{}:{}: unexpected second 'toolchain' directive".format(path, line_no))
143+
if len(tokens) > 2:
144+
fail("{}:{}: unexpected token '{}' after '{}'".format(path, line_no, tokens[2], tokens[1]))
145+
if not tokens[1].startswith("go"):
146+
fail("{}:{}: expected toolchain version to start with 'go', not '{}'".format(path, line_no, tokens[1]))
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
load("@io_bazel_rules_go//go/tools/bazel_testing:def.bzl", "go_bazel_test")
2+
3+
go_bazel_test(
4+
name = "from_go_mod_file_test",
5+
srcs = ["from_go_mod_file_test.go"],
6+
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from_go_mod_file
2+
================
3+
4+
from_go_mod_file_test
5+
---------------------
6+
Verifies that ``go_sdk.from_file`` can be used to fetch the Go SDK version
7+
that is described by the toolchain in ``go.mod``.

0 commit comments

Comments
 (0)