Create empty `.go` file in a random location (#3566)
Isolating the empty `.go` files generated for targets without sources
and removing them after a build turned out not to solve issues with
concurrent unsandboxed builds causing races. Instead, just generate the
file in a temporary location, which for a truly empty file does not
result in (non-hermetic) source file paths being included in the
archive.
Along the way fix a potential source of non-hermeticity in
`go_bazel_test`.
diff --git a/go/tools/bazel_testing/bazel_testing.go b/go/tools/bazel_testing/bazel_testing.go
index aa2d29e..4543140 100644
--- a/go/tools/bazel_testing/bazel_testing.go
+++ b/go/tools/bazel_testing/bazel_testing.go
@@ -95,9 +95,9 @@
// TestMain should be called by tests using this framework from a function named
// "TestMain". For example:
//
-// func TestMain(m *testing.M) {
-// os.Exit(bazel_testing.TestMain(m, bazel_testing.Args{...}))
-// }
+// func TestMain(m *testing.M) {
+// os.Exit(bazel_testing.TestMain(m, bazel_testing.Args{...}))
+// }
//
// TestMain constructs a set of workspaces and changes the working directory to
// the main workspace.
@@ -165,7 +165,11 @@
func BazelCmd(args ...string) *exec.Cmd {
cmd := exec.Command("bazel")
if outputUserRoot != "" {
- cmd.Args = append(cmd.Args, "--output_user_root="+outputUserRoot)
+ cmd.Args = append(cmd.Args,
+ "--output_user_root="+outputUserRoot,
+ "--nosystem_rc",
+ "--nohome_rc",
+ )
}
cmd.Args = append(cmd.Args, args...)
for _, e := range os.Environ() {
diff --git a/go/tools/builders/compilepkg.go b/go/tools/builders/compilepkg.go
index 92b3dfd..6e21ca2 100644
--- a/go/tools/builders/compilepkg.go
+++ b/go/tools/builders/compilepkg.go
@@ -206,33 +206,31 @@
nogoSrcsOrigin := make(map[string]string)
if len(srcs.goSrcs) == 0 {
- // We need to run the compiler to create a valid archive, even if there's
- // nothing in it. GoPack will complain if we try to add assembly or cgo
- // objects.
- //
- // _empty.go needs to be in a deterministic location (not tmpdir) in order
- // to ensure deterministic output. The location also needs to be unique
- // otherwise platforms without sandbox support may race to create/remove
- // the file during parallel compilation.
- emptyDir := filepath.Join(filepath.Dir(outPath), sanitizePathForIdentifier(importPath))
- if err := os.Mkdir(emptyDir, 0o700); err != nil {
- return fmt.Errorf("could not create directory for _empty.go: %v", err)
+ // We need to run the compiler to create a valid archive, even if there's nothing in it.
+ // Otherwise, GoPack will complain if we try to add assembly or cgo objects.
+ // A truly empty archive does not include any references to source file paths, which
+ // ensures hermeticity even though the temp file path is random.
+ emptyGoFile, err := os.CreateTemp(filepath.Dir(outPath), "*.go")
+ if err != nil {
+ return err
}
- defer os.RemoveAll(emptyDir)
-
- emptyPath := filepath.Join(emptyDir, "_empty.go")
- if err := os.WriteFile(emptyPath, []byte("package empty\n"), 0o666); err != nil {
+ defer os.Remove(emptyGoFile.Name())
+ defer emptyGoFile.Close()
+ if _, err := emptyGoFile.WriteString("package empty\n"); err != nil {
+ return err
+ }
+ if err := emptyGoFile.Close(); err != nil {
return err
}
srcs.goSrcs = append(srcs.goSrcs, fileInfo{
- filename: emptyPath,
+ filename: emptyGoFile.Name(),
ext: goExt,
matched: true,
pkg: "empty",
})
- nogoSrcsOrigin[emptyPath] = ""
+ nogoSrcsOrigin[emptyGoFile.Name()] = ""
}
packageName := srcs.goSrcs[0].pkg
var goSrcs, cgoSrcs []string
diff --git a/tests/core/go_library/BUILD.bazel b/tests/core/go_library/BUILD.bazel
index 56ec563..25293cb 100644
--- a/tests/core/go_library/BUILD.bazel
+++ b/tests/core/go_library/BUILD.bazel
@@ -155,3 +155,14 @@
srcs = ["embedsrcs_simple_test.go"],
embedsrcs = ["embedsrcs_static/no"],
)
+
+go_bazel_test(
+ name = "no_srcs_test",
+ size = "medium",
+ srcs = ["no_srcs_test.go"],
+)
+
+go_library(
+ name = "no_srcs_lib",
+ importpath = "github.com/bazelbuild/rules_go/tests/core/no_srcs_lib",
+)
diff --git a/tests/core/go_library/README.rst b/tests/core/go_library/README.rst
index 1679904..4410902 100644
--- a/tests/core/go_library/README.rst
+++ b/tests/core/go_library/README.rst
@@ -2,10 +2,11 @@
==============================
.. _go_library: /docs/go/core/rules.md#_go_library
-.. #1262: https://github.com/bazelbuild/rules_go/issues/1262
-.. #1520: https://github.com/bazelbuild/rules_go/issues/1520
-.. #1772: https://github.com/bazelbuild/rules_go/issues/1772
-.. #2058: https://github.com/bazelbuild/rules_go/issues/2058
+.. _#1262: https://github.com/bazelbuild/rules_go/issues/1262
+.. _#1520: https://github.com/bazelbuild/rules_go/issues/1520
+.. _#1772: https://github.com/bazelbuild/rules_go/issues/1772
+.. _#2058: https://github.com/bazelbuild/rules_go/issues/2058
+.. _#3558: https://github.com/bazelbuild/rules_go/issues/3558
empty
-----
@@ -48,3 +49,9 @@
--------------------
Verifies common errors with ``//go:embed`` directives are correctly reported.
+
+no_srcs_test
+------------
+
+Verifies that `go_library`_ targets without Go source files build concurrently,
+even unsandboxed, and reproducibly. Verifies `#3558`_.
\ No newline at end of file
diff --git a/tests/core/go_library/no_srcs_test.go b/tests/core/go_library/no_srcs_test.go
new file mode 100644
index 0000000..8e6bebc
--- /dev/null
+++ b/tests/core/go_library/no_srcs_test.go
@@ -0,0 +1,78 @@
+// Copyright 2021 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 no_srcs
+
+import (
+ "bytes"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/bazelbuild/rules_go/go/tools/bazel_testing"
+)
+
+func TestMain(m *testing.M) {
+ bazel_testing.TestMain(m, bazel_testing.Args{
+ Main: `
+-- BUILD.bazel --
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+[
+ go_library(
+ name = "lib_" + str(i),
+ srcs = [],
+ importpath = "example.com/some/path",
+ )
+ for i in range(1000)
+]
+`,
+ })
+}
+
+func Test(t *testing.T) {
+ commonArgs := []string{
+ "--spawn_strategy=local",
+ "--compilation_mode=dbg",
+ }
+
+ if err := bazel_testing.RunBazel(append([]string{"build", "//..."}, commonArgs...)...); err != nil {
+ t.Fatal(err)
+ }
+
+ out, err := bazel_testing.BazelOutput(append([]string{"cquery", "--output=files", "//..."}, commonArgs...)...)
+ if err != nil {
+ t.Fatal(err)
+ }
+ archives := strings.Split(strings.TrimSpace(string(out)), "\n")
+
+ if len(archives) != 1000 {
+ t.Fatalf("expected 1000 archives, got %d", len(archives))
+ }
+
+ referenceContent, err := os.ReadFile(archives[0])
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, archive := range archives {
+ content, err := os.ReadFile(archive)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(content, referenceContent) {
+ t.Fatalf("expected all archives to be identical, got:\n\n%s\n\n%s\n", string(content), string(referenceContent))
+ }
+ }
+}