diff options
Diffstat (limited to 'go/private/sdk.bzl')
-rw-r--r-- | go/private/sdk.bzl | 704 |
1 files changed, 704 insertions, 0 deletions
diff --git a/go/private/sdk.bzl b/go/private/sdk.bzl new file mode 100644 index 00000000..a3fb6772 --- /dev/null +++ b/go/private/sdk.bzl @@ -0,0 +1,704 @@ +# Copyright 2014 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load( + "//go/private:common.bzl", + "executable_path", +) +load( + "//go/private:nogo.bzl", + "go_register_nogo", +) +load( + "//go/private/skylib/lib:versions.bzl", + "versions", +) + +MIN_SUPPORTED_VERSION = (1, 14, 0) + +def _go_host_sdk_impl(ctx): + goroot = _detect_host_sdk(ctx) + platform = _detect_sdk_platform(ctx, goroot) + version = _detect_sdk_version(ctx, goroot) + _sdk_build_file(ctx, platform, version, experiments = ctx.attr.experiments) + _local_sdk(ctx, goroot) + +go_host_sdk_rule = repository_rule( + implementation = _go_host_sdk_impl, + environ = ["GOROOT"], + attrs = { + "version": attr.string(), + "experiments": attr.string_list( + doc = "Go experiments to enable via GOEXPERIMENT", + ), + "_sdk_build_file": attr.label( + default = Label("//go/private:BUILD.sdk.bazel"), + ), + }, +) + +def go_host_sdk(name, register_toolchains = True, **kwargs): + go_host_sdk_rule(name = name, **kwargs) + _go_toolchains( + name = name + "_toolchains", + sdk_repo = name, + sdk_type = "host", + sdk_version = kwargs.get("version"), + goos = kwargs.get("goos"), + goarch = kwargs.get("goarch"), + ) + if register_toolchains: + _register_toolchains(name) + +def _go_download_sdk_impl(ctx): + if not ctx.attr.goos and not ctx.attr.goarch: + goos, goarch = detect_host_platform(ctx) + else: + if not ctx.attr.goos: + fail("goarch set but goos not set") + if not ctx.attr.goarch: + fail("goos set but goarch not set") + goos, goarch = ctx.attr.goos, ctx.attr.goarch + platform = goos + "_" + goarch + + version = ctx.attr.version + sdks = ctx.attr.sdks + + if not sdks: + # If sdks was unspecified, download a full list of files. + # If version was unspecified, pick the latest version. + # Even if version was specified, we need to download the file list + # to find the SHA-256 sum. If we don't have it, Bazel won't cache + # the downloaded archive. + if not version: + ctx.report_progress("Finding latest Go version") + else: + ctx.report_progress("Finding Go SHA-256 sums") + ctx.download( + url = [ + "https://go.dev/dl/?mode=json&include=all", + "https://golang.google.cn/dl/?mode=json&include=all", + ], + output = "versions.json", + ) + + data = ctx.read("versions.json") + sdks_by_version = _parse_versions_json(data) + + if not version: + highest_version = None + for v in sdks_by_version.keys(): + pv = parse_version(v) + if not pv or _version_is_prerelease(pv): + # skip parse errors and pre-release versions + continue + if not highest_version or _version_less(highest_version, pv): + highest_version = pv + if not highest_version: + fail("did not find any Go versions in https://go.dev/dl/?mode=json") + version = _version_string(highest_version) + if version not in sdks_by_version: + fail("did not find version {} in https://go.dev/dl/?mode=json".format(version)) + sdks = sdks_by_version[version] + + if platform not in sdks: + fail("unsupported platform {}".format(platform)) + filename, sha256 = sdks[platform] + _remote_sdk(ctx, [url.format(filename) for url in ctx.attr.urls], ctx.attr.strip_prefix, sha256) + + detected_version = _detect_sdk_version(ctx, ".") + _sdk_build_file(ctx, platform, detected_version, experiments = ctx.attr.experiments) + + if not ctx.attr.sdks and not ctx.attr.version: + # Returning this makes Bazel print a message that 'version' must be + # specified for a reproducible build. + return { + "name": ctx.attr.name, + "goos": ctx.attr.goos, + "goarch": ctx.attr.goarch, + "sdks": ctx.attr.sdks, + "urls": ctx.attr.urls, + "version": version, + "strip_prefix": ctx.attr.strip_prefix, + } + return None + +go_download_sdk_rule = repository_rule( + implementation = _go_download_sdk_impl, + attrs = { + "goos": attr.string(), + "goarch": attr.string(), + "sdks": attr.string_list_dict(), + "experiments": attr.string_list( + doc = "Go experiments to enable via GOEXPERIMENT", + ), + "urls": attr.string_list(default = ["https://dl.google.com/go/{}"]), + "version": attr.string(), + "strip_prefix": attr.string(default = "go"), + "_sdk_build_file": attr.label( + default = Label("//go/private:BUILD.sdk.bazel"), + ), + }, +) + +def _define_version_constants(version, prefix = ""): + pv = parse_version(version) + if pv == None or len(pv) < 3: + fail("error parsing sdk version: " + version) + major, minor, patch = pv[0], pv[1], pv[2] + prerelease = pv[3] if len(pv) > 3 else "" + return """ +{prefix}MAJOR_VERSION = "{major}" +{prefix}MINOR_VERSION = "{minor}" +{prefix}PATCH_VERSION = "{patch}" +{prefix}PRERELEASE_SUFFIX = "{prerelease}" +""".format( + prefix = prefix, + major = major, + minor = minor, + patch = patch, + prerelease = prerelease, + ) + +def _to_constant_name(s): + # Prefix with _ as identifiers are not allowed to start with numbers. + return "_" + "".join([c if c.isalnum() else "_" for c in s.elems()]).upper() + +def go_toolchains_single_definition(ctx, *, prefix, goos, goarch, sdk_repo, sdk_type, sdk_version): + if not goos and not goarch: + goos, goarch = detect_host_platform(ctx) + else: + if not goos: + fail("goarch set but goos not set") + if not goarch: + fail("goos set but goarch not set") + + chunks = [] + loads = [] + identifier_prefix = _to_constant_name(prefix) + + # If a sdk_version attribute is provided, use that version. This avoids + # eagerly fetching the SDK repository. But if it's not provided, we have + # no choice and must load version constants from the version.bzl file that + # _sdk_build_file creates. This will trigger an eager fetch. + if sdk_version: + chunks.append(_define_version_constants(sdk_version, prefix = identifier_prefix)) + else: + loads.append("""load( + "@{sdk_repo}//:version.bzl", + {identifier_prefix}MAJOR_VERSION = "MAJOR_VERSION", + {identifier_prefix}MINOR_VERSION = "MINOR_VERSION", + {identifier_prefix}PATCH_VERSION = "PATCH_VERSION", + {identifier_prefix}PRERELEASE_SUFFIX = "PRERELEASE_SUFFIX", +) +""".format( + sdk_repo = sdk_repo, + identifier_prefix = identifier_prefix, + )) + + chunks.append("""declare_bazel_toolchains( + prefix = "{prefix}", + go_toolchain_repo = "@{sdk_repo}", + host_goarch = "{goarch}", + host_goos = "{goos}", + major = {identifier_prefix}MAJOR_VERSION, + minor = {identifier_prefix}MINOR_VERSION, + patch = {identifier_prefix}PATCH_VERSION, + prerelease = {identifier_prefix}PRERELEASE_SUFFIX, + sdk_type = "{sdk_type}", +) +""".format( + prefix = prefix, + identifier_prefix = identifier_prefix, + sdk_repo = sdk_repo, + goarch = goarch, + goos = goos, + sdk_type = sdk_type, + )) + + return struct( + loads = loads, + chunks = chunks, + ) + +def go_toolchains_build_file_content( + ctx, + prefixes, + geese, + goarchs, + sdk_repos, + sdk_types, + sdk_versions): + if not _have_same_length(prefixes, geese, goarchs, sdk_repos, sdk_types, sdk_versions): + fail("all lists must have the same length") + + loads = [ + """load("@io_bazel_rules_go//go/private:go_toolchain.bzl", "declare_bazel_toolchains")""", + ] + chunks = [ + """package(default_visibility = ["//visibility:public"])""", + ] + + for i in range(len(geese)): + definition = go_toolchains_single_definition( + ctx, + prefix = prefixes[i], + goos = geese[i], + goarch = goarchs[i], + sdk_repo = sdk_repos[i], + sdk_type = sdk_types[i], + sdk_version = sdk_versions[i], + ) + loads.extend(definition.loads) + chunks.extend(definition.chunks) + + return "\n".join(loads + chunks) + +def _go_multiple_toolchains_impl(ctx): + ctx.file( + "BUILD.bazel", + go_toolchains_build_file_content( + ctx, + prefixes = ctx.attr.prefixes, + geese = ctx.attr.geese, + goarchs = ctx.attr.goarchs, + sdk_repos = ctx.attr.sdk_repos, + sdk_types = ctx.attr.sdk_types, + sdk_versions = ctx.attr.sdk_versions, + ), + executable = False, + ) + +go_multiple_toolchains = repository_rule( + implementation = _go_multiple_toolchains_impl, + attrs = { + "prefixes": attr.string_list(mandatory = True), + "sdk_repos": attr.string_list(mandatory = True), + "sdk_types": attr.string_list(mandatory = True), + "sdk_versions": attr.string_list(mandatory = True), + "geese": attr.string_list(mandatory = True), + "goarchs": attr.string_list(mandatory = True), + }, +) + +def _go_toolchains(name, sdk_repo, sdk_type, sdk_version = None, goos = None, goarch = None): + go_multiple_toolchains( + name = name, + prefixes = [""], + geese = [goos or ""], + goarchs = [goarch or ""], + sdk_repos = [sdk_repo], + sdk_types = [sdk_type], + sdk_versions = [sdk_version or ""], + ) + +def go_download_sdk(name, register_toolchains = True, **kwargs): + go_download_sdk_rule(name = name, **kwargs) + _go_toolchains( + name = name + "_toolchains", + sdk_repo = name, + sdk_type = "remote", + sdk_version = kwargs.get("version"), + goos = kwargs.get("goos"), + goarch = kwargs.get("goarch"), + ) + if register_toolchains: + _register_toolchains(name) + +def _go_local_sdk_impl(ctx): + goroot = ctx.attr.path + platform = _detect_sdk_platform(ctx, goroot) + version = _detect_sdk_version(ctx, goroot) + _sdk_build_file(ctx, platform, version, ctx.attr.experiments) + _local_sdk(ctx, goroot) + +_go_local_sdk = repository_rule( + implementation = _go_local_sdk_impl, + attrs = { + "path": attr.string(), + "version": attr.string(), + "experiments": attr.string_list( + doc = "Go experiments to enable via GOEXPERIMENT", + ), + "_sdk_build_file": attr.label( + default = Label("//go/private:BUILD.sdk.bazel"), + ), + }, +) + +def go_local_sdk(name, register_toolchains = True, **kwargs): + _go_local_sdk(name = name, **kwargs) + _go_toolchains( + name = name + "_toolchains", + sdk_repo = name, + sdk_type = "remote", + sdk_version = kwargs.get("version"), + goos = kwargs.get("goos"), + goarch = kwargs.get("goarch"), + ) + if register_toolchains: + _register_toolchains(name) + +def _go_wrap_sdk_impl(ctx): + if not ctx.attr.root_file and not ctx.attr.root_files: + fail("either root_file or root_files must be provided") + if ctx.attr.root_file and ctx.attr.root_files: + fail("root_file and root_files cannot be both provided") + if ctx.attr.root_file: + root_file = ctx.attr.root_file + else: + goos, goarch = detect_host_platform(ctx) + platform = goos + "_" + goarch + if platform not in ctx.attr.root_files: + fail("unsupported platform {}".format(platform)) + root_file = Label(ctx.attr.root_files[platform]) + goroot = str(ctx.path(root_file).dirname) + platform = _detect_sdk_platform(ctx, goroot) + version = _detect_sdk_version(ctx, goroot) + _sdk_build_file(ctx, platform, version, ctx.attr.experiments) + _local_sdk(ctx, goroot) + +_go_wrap_sdk = repository_rule( + implementation = _go_wrap_sdk_impl, + attrs = { + "root_file": attr.label( + mandatory = False, + doc = "A file in the SDK root direcotry. Used to determine GOROOT.", + ), + "root_files": attr.string_dict( + mandatory = False, + doc = "A set of mappings from the host platform to a file in the SDK's root directory", + ), + "version": attr.string(), + "experiments": attr.string_list( + doc = "Go experiments to enable via GOEXPERIMENT", + ), + "_sdk_build_file": attr.label( + default = Label("//go/private:BUILD.sdk.bazel"), + ), + }, +) + +def go_wrap_sdk(name, register_toolchains = True, **kwargs): + _go_wrap_sdk(name = name, **kwargs) + _go_toolchains( + name = name + "_toolchains", + sdk_repo = name, + sdk_type = "remote", + sdk_version = kwargs.get("version"), + goos = kwargs.get("goos"), + goarch = kwargs.get("goarch"), + ) + if register_toolchains: + _register_toolchains(name) + +def _register_toolchains(repo): + native.register_toolchains("@{}_toolchains//:all".format(repo)) + +def _remote_sdk(ctx, urls, strip_prefix, sha256): + if len(urls) == 0: + fail("no urls specified") + host_goos, _ = detect_host_platform(ctx) + + ctx.report_progress("Downloading and extracting Go toolchain") + + # TODO(#2771): After bazelbuild/bazel#18448 is merged and available in + # the minimum supported version of Bazel, remove the workarounds below. + # + # Go ships archives containing some non-ASCII file names, used in + # test cases for Go's build system. Bazel has a bug extracting these + # archives on certain file systems (macOS AFS at least, possibly also + # Docker on macOS with a bind mount). + # + # For .tar.gz files (available for most platforms), we work around this bug + # by using the system tar instead of ctx.download_and_extract. + # + # For .zip files, we use ctx.download_and_extract but with rename_files, + # changing certain paths that trigger the bug. This is only available + # in Bazel 6.0.0+ (bazelbuild/bazel#16052). The only situation where + # .zip files are needed seems to be a macOS host using a Windows toolchain + # for remote execution. + if urls[0].endswith(".tar.gz"): + if strip_prefix != "go": + fail("strip_prefix not supported") + ctx.download( + url = urls, + sha256 = sha256, + output = "go_sdk.tar.gz", + ) + res = ctx.execute(["tar", "-xf", "go_sdk.tar.gz", "--strip-components=1"]) + if res.return_code: + fail("error extracting Go SDK:\n" + res.stdout + res.stderr) + ctx.delete("go_sdk.tar.gz") + elif (urls[0].endswith(".zip") and + host_goos != "windows" and + # Development versions of Bazel have an empty version string. We assume that they are + # more recent than the version that introduced rename_files. + versions.is_at_least("6.0.0", versions.get() or "6.0.0")): + ctx.download_and_extract( + url = urls, + stripPrefix = strip_prefix, + sha256 = sha256, + rename_files = { + "go/test/fixedbugs/issue27836.dir/\336foo.go": "go/test/fixedbugs/issue27836.dir/thfoo.go", + "go/test/fixedbugs/issue27836.dir/\336main.go": "go/test/fixedbugs/issue27836.dir/thmain.go", + }, + ) + else: + ctx.download_and_extract( + url = urls, + stripPrefix = strip_prefix, + sha256 = sha256, + ) + +def _local_sdk(ctx, path): + for entry in ["src", "pkg", "bin", "lib", "misc"]: + ctx.symlink(path + "/" + entry, entry) + +def _sdk_build_file(ctx, platform, version, experiments): + ctx.file("ROOT") + goos, _, goarch = platform.partition("_") + + pv = parse_version(version) + if pv != None and pv[1] >= 20: + # Turn off coverageredesign GOEXPERIMENT on 1.20+ + # until rules_go is updated to work with the + # coverage redesign. + if not "nocoverageredesign" in experiments and not "coverageredesign" in experiments: + experiments = experiments + ["nocoverageredesign"] + + ctx.template( + "BUILD.bazel", + ctx.path(ctx.attr._sdk_build_file), + executable = False, + substitutions = { + "{goos}": goos, + "{goarch}": goarch, + "{exe}": ".exe" if goos == "windows" else "", + "{version}": version, + "{experiments}": repr(experiments), + }, + ) + + ctx.file( + "version.bzl", + executable = False, + content = _define_version_constants(version), + ) + +def detect_host_platform(ctx): + goos = ctx.os.name + if goos == "mac os x": + goos = "darwin" + elif goos.startswith("windows"): + goos = "windows" + + goarch = ctx.os.arch + if goarch == "aarch64": + goarch = "arm64" + elif goarch == "x86_64": + goarch = "amd64" + + return goos, goarch + +def _detect_host_sdk(ctx): + root = "@invalid@" + if "GOROOT" in ctx.os.environ: + return ctx.os.environ["GOROOT"] + res = ctx.execute([executable_path(ctx, "go"), "env", "GOROOT"]) + if res.return_code: + fail("Could not detect host go version") + root = res.stdout.strip() + if not root: + fail("host go version failed to report it's GOROOT") + return root + +def _detect_sdk_platform(ctx, goroot): + path = ctx.path(goroot + "/pkg/tool") + if not path.exists: + fail("Could not detect SDK platform: failed to find " + str(path)) + tool_entries = path.readdir() + + platforms = [] + for f in tool_entries: + if f.basename.find("_") >= 0: + platforms.append(f.basename) + + if len(platforms) == 0: + fail("Could not detect SDK platform: found no platforms in %s" % path) + if len(platforms) > 1: + fail("Could not detect SDK platform: found multiple platforms %s in %s" % (platforms, path)) + return platforms[0] + +def _detect_sdk_version(ctx, goroot): + version_file_path = goroot + "/VERSION" + if ctx.path(version_file_path).exists: + # VERSION file has version prefixed by go, eg. go1.18.3 + version = ctx.read(version_file_path)[2:] + if ctx.attr.version and ctx.attr.version != version: + fail("SDK is version %s, but version %s was expected" % (version, ctx.attr.version)) + return version + + # The top-level VERSION file does not exist in all Go SDK distributions, e.g. those shipped by Debian or Fedora. + # Falling back to running "go version" + go_binary_path = goroot + "/bin/go" + result = ctx.execute([go_binary_path, "version"]) + if result.return_code != 0: + fail("Could not detect SDK version: '%s version' exited with exit code %d" % (go_binary_path, result.return_code)) + + # go version output is of the form "go version go1.18.3 linux/amd64" or "go + # version devel go1.19-fd1b5904ae Tue Mar 22 21:38:10 2022 +0000 + # linux/amd64". See the following links for how this output is generated: + # - https://github.com/golang/go/blob/2bdb5c57f1efcbddab536028d053798e35de6226/src/cmd/go/internal/version/version.go#L75 + # - https://github.com/golang/go/blob/2bdb5c57f1efcbddab536028d053798e35de6226/src/cmd/dist/build.go#L333 + # + # Read the third word, or the fourth word if the third word is "devel", to + # find the version number. + output_parts = result.stdout.split(" ") + if len(output_parts) > 2 and output_parts[2].startswith("go"): + version = output_parts[2][len("go"):] + elif len(output_parts) > 3 and output_parts[2] == "devel" and output_parts[3].startswith("go"): + version = output_parts[3][len("go"):] + else: + fail("Could not parse SDK version from '%s version' output: %s" % (go_binary_path, result.stdout)) + if parse_version(version) == None: + fail("Could not parse SDK version from '%s version' output: %s" % (go_binary_path, result.stdout)) + if ctx.attr.version and ctx.attr.version != version: + fail("SDK is version %s, but version %s was expected" % (version, ctx.attr.version)) + return version + +def _parse_versions_json(data): + """Parses version metadata returned by go.dev. + + Args: + data: the contents of the file downloaded from + https://go.dev/dl/?mode=json. We assume the file is valid + JSON, is spaced and indented, and is in a particular format. + + Return: + A dict mapping version strings (like "1.15.5") to dicts mapping + platform names (like "linux_amd64") to pairs of filenames + (like "go1.15.5.linux-amd64.tar.gz") and hex-encoded SHA-256 sums. + """ + sdks = json.decode(data) + return { + sdk["version"][len("go"):]: { + "%s_%s" % (file["os"], file["arch"]): ( + file["filename"], + file["sha256"], + ) + for file in sdk["files"] + if file["kind"] == "archive" + } + for sdk in sdks + } + +def parse_version(version): + """Parses a version string like "1.15.5" and returns a tuple of numbers or None""" + l, r = 0, 0 + parsed = [] + for c in version.elems(): + if c == ".": + if l == r: + # empty component + return None + parsed.append(int(version[l:r])) + r += 1 + l = r + continue + + if c.isdigit(): + r += 1 + continue + + # pre-release suffix + break + + if l == r: + # empty component + return None + parsed.append(int(version[l:r])) + if len(parsed) == 2: + # first minor version, like (1, 15) + parsed.append(0) + if len(parsed) != 3: + # too many or too few components + return None + if r < len(version): + # pre-release suffix + parsed.append(version[r:]) + return tuple(parsed) + +def _version_is_prerelease(v): + return len(v) > 3 + +def _version_less(a, b): + if a[:3] < b[:3]: + return True + if a[:3] > b[:3]: + return False + if len(a) > len(b): + return True + if len(a) < len(b) or len(a) == 3: + return False + return a[3:] < b[3:] + +def _version_string(v): + suffix = v[3] if _version_is_prerelease(v) else "" + if v[-1] == 0: + v = v[:-1] + return ".".join([str(n) for n in v]) + suffix + +def _have_same_length(*lists): + if not lists: + fail("expected at least one list") + return len({len(l): None for l in lists}) == 1 + +def go_register_toolchains(version = None, nogo = None, go_version = None, experiments = None): + """See /go/toolchains.rst#go-register-toolchains for full documentation.""" + if not version: + version = go_version # old name + + sdk_kinds = ("go_download_sdk_rule", "go_host_sdk_rule", "_go_local_sdk", "_go_wrap_sdk") + existing_rules = native.existing_rules() + sdk_rules = [r for r in existing_rules.values() if r["kind"] in sdk_kinds] + if len(sdk_rules) == 0 and "go_sdk" in existing_rules: + # may be local_repository in bazel_tests. + sdk_rules.append(existing_rules["go_sdk"]) + + if version and len(sdk_rules) > 0: + fail("go_register_toolchains: version set after go sdk rule declared ({})".format(", ".join([r["name"] for r in sdk_rules]))) + if len(sdk_rules) == 0: + if not version: + fail('go_register_toolchains: version must be a string like "1.15.5" or "host"') + elif version == "host": + go_host_sdk(name = "go_sdk", experiments = experiments) + else: + pv = parse_version(version) + if not pv: + fail('go_register_toolchains: version must be a string like "1.15.5" or "host"') + if _version_less(pv, MIN_SUPPORTED_VERSION): + print("DEPRECATED: Go versions before {} are not supported and may not work".format(_version_string(MIN_SUPPORTED_VERSION))) + go_download_sdk( + name = "go_sdk", + version = version, + experiments = experiments, + ) + + if nogo: + # Override default definition in go_rules_dependencies(). + go_register_nogo( + name = "io_bazel_rules_nogo", + nogo = nogo, + ) |