Add `go_cross_binary` rule for cross-compilation. (#3261)

* Add `go_cross_binary` rule for cross-compilation.

- Adds a `go_cross_binary` rule that wraps a `go_binary` to generate a
  cross-compiled version of the binary.
- Supports compiling for a different platform, and/or a different golang
  SDK version.
- Adds docs for the new `go_cross_binary` rule.
- Adds testing in `tests/core/cross` for the new `go_cross_binary` rule.

Signed-off-by: James Bartlett <jamesbartlett@newrelic.com>
diff --git a/docs/go/core/BUILD.bazel b/docs/go/core/BUILD.bazel
index 61077c7..b43a6d9 100644
--- a/docs/go/core/BUILD.bazel
+++ b/docs/go/core/BUILD.bazel
@@ -12,6 +12,7 @@
     deps = [
         "//go/private:rpath",
         "//go/private/rules:binary",
+        "//go/private/rules:cross",
         "//go/private/rules:library",
         "//go/private/rules:library.bzl",
         "//go/private/rules:source",
diff --git a/docs/go/core/rules.bzl b/docs/go/core/rules.bzl
index 061ef2e..9db8e66 100644
--- a/docs/go/core/rules.bzl
+++ b/docs/go/core/rules.bzl
@@ -119,9 +119,11 @@
 load("//go/private/rules:test.bzl", _go_test = "go_test")
 load("//go/private/rules:source.bzl", _go_source = "go_source")
 load("//go/private/tools:path.bzl", _go_path = "go_path")
+load("//go/private/rules:cross.bzl", _go_cross_binary = "go_cross_binary")
 
 go_library = _go_library
 go_binary = _go_binary
 go_test = _go_test
 go_source = _go_source
 go_path = _go_path
+go_cross_binary = _go_cross_binary
diff --git a/docs/go/core/rules.md b/docs/go/core/rules.md
index dc10f68..bd76f80 100644
--- a/docs/go/core/rules.md
+++ b/docs/go/core/rules.md
@@ -177,6 +177,39 @@
 
 
 
+<a id="#go_cross_binary"></a>
+
+## go_cross_binary
+
+<pre>
+go_cross_binary(<a href="#go_cross_binary-name">name</a>, <a href="#go_cross_binary-platform">platform</a>, <a href="#go_cross_binary-sdk_version">sdk_version</a>, <a href="#go_cross_binary-target">target</a>)
+</pre>
+
+This wraps an executable built by `go_binary` to cross compile it
+    for a different platform, and/or compile it using a different version
+    of the golang SDK.<br><br>
+    **Providers:**
+    <ul>
+      <li>[GoLibrary]</li>
+      <li>[GoSource]</li>
+      <li>[GoArchive]</li>
+    </ul>
+    
+
+### **Attributes**
+
+
+| Name  | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :------------- | :------------- | :------------- |
+| <a id="go_cross_binary-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/docs/build-ref.html#name">Name</a> | required |  |
+| <a id="go_cross_binary-platform"></a>platform |  The platform to cross compile the <code>target</code> for.             If unspecified, the <code>target</code> will be compiled with the             same platform as it would've with the original <code>go_binary</code> rule.   | <a href="https://bazel.build/docs/build-ref.html#labels">Label</a> | optional | None |
+| <a id="go_cross_binary-sdk_version"></a>sdk_version |  The golang SDK version to use for compiling the <code>target</code>.             Supports specifying major, minor, and/or patch versions, eg. <code>"1"</code>,             <code>"1.17"</code>, or <code>"1.17.1"</code>. The first Go SDK provider installed in the             repo's workspace (via <code>go_download_sdk</code>, <code>go_wrap_sdk</code>, etc) that             matches the specified version will be used for compiling the given             <code>target</code>. If unspecified, the <code>target</code> will be compiled with the same             SDK as it would've with the original <code>go_binary</code> rule.             Transitions <code>target</code> by changing the <code>--@io_bazel_rules_go//go/toolchain:sdk_version</code>             build flag to the value provided for <code>sdk_version</code> here.   | String | optional | "" |
+| <a id="go_cross_binary-target"></a>target |  Go binary target to transition to the given platform and/or sdk_version.   | <a href="https://bazel.build/docs/build-ref.html#labels">Label</a> | required |  |
+
+
+
+
+
 <a id="#go_library"></a>
 
 ## go_library
diff --git a/go/def.bzl b/go/def.bzl
index 0fd4129..f5e52e2 100644
--- a/go/def.bzl
+++ b/go/def.bzl
@@ -74,6 +74,10 @@
     "//go/private/rules:nogo.bzl",
     _nogo = "nogo_wrapper",
 )
+load(
+    "//go/private/rules:cross.bzl",
+    _go_cross_binary = "go_cross_binary",
+)
 
 # TOOLS_NOGO is a list of all analysis passes in
 # golang.org/x/tools/go/analysis/passes.
@@ -164,6 +168,9 @@
 # See docs/go/core/rules.md#go_path for full documentation.
 go_path = _go_path
 
+# See docs/go/core/rules.md#go_cross_binary for full documentation.
+go_cross_binary = _go_cross_binary
+
 def go_vet_test(*args, **kwargs):
     fail("The go_vet_test rule has been removed. Please migrate to nogo instead, which supports vet tests.")
 
diff --git a/go/private/rules/BUILD.bazel b/go/private/rules/BUILD.bazel
index 2341744..2de381d 100644
--- a/go/private/rules/BUILD.bazel
+++ b/go/private/rules/BUILD.bazel
@@ -152,6 +152,19 @@
 )
 
 bzl_library(
+    name = "cross",
+    srcs = ["cross.bzl"],
+    visibility = [
+        "//docs:__subpackages__",
+        "//go:__subpackages__",
+    ],
+    deps = [
+        "//go/private:providers",
+        "//go/private/rules:transition",
+    ],
+)
+
+bzl_library(
     name = "wrappers",
     srcs = ["wrappers.bzl"],
     visibility = [
@@ -161,6 +174,7 @@
     deps = [
         "//go/private/rules:binary",
         "//go/private/rules:cgo",
+        "//go/private/rules:cross",
         "//go/private/rules:library",
         "//go/private/rules:test",
         "//go/private/rules:transition",
diff --git a/go/private/rules/cross.bzl b/go/private/rules/cross.bzl
new file mode 100644
index 0000000..d98e2f6
--- /dev/null
+++ b/go/private/rules/cross.bzl
@@ -0,0 +1,141 @@
+# Copyright 2022 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/rules:transition.bzl",
+    "go_cross_transition",
+)
+load(
+    "//go/private:providers.bzl",
+    "GoArchive",
+    "GoLibrary",
+    "GoSource",
+)
+
+def _is_windows(ctx):
+    return ctx.configuration.host_path_separator == ";"
+
+WINDOWS_ERR_SCRIPT = """
+@echo off
+>&2 echo {}
+exit /b 1
+"""
+UNIX_ERR_SCRIPT = """
+>&2 echo '{}'
+exit 1
+"""
+
+def _error_script(ctx):
+    errmsg = 'cannot run go_cross target "{}": underlying target "{}" is not executable'.format(
+        ctx.attr.name,
+        ctx.attr.target.label,
+    )
+    if _is_windows(ctx):
+        error_script = ctx.actions.declare_file("error.bat")
+        ctx.actions.write(error_script, WINDOWS_ERR_SCRIPT.format(errmsg), is_executable = True)
+        return error_script
+
+    error_script = ctx.actions.declare_file("error")
+    ctx.actions.write(error_script, UNIX_ERR_SCRIPT.format(errmsg), is_executable = True)
+    return error_script
+
+def _go_cross_impl(ctx):
+    old_default_info = ctx.attr.target[DefaultInfo]
+    old_executable = old_default_info.files_to_run.executable
+
+    new_default_info = None
+    if old_executable:
+        # Bazel requires executable rules to created the executable themselves,
+        # so we create a symlink in this rule so that it appears this rule created its executable.
+        new_executable = ctx.actions.declare_file(ctx.attr.name)
+        ctx.actions.symlink(output = new_executable, target_file = old_executable)
+        new_default_info = DefaultInfo(
+            files = depset([new_executable]),
+            runfiles = old_default_info.default_runfiles,
+            executable = new_executable,
+        )
+    else:
+        # There's no way to determine if the underlying `go_binary` target is executable at loading time
+        # so we must set the `go_cross` rule to be always executable. If the `go_binary` target is not
+        # executable, we set the `go_cross` executable to a simple script that prints an error message
+        # when executed. This way users can still run a `go_cross` target using `bazel run` as long as
+        # the underlying `go_binary` target is executable.
+        error_script = _error_script(ctx)
+
+        # See the implementation of `go_binary` for an explanation of the need for default vs data runfiles here.
+        new_default_info = DefaultInfo(
+            files = depset([error_script] + old_default_info.files.to_list()),
+            default_runfiles = old_default_info.default_runfiles,
+            data_runfiles = old_default_info.data_runfiles.merge(ctx.runfiles([error_script])),
+            executable = error_script,
+        )
+
+    providers = [
+        ctx.attr.target[provider]
+        for provider in [
+            GoLibrary,
+            GoSource,
+            GoArchive,
+            OutputGroupInfo,
+            CcInfo,
+        ]
+        if provider in ctx.attr.target
+    ]
+    return [new_default_info] + providers
+
+_go_cross_kwargs = {
+    "implementation": _go_cross_impl,
+    "attrs": {
+        "target": attr.label(
+            doc = """Go binary target to transition to the given platform and/or sdk_version.
+            """,
+            providers = [GoLibrary, GoSource, GoArchive],
+            mandatory = True,
+        ),
+        "platform": attr.label(
+            doc = """The platform to cross compile the `target` for.
+            If unspecified, the `target` will be compiled with the
+            same platform as it would've with the original `go_binary` rule.
+            """,
+        ),
+        "sdk_version": attr.string(
+            doc = """The golang SDK version to use for compiling the `target`.
+            Supports specifying major, minor, and/or patch versions, eg. `"1"`,
+            `"1.17"`, or `"1.17.1"`. The first Go SDK provider installed in the
+            repo's workspace (via `go_download_sdk`, `go_wrap_sdk`, etc) that
+            matches the specified version will be used for compiling the given
+            `target`. If unspecified, the `target` will be compiled with the same
+            SDK as it would've with the original `go_binary` rule.
+            Transitions `target` by changing the `--@io_bazel_rules_go//go/toolchain:sdk_version`
+            build flag to the value provided for `sdk_version` here.
+            """,
+        ),
+        "_allowlist_function_transition": attr.label(
+            default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
+        ),
+    },
+    "cfg": go_cross_transition,
+    "doc": """This wraps an executable built by `go_binary` to cross compile it
+    for a different platform, and/or compile it using a different version
+    of the golang SDK.<br><br>
+    **Providers:**
+    <ul>
+      <li>[GoLibrary]</li>
+      <li>[GoSource]</li>
+      <li>[GoArchive]</li>
+    </ul>
+    """,
+}
+
+go_cross_binary = rule(executable = True, **_go_cross_kwargs)
diff --git a/go/private/rules/transition.bzl b/go/private/rules/transition.bzl
index f567028..f07d024 100644
--- a/go/private/rules/transition.bzl
+++ b/go/private/rules/transition.bzl
@@ -400,3 +400,25 @@
         label = filter_transition_label("@io_bazel_rules_go//go/config:{}".format(name))
         settings[label] = value == "on"
     return value
+
+_SDK_VERSION_BUILD_SETTING = filter_transition_label("@io_bazel_rules_go//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,
+)
diff --git a/tests/core/cross/BUILD.bazel b/tests/core/cross/BUILD.bazel
index 9dfd683..04a7ba1 100644
--- a/tests/core/cross/BUILD.bazel
+++ b/tests/core/cross/BUILD.bazel
@@ -1,4 +1,4 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_cross_binary", "go_library", "go_test")
 load("@io_bazel_rules_go//go/tools/bazel_testing:def.bzl", "go_bazel_test")
 load(":def.bzl", "no_context_info")
 
@@ -33,6 +33,31 @@
     deps = [":platform_lib"],
 )
 
+go_binary(
+    name = "native_bin",
+    srcs = ["main.go"],
+    pure = "on",
+    deps = [":platform_lib"],
+)
+
+go_cross_binary(
+    name = "windows_go_cross",
+    platform = "@io_bazel_rules_go//go/toolchain:windows_amd64",
+    target = ":native_bin",
+)
+
+go_cross_binary(
+    name = "linux_go_cross",
+    platform = "@io_bazel_rules_go//go/toolchain:linux_amd64",
+    target = ":native_bin",
+)
+
+go_cross_binary(
+    name = "darwin_go_cross",
+    platform = "@io_bazel_rules_go//go/toolchain:darwin_amd64",
+    target = ":native_bin",
+)
+
 go_library(
     name = "platform_lib",
     srcs = select({
@@ -64,6 +89,27 @@
     deps = ["//go/tools/bazel:go_default_library"],
 )
 
+go_test(
+    name = "go_cross_binary_test",
+    size = "small",
+    srcs = ["cross_test.go"],
+    args = [
+        "-darwin",
+        "$(location :darwin_go_cross)",
+        "-linux",
+        "$(location :linux_go_cross)",
+        "-windows",
+        "$(location :windows_go_cross)",
+    ],
+    data = [
+        ":darwin_go_cross",
+        ":linux_go_cross",
+        ":windows_go_cross",
+    ],
+    rundir = ".",
+    deps = ["//go/tools/bazel:go_default_library"],
+)
+
 go_bazel_test(
     name = "ios_select_test",
     srcs = ["ios_select_test.go"],
@@ -74,6 +120,16 @@
     srcs = ["proto_test.go"],
 )
 
+go_bazel_test(
+    name = "sdk_version_test",
+    srcs = ["sdk_version_test.go"],
+)
+
+go_bazel_test(
+    name = "non_executable_test",
+    srcs = ["non_executable_test.go"],
+)
+
 no_context_info(
     name = "no_context_info",
 )
diff --git a/tests/core/cross/README.rst b/tests/core/cross/README.rst
index 38efea0..283eb26 100644
--- a/tests/core/cross/README.rst
+++ b/tests/core/cross/README.rst
@@ -3,6 +3,7 @@
 
 .. _go_binary: /docs/go/core/rules.md#go_binary
 .. _go_library: /docs/go/core/rules.md#go_library
+.. _go_cross_binary: /docs/go/core/rules.md#go_cross_binary
 .. _#2523: https://github.com/bazelbuild/rules_go/issues/2523
 
 Tests to ensure that cross compilation is working as expected.
@@ -30,6 +31,15 @@
 If the wrong source file is used or if all files are filtered out, the
 `go_binary`_ will not build.
 
+go_cross_test
+-------------
+
+Indentical test to ``cross_test`` except tests using a `go_cross_binary`_ rule wrapping a `go_binary`_ instead of the ``goos`` and ``goarch`` attributes on a `go_binary`_.
+
+sdk_version_test
+----------------
+Tests that a `go_binary`_ wrapped in a `go_cross_binary`_ rule, with the ``sdk_version`` attribute set, produces an executable built with the correct Go SDK version.
+
 ios_select_test
 ---------------
 
diff --git a/tests/core/cross/non_executable_test.go b/tests/core/cross/non_executable_test.go
new file mode 100644
index 0000000..6bdf732
--- /dev/null
+++ b/tests/core/cross/non_executable_test.go
@@ -0,0 +1,104 @@
+// Copyright 2022 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.
+
+package non_executable_test
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/bazelbuild/rules_go/go/tools/bazel_testing"
+)
+
+func TestMain(m *testing.M) {
+	bazel_testing.TestMain(m, bazel_testing.Args{
+		Main: `
+-- src/BUILD.bazel --
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_cross_binary")
+load(":rules.bzl", "no_runfiles_check")
+
+go_binary(
+    name = "archive",
+    srcs = ["archive.go"],
+    cgo = True,
+    linkmode = "c-archive",
+)
+
+# We make a new platform here so that we can exercise the go_cross_binary rule.
+# However, the test needs to run on all hosts, so the platform needs
+# to inherit from the host platform.
+platform(
+  name = "host_cgo",
+  parents = ["@local_config_platform//:host"],
+  constraint_values = [
+    "@io_bazel_rules_go//go/toolchain:cgo_on",
+  ],
+)
+
+go_cross_binary(
+    name = "host_archive",
+    target = ":archive",
+    platform = ":host_cgo",
+)
+
+cc_binary(
+    name = "main",
+    srcs = ["main.c"],
+    deps = [":host_archive"],
+)
+
+no_runfiles_check(
+    name = "no_runfiles",
+    target = ":main",
+)
+-- src/archive.go --
+package main
+
+import "C"
+
+func main() {}
+-- src/main.c --
+int main() {}
+-- src/rules.bzl --
+def _no_runfiles_check_impl(ctx):
+    runfiles = ctx.attr.target[DefaultInfo].default_runfiles.files.to_list()
+    for runfile in runfiles:
+        if runfile.short_path not in ["src/main", "src/main.exe"]:
+            fail("Unexpected runfile: %s" % runfile.short_path)
+
+no_runfiles_check = rule(
+    implementation = _no_runfiles_check_impl,
+    attrs = {
+        "target": attr.label(),
+    }
+)
+`,
+	})
+}
+
+func TestNonExecutableGoBinaryCantBeRun(t *testing.T) {
+	if err := bazel_testing.RunBazel("build", "//src:host_archive"); err != nil {
+		t.Fatal(err)
+	}
+	err := bazel_testing.RunBazel("run", "//src:host_archive")
+	if err == nil || !strings.Contains(err.Error(), "cannot run go_cross target \"host_archive\": underlying target \"//src:archive\" is not executable") {
+		t.Errorf("Expected bazel run to fail due to //src:host_archive not being executable")
+	}
+}
+
+func TestNonExecutableGoBinaryNotInRunfiles(t *testing.T) {
+	if err := bazel_testing.RunBazel("build", "//src:no_runfiles"); err != nil {
+		t.Fatal(err)
+	}
+}
diff --git a/tests/core/cross/sdk_version_test.go b/tests/core/cross/sdk_version_test.go
new file mode 100644
index 0000000..9cb7ad5
--- /dev/null
+++ b/tests/core/cross/sdk_version_test.go
@@ -0,0 +1,135 @@
+// Copyright 2022 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.
+
+package go_download_sdk_test
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+	"text/template"
+
+	"github.com/bazelbuild/rules_go/go/tools/bazel_testing"
+)
+
+type testcase struct {
+	Name, SDKVersion, expectedVersion string
+}
+
+var testCases = []testcase{
+	{
+		Name:            "major_version",
+		SDKVersion:      "1",
+		expectedVersion: "go1.16",
+	},
+	{
+		Name:            "minor_version",
+		SDKVersion:      "1.16",
+		expectedVersion: "go1.16",
+	},
+	{
+		Name:            "patch_version",
+		SDKVersion:      "1.16.0",
+		expectedVersion: "go1.16",
+	},
+	{
+		Name:            "1_17_minor_version",
+		SDKVersion:      "1.17",
+		expectedVersion: "go1.17",
+	},
+	{
+		Name:            "1_17_patch_version",
+		SDKVersion:      "1.17.1",
+		expectedVersion: "go1.17.1",
+	},
+}
+
+func TestMain(m *testing.M) {
+	mainFilesTmpl := template.Must(template.New("").Parse(`
+-- WORKSPACE --
+local_repository(
+    name = "io_bazel_rules_go",
+    path = "../io_bazel_rules_go",
+)
+
+load("@io_bazel_rules_go//go:deps.bzl", "go_download_sdk", "go_rules_dependencies", "go_register_toolchains")
+
+go_rules_dependencies()
+
+go_download_sdk(
+    name = "go_sdk",
+    version = "1.16",
+)
+go_download_sdk(
+    name = "go_sdk_1_17",
+    version = "1.17",
+)
+go_download_sdk(
+    name = "go_sdk_1_17_1",
+    version = "1.17.1",
+)
+go_register_toolchains()
+-- main.go --
+package main
+
+import (
+  "fmt"
+	"runtime"
+)
+
+func main() {
+  fmt.Print(runtime.Version())
+}
+-- BUILD.bazel --
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_cross_binary")
+
+go_binary(
+  name = "print_version",
+  srcs = ["main.go"],
+)
+{{range .TestCases}}
+go_cross_binary(
+  name = "{{.Name}}",
+  target = ":print_version",
+  sdk_version = "{{.SDKVersion}}",
+)
+{{end}}
+`))
+  tmplValues := struct{
+    TestCases []testcase
+  }{
+    TestCases: testCases,
+  }
+  mainFilesBuilder := &strings.Builder{}
+  if err := mainFilesTmpl.Execute(mainFilesBuilder, tmplValues); err != nil {
+    panic(err)
+  }
+
+  bazel_testing.TestMain(m, bazel_testing.Args{Main: mainFilesBuilder.String()})
+}
+
+func Test(t *testing.T) {
+	for _, test := range testCases {
+		t.Run(test.Name, func(t *testing.T) {
+			output, err := bazel_testing.BazelOutput("run", fmt.Sprintf("//:%s", test.Name))
+			if err != nil {
+				t.Fatal(err)
+			}
+			actualVersion := string(output)
+			if actualVersion != test.expectedVersion {
+				t.Fatal("actual", actualVersion, "vs expected", test.expectedVersion)
+			}
+		})
+	}
+}