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)
+ }
+ })
+ }
+}