# Copyright 2020 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( "@bazel_skylib//lib:paths.bzl", "paths", ) load( "//go/private:mode.bzl", "LINKMODES", "LINKMODE_NORMAL", ) load( "//go/private:platforms.bzl", "CGO_GOOS_GOARCH", "GOOS_GOARCH", ) load( "//go/private:providers.bzl", "GoArchive", "GoLibrary", "GoSource", ) # A list of rules_go settings that are possibly set by go_transition. # Keep their package name in sync with the implementation of # _original_setting_key. TRANSITIONED_GO_SETTING_KEYS = [ "//go/config:static", "//go/config:msan", "//go/config:race", "//go/config:pure", "//go/config:linkmode", "//go/config:tags", "//go/config:pgoprofile", ] def _deduped_and_sorted(strs): return sorted({s: None for s in strs}.keys()) def _original_setting_key(key): if not "//go/config:" in key: fail("_original_setting_key currently assumes that all Go settings live under //go/config, got: " + key) name = key.split(":")[1] return "//go/private/rules:original_" + name _SETTING_KEY_TO_ORIGINAL_SETTING_KEY = { setting: _original_setting_key(setting) for setting in TRANSITIONED_GO_SETTING_KEYS } def _go_transition_impl(settings, attr): # NOTE: Keep the list of rules_go settings set by this transition in sync # with POTENTIALLY_TRANSITIONED_SETTINGS. # # NOTE(bazelbuild/bazel#11409): Calling fail here for invalid combinations # of flags reports an error but does not stop the build. # In any case, get_mode should mainly be responsible for reporting # invalid modes, since it also takes --features flags into account. original_settings = settings settings = dict(settings) _set_ternary(settings, attr, "static") race = _set_ternary(settings, attr, "race") msan = _set_ternary(settings, attr, "msan") pure = _set_ternary(settings, attr, "pure") if race == "on": if pure == "on": fail('race = "on" cannot be set when pure = "on" is set. race requires cgo.') pure = "off" settings["//go/config:pure"] = False if msan == "on": if pure == "on": fail('msan = "on" cannot be set when msan = "on" is set. msan requires cgo.') pure = "off" settings["//go/config:pure"] = False if pure == "on": race = "off" settings["//go/config:race"] = False msan = "off" settings["//go/config:msan"] = False cgo = pure == "off" goos = getattr(attr, "goos", "auto") goarch = getattr(attr, "goarch", "auto") _check_ternary("pure", pure) if goos != "auto" or goarch != "auto": if goos == "auto": fail("goos must be set if goarch is set") if goarch == "auto": fail("goarch must be set if goos is set") if (goos, goarch) not in GOOS_GOARCH: fail("invalid goos, goarch pair: {}, {}".format(goos, goarch)) if cgo and (goos, goarch) not in CGO_GOOS_GOARCH: fail('pure is "off" but cgo is not supported on {} {}'.format(goos, goarch)) platform = "@io_bazel_rules_go//go/toolchain:{}_{}{}".format(goos, goarch, "_cgo" if cgo else "") settings["//command_line_option:platforms"] = platform tags = getattr(attr, "gotags", []) if tags: settings["//go/config:tags"] = _deduped_and_sorted(tags) linkmode = getattr(attr, "linkmode", "auto") if linkmode != "auto": if linkmode not in LINKMODES: fail("linkmode: invalid mode {}; want one of {}".format(linkmode, ", ".join(LINKMODES))) settings["//go/config:linkmode"] = linkmode pgoprofile = getattr(attr, "pgoprofile", "auto") if pgoprofile != "auto": settings["//go/config:pgoprofile"] = pgoprofile for key, original_key in _SETTING_KEY_TO_ORIGINAL_SETTING_KEY.items(): old_value = original_settings[key] value = settings[key] # If the outgoing configuration would differ from the incoming one in a # value, record the old value in the special original_* key so that the # real setting can be reset to this value before the new configuration # would cross a non-deps dependency edge. if value != old_value: # Encoding as JSON makes it possible to embed settings of arbitrary # types (currently bool, string and string_list) into a single type # of setting (string) with the information preserved whether the # original setting wasn't set explicitly (empty string) or was set # explicitly to its default (always a non-empty string with JSON # encoding, e.g. "\"\"" or "[]"). if type(old_value) == "Label": # Label is not JSON serializable, so we need to convert it to a string. old_value = str(old_value) settings[original_key] = json.encode(old_value) else: settings[original_key] = "" return settings def _request_nogo_transition(settings, _attr): """Indicates that we want the project configured nogo instead of a noop. This does not guarantee that the project configured nogo will be used (if bootstrap is true we are currently building nogo so that is a cyclic dependency). The config setting nogo_active requires bootstrap to be false and request_nogo to be true to provide the project configured nogo. """ settings = dict(settings) settings["//go/private:request_nogo"] = True return settings request_nogo_transition = transition( implementation = _request_nogo_transition, inputs = [], outputs = ["//go/private:request_nogo"], ) go_transition = transition( implementation = _go_transition_impl, inputs = [ "//command_line_option:platforms", ] + TRANSITIONED_GO_SETTING_KEYS, outputs = [ "//command_line_option:platforms", ] + TRANSITIONED_GO_SETTING_KEYS + _SETTING_KEY_TO_ORIGINAL_SETTING_KEY.values(), ) _common_reset_transition_dict = dict({ "//go/private:request_nogo": False, "//go/config:static": False, "//go/config:msan": False, "//go/config:race": False, "//go/config:pure": False, "//go/config:debug": False, "//go/config:linkmode": LINKMODE_NORMAL, "//go/config:tags": [], "//go/config:pgoprofile": Label("//go/config:empty"), }, **{setting: "" for setting in _SETTING_KEY_TO_ORIGINAL_SETTING_KEY.values()}) _reset_transition_dict = dict(_common_reset_transition_dict, **{ "//go/private:bootstrap_nogo": True, }) _reset_transition_keys = sorted(_reset_transition_dict.keys()) _stdlib_keep_keys = sorted([ "//go/config:msan", "//go/config:race", "//go/config:pure", "//go/config:linkmode", "//go/config:tags", "//go/config:pgoprofile", ]) def _go_tool_transition_impl(settings, _attr): """Sets most Go settings to default values (use for external Go tools). go_tool_transition sets all of the //go/config settings to their default values and disables nogo. This is used for Go tool binaries like nogo itself. Tool binaries shouldn't depend on the link mode or tags of the target configuration and neither the tools nor the code they potentially generate should be subject to nogo's static analysis. This transition doesn't change the platform (goos, goarch), but tool binaries should also have `cfg = "exec"` so tool binaries should be built for the execution platform. """ return dict(settings, **_reset_transition_dict) go_tool_transition = transition( implementation = _go_tool_transition_impl, inputs = _reset_transition_keys, outputs = _reset_transition_keys, ) def _non_go_tool_transition_impl(settings, _attr): """Sets all Go settings to default values (use for external non-Go tools). non_go_tool_transition sets all of the //go/config settings as well as the nogo settings to their default values. This is used for all tools that are not themselves targets created from rules_go rules and thus do not read these settings. Resetting all of them to defaults prevents unnecessary configuration changes for these targets that could cause rebuilds. Examples: This transition is applied to attributes referencing proto_library targets or protoc directly. """ settings = dict(settings, **_reset_transition_dict) settings["//go/private:bootstrap_nogo"] = False return settings non_go_tool_transition = transition( implementation = _non_go_tool_transition_impl, inputs = _reset_transition_keys, outputs = _reset_transition_keys, ) def _go_stdlib_transition_impl(settings, _attr): """Sets all Go settings to their default values, except for those affecting the Go SDK. This transition is similar to _non_go_tool_transition except that it keeps the parts of the configuration that determine how to build the standard library. It's used to consolidate the configurations used to build the standard library to limit the number built. """ settings = dict(settings) for label, value in _reset_transition_dict.items(): if label not in _stdlib_keep_keys: settings[label] = value settings["//go/config:tags"] = [t for t in settings["//go/config:tags"] if t in _TAG_AFFECTS_STDLIB] settings["//go/private:bootstrap_nogo"] = False return settings go_stdlib_transition = transition( implementation = _go_stdlib_transition_impl, inputs = _reset_transition_keys, outputs = _reset_transition_keys, ) def _go_reset_target_impl(ctx): t = ctx.attr.dep[0] # [0] seems to be necessary with the transition providers = [t[p] for p in [GoLibrary, GoSource, GoArchive] if p in t] # We can't pass DefaultInfo through as-is, since Bazel forbids executable # if it's a file declared in a different target. To emulate that, symlink # to the original executable, if there is one. default_info = t[DefaultInfo] new_executable = None original_executable = default_info.files_to_run.executable default_runfiles = default_info.default_runfiles if original_executable: # In order for the symlink to have the same basename as the original # executable (important in the case of proto plugins), put it in a # subdirectory named after the label to prevent collisions. new_executable = ctx.actions.declare_file(paths.join(ctx.label.name, original_executable.basename)) ctx.actions.symlink( output = new_executable, target_file = original_executable, is_executable = True, ) default_runfiles = default_runfiles.merge(ctx.runfiles([new_executable])) providers.append( DefaultInfo( files = default_info.files, data_runfiles = default_info.data_runfiles, default_runfiles = default_runfiles, executable = new_executable, ), ) return providers go_reset_target = rule( implementation = _go_reset_target_impl, attrs = { "dep": attr.label( mandatory = True, cfg = go_tool_transition, ), "_allowlist_function_transition": attr.label( default = "@bazel_tools//tools/allowlists/function_transition_allowlist", ), }, doc = """Forwards providers from a target and applies go_tool_transition. go_reset_target depends on a single target, built using go_tool_transition. It forwards Go providers and DefaultInfo. This is used to work around a problem with building tools: Go tools should be built with 'cfg = "exec"' so they work on the execution platform, but we also need to apply go_tool_transition so that e.g. a tool isn't built as a shared library with race instrumentation. This acts as an intermediate rule that allows to apply both both transitions. """, ) non_go_reset_target = rule( implementation = _go_reset_target_impl, attrs = { "dep": attr.label( mandatory = True, cfg = non_go_tool_transition, ), "_allowlist_function_transition": attr.label( default = "@bazel_tools//tools/allowlists/function_transition_allowlist", ), }, doc = """Forwards providers from a target and applies non_go_tool_transition. non_go_reset_target depends on a single target, built using non_go_tool_transition. It forwards Go providers and DefaultInfo. This is used to work around a problem with building tools: Non-Go tools should be built with 'cfg = "exec"' so they work on the execution platform, but they also shouldn't be affected by Go-specific config changes applied by go_transition. """, ) def _non_go_transition_impl(settings, _attr): """Sets all Go settings to the values they had before the last go_transition. non_go_transition sets all of the //go/config settings to the value they had before the last go_transition. This should be used on all attributes of go_library/go_binary/go_test that are built in the target configuration and do not constitute advertise any Go providers. Examples: This transition is applied to the 'data' attribute of go_binary so that other Go binaries used at runtime aren't affected by a non-standard link mode set on the go_binary target, but still use the same top-level settings such as e.g. race instrumentation. """ new_settings = {} for key, original_key in _SETTING_KEY_TO_ORIGINAL_SETTING_KEY.items(): original_value = settings[original_key] if original_value: # Reset to the original value of the setting before go_transition. new_settings[key] = json.decode(original_value) else: new_settings[key] = settings[key] # Reset the value of the helper setting to its default for two reasons: # 1. Performance: This ensures that the Go settings of non-Go # dependencies have the same values as before the go_transition, # which can prevent unnecessary rebuilds caused by configuration # changes. # 2. Correctness in edge cases: If there is a path in the build graph # from a go_binary's non-Go dependency to a go_library that does not # pass through another go_binary (e.g., through a custom rule # replacement for go_binary), this transition could be applied again # and cause incorrect Go setting values. new_settings[original_key] = "" return new_settings non_go_transition = transition( implementation = _non_go_transition_impl, inputs = TRANSITIONED_GO_SETTING_KEYS + _SETTING_KEY_TO_ORIGINAL_SETTING_KEY.values(), outputs = TRANSITIONED_GO_SETTING_KEYS + _SETTING_KEY_TO_ORIGINAL_SETTING_KEY.values(), ) def _check_ternary(name, value): if value not in ("on", "off", "auto"): fail('{}: must be "on", "off", or "auto"'.format(name)) def _set_ternary(settings, attr, name): value = getattr(attr, name, "auto") _check_ternary(name, value) if value != "auto": label = "//go/config:{}".format(name) settings[label] = value == "on" return value _SDK_VERSION_BUILD_SETTING = "//go/toolchain:sdk_version" TRANSITIONED_GO_CROSS_SETTING_KEYS = [ _SDK_VERSION_BUILD_SETTING, "//command_line_option:platforms", ] def _go_cross_transition_impl(settings, attr): settings = dict(settings) if attr.sdk_version != None: settings[_SDK_VERSION_BUILD_SETTING] = attr.sdk_version if attr.platform != None: settings["//command_line_option:platforms"] = str(attr.platform) return settings go_cross_transition = transition( implementation = _go_cross_transition_impl, inputs = TRANSITIONED_GO_CROSS_SETTING_KEYS, outputs = TRANSITIONED_GO_CROSS_SETTING_KEYS, ) # A list of Go build tags that potentially affect the build of the standard # library. # # This should be updated to contain the union of all tags relevant for all # versions of Go that are still relevant. # # Currently supported versions: 1.18, 1.19, 1.20 # # To regenerate, run and paste the output of # bazel run //go/tools/internal/stdlib_tags:stdlib_tags -- path/to/go_sdk_1/src ... _TAG_AFFECTS_STDLIB = { "alpha": None, "appengine": None, "asan": None, "boringcrypto": None, "cmd_go_bootstrap": None, "compiler_bootstrap": None, "debuglog": None, "faketime": None, "gc": None, "gccgo": None, "gen": None, "generate": None, "gofuzz": None, "ignore": None, "libfuzzer": None, "m68k": None, "math_big_pure_go": None, "msan": None, "netcgo": None, "netgo": None, "nethttpomithttp2": None, "nios2": None, "noopt": None, "osusergo": None, "purego": None, "race": None, "sh": None, "shbe": None, "tablegen": None, "testgo": None, "timetzdata": None, }