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