Skip to content

Commit 30a6f8d

Browse files
authored
Reland "go_sdk: store SDK filenames and hashes in lockfile facts" (#4493)
**What type of PR is this?** Feature **What does this PR do? Why is it needed?** This moves the download of the "all versions" JSON, which can't hit the repository cache, from each individual `go_download_sdk` into the module extension. If the current version of Bazel supports facts, this information will also be persisted in the lockfile, allowing for truly airgapped builds assuming an up-to-date download (formerly repository) cache. This is a reland of #4393 with the following improvements: * Adapted to the more limited facts API that actually got merged. * Prefetching of SDK hashes is now performed on a best-effort basis so that otherwise airgapped builds that provide SDK hashes do not result in failures. See bazelbuild/bazel#26198 for the PR that added facts support to Bazel. **Which issues(s) does this PR fix?** Fixes #3945 **Other notes for review** You can verify that this works by running `bazel mod show_repo @go_default_sdk` in the BCR test repo.
1 parent 3c293b0 commit 30a6f8d

File tree

6 files changed

+3256
-36
lines changed

6 files changed

+3256
-36
lines changed

go/private/extensions.bzl

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
load("@io_bazel_rules_go_bazel_features//:features.bzl", "bazel_features")
1616
load("//go/private:go_mod.bzl", "version_from_go_mod")
1717
load("//go/private:nogo.bzl", "DEFAULT_NOGO", "NOGO_DEFAULT_EXCLUDES", "NOGO_DEFAULT_INCLUDES", "go_register_nogo")
18-
load("//go/private:sdk.bzl", "detect_host_platform", "go_download_sdk_rule", "go_host_sdk_rule", "go_multiple_toolchains", "go_wrap_sdk_rule")
18+
load("//go/private:sdk.bzl", "detect_host_platform", "fetch_sdks_by_version", "go_download_sdk_rule", "go_host_sdk_rule", "go_multiple_toolchains", "go_wrap_sdk_rule")
1919

2020
def host_compatible_toolchain_impl(ctx):
2121
ctx.file("BUILD.bazel")
@@ -197,6 +197,38 @@ def _go_sdk_impl(ctx):
197197
first_host_compatible_toolchain = None
198198
host_detected_goos, host_detected_goarch = detect_host_platform(ctx)
199199
toolchains = []
200+
201+
all_sdks_by_version = {}
202+
used_sdks_by_version = {}
203+
facts = getattr(ctx, "facts", {})
204+
205+
def get_sdks_by_version_cached(version):
206+
# Avoid a download without a known digest in the SDK repo rule by fetching the SDKs filename
207+
# and digest here. When using a version of Bazel that supports module extension facts, this
208+
# info will be persisted in the lockfile, allowing for truly airgapped builds with an
209+
# up-to-date lockfile and download (formerly repository) cache.
210+
sdks = facts.get(version)
211+
if sdks == None:
212+
# Lazily fetch the information about all SDKs so that we avoid the download if the facts
213+
# already contain all the versions we care about. We take care to only do this once and
214+
# also accept failures to support airgapped builds: the user may have set sdk hashes on
215+
# all SDK repos they actually intend to use, but others (e.g., the default SDK added by
216+
# rules_go) trigger this path even if they would never be selected by toolchain
217+
# resolution. We must not break those builds.
218+
if not all_sdks_by_version:
219+
all_sdks_by_version.clear()
220+
all_sdks_by_version.update(fetch_sdks_by_version(ctx, allow_fail = True) or {
221+
"fetch_failed_but_should_not_fetch_again_sentinel": [],
222+
})
223+
sdks = all_sdks_by_version.get(version)
224+
if sdks == None:
225+
# This is either caused by an invalid version or because we are in an airgapped build
226+
# and the version wasn't present in facts. Since we don't want to fail in the latter
227+
# case, we leave it to the repository rule to report a useful error message.
228+
return None
229+
used_sdks_by_version[version] = sdks
230+
return sdks
231+
200232
for module in ctx.modules:
201233
# Apply wrapped toolchains first to override specific platforms from the
202234
# default toolchain or any downloads.
@@ -261,18 +293,12 @@ def _go_sdk_impl(ctx):
261293
index = index,
262294
)
263295

264-
# Keep in sync with the other calls to `go_download_sdk_rule` above and below.
265-
go_download_sdk_rule(
296+
_download_sdk(
297+
get_sdks_by_version = get_sdks_by_version_cached,
266298
name = name,
267299
goos = download_tag.goos,
268300
goarch = download_tag.goarch,
269-
sdks = download_tag.sdks,
270-
experiments = download_tag.experiments,
271-
patches = download_tag.patches,
272-
patch_strip = download_tag.patch_strip,
273-
urls = download_tag.urls,
274-
version = download_tag.version,
275-
strip_prefix = download_tag.strip_prefix,
301+
download_tag = download_tag,
276302
)
277303

278304
if (not download_tag.goos or download_tag.goos == host_detected_goos) and (not download_tag.goarch or download_tag.goarch == host_detected_goarch):
@@ -306,18 +332,12 @@ def _go_sdk_impl(ctx):
306332
suffix = "_{}_{}".format(goos, goarch),
307333
)
308334

309-
# Keep in sync with the other calls to `go_download_sdk_rule` above.
310-
go_download_sdk_rule(
335+
_download_sdk(
336+
get_sdks_by_version = get_sdks_by_version_cached,
311337
name = default_name,
312338
goos = goos,
313339
goarch = goarch,
314-
sdks = download_tag.sdks,
315-
experiments = download_tag.experiments,
316-
patches = download_tag.patches,
317-
patch_strip = download_tag.patch_strip,
318-
urls = download_tag.urls,
319-
version = download_tag.version,
320-
strip_prefix = download_tag.strip_prefix,
340+
download_tag = download_tag,
321341
)
322342

323343
toolchains.append(struct(
@@ -379,7 +399,14 @@ def _go_sdk_impl(ctx):
379399
)
380400

381401
if bazel_features.external_deps.extension_metadata_has_reproducible:
382-
return ctx.extension_metadata(reproducible = True)
402+
kwargs = {
403+
"reproducible": True,
404+
}
405+
406+
# See get_sdks_by_version_cached above for details on these facts.
407+
if hasattr(ctx, "facts"):
408+
kwargs["facts"] = used_sdks_by_version
409+
return ctx.extension_metadata(**kwargs)
383410
else:
384411
return None
385412

@@ -409,6 +436,25 @@ def _left_pad_zero(index, length):
409436
fail("index must be non-negative")
410437
return ("0" * length + str(index))[-length:]
411438

439+
def _download_sdk(*, get_sdks_by_version, name, goos, goarch, download_tag):
440+
version = download_tag.version
441+
sdks = download_tag.sdks
442+
if version and not sdks:
443+
sdks = get_sdks_by_version(version)
444+
445+
go_download_sdk_rule(
446+
name = name,
447+
goos = goos,
448+
goarch = goarch,
449+
sdks = sdks,
450+
experiments = download_tag.experiments,
451+
patches = download_tag.patches,
452+
patch_strip = download_tag.patch_strip,
453+
urls = download_tag.urls,
454+
version = download_tag.version,
455+
strip_prefix = download_tag.strip_prefix,
456+
)
457+
412458
go_sdk_extra_kwargs = {
413459
# The choice of a host-compatible SDK is expressed in repository rule attribute values and
414460
# depends on host OS and architecture.

go/private/sdk.bzl

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,7 @@ def _go_download_sdk_impl(ctx):
8181
ctx.report_progress("Finding latest Go version")
8282
else:
8383
ctx.report_progress("Finding Go SHA-256 sums")
84-
ctx.download(
85-
url = [
86-
"https://go.dev/dl/?mode=json&include=all",
87-
"https://golang.google.cn/dl/?mode=json&include=all",
88-
],
89-
output = "versions.json",
90-
)
91-
92-
data = ctx.read("versions.json")
93-
ctx.delete("versions.json")
94-
sdks_by_version = _parse_versions_json(data)
84+
sdks_by_version = fetch_sdks_by_version(ctx)
9585

9686
if not version:
9787
highest_version = None
@@ -580,6 +570,31 @@ def _parse_versions_json(data):
580570
for sdk in sdks
581571
}
582572

573+
def fetch_sdks_by_version(ctx, allow_fail = False):
574+
result = ctx.download(
575+
url = [
576+
"https://go.dev/dl/?mode=json&include=all",
577+
"https://golang.google.cn/dl/?mode=json&include=all",
578+
],
579+
output = "versions.json",
580+
allow_fail = allow_fail,
581+
)
582+
if not result.success:
583+
return None
584+
data = ctx.read("versions.json")
585+
586+
# If the download is redirected through a proxy such as Artifactory, it may
587+
# drop the query parameters and return an HTML page instead. In that case,
588+
# just return an empty map if allow_fail is set. It is unfortunately not
589+
# possible to attempt parsing as JSON and catch the error.
590+
if (not data or data[0] != "[") and allow_fail:
591+
return None
592+
593+
# module_ctx doesn't have delete, but its files are temporary anyway.
594+
if hasattr(ctx, "delete"):
595+
ctx.delete("versions.json")
596+
return _parse_versions_json(data)
597+
583598
def parse_version(version):
584599
"""Parses a version string like "1.15.5" and returns a tuple of numbers or None"""
585600
l, r = 0, 0

tests/bcr/.bazelversion

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
7.4.1
1+
1079973bd513b69fb56becc7fe678c03449b30c3
2+
# A commit on the 8.5.0 release branch with support for facts.
3+
# TODO: Update to 8.5.0 when released.

0 commit comments

Comments
 (0)