Skip to content

Commit e64c871

Browse files
Bryan C. Millsgopherbot
Bryan C. Mills
authored andcommitted
cmd/go: run tests when cmd/go is cross-compiled
When the GOOS or GOARCH of the cmd/go test binary does not match the GOOS or GOARCH of the installed 'go' binary itself, the test currently attempts to trick 'go test' into thinking that there were no test functions to run. That makes it very difficult to discover how to actually run the tests, which in turn makes it difficult to diagnose and fix regressions in, say, the linux-386-longtest builders. (We have had a few of those lately, and they shouldn't be as much of an ordeal to fix as they currently are.) There are three underlying problems: 1. cmd/go uses its own GOOS and GOARCH to figure out which variant of other tools to use, and the cache keys for all installed tools and libraries include the IDs of the tools used to build them. So when cmd/go's GOARCH changes, all installed tools and binaries appear stale *even if* they were just installed by invoking the native cmd/go with the appropriate GOARCH value set. 2. The "go/build" library used by cmd/go toggles its default CGO_ENABLED behavior depending on whether the GOOS and GOARCH being imported match runtime.GOOS and runtime.GOARCH. 3. A handful of cmd/go tests explicitly use gccgo, but the user's installed gccgo binary cannot necessarily cross-compile to the same platforms as cmd/go. To address the cache-invalidation problem, we modify the test variant of cmd/go to use the host's native toolchain (as indicated by the new TESTGO_GOHOSTOS and TESTGO_GOHOSTARCH environment variables) instead of the toolchain matching the test binary itself. That allows a test cmd/go binary compiled with GOARCH=386 to use libraries and tools cross-compiled by the native toolchain, so that $ GOARCH=386 go install std cmd suffices to make the packages in std and cmd non-stale in the tests. To address the CGO_ENABLED mismatch, we set CGO_ENABLED explicitly in the test's environment whenever it may differ from the default. Since script tests that use cgo are already expected to use a [cgo] condition, setting the environment to match that condition fixes the cgo-specific tests. To address the gccgo-specific cross-compilation failures, we add a new script condition, [cross], which evaluates to true whenever the platform of the test binary differs from that of the native toolchain. We can then use that condition to explicitly skip the handful of gccgo tests that fail under cross-compilation. Fixes #53936. Change-Id: I8633944f674eb5941ccc95df928991660e7e8137 Reviewed-on: https://go-review.googlesource.com/c/go/+/356611 Run-TryBot: Bryan Mills <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Auto-Submit: Bryan Mills <[email protected]> Reviewed-by: Russ Cox <[email protected]>
1 parent d8f90ce commit e64c871

File tree

17 files changed

+120
-76
lines changed

17 files changed

+120
-76
lines changed

src/cmd/go/go_test.go

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ var (
5555
fuzzInstrumented = false // whether fuzzing uses instrumentation
5656
)
5757

58+
var (
59+
goHostOS, goHostArch string
60+
cgoEnabled string // raw value from 'go env CGO_ENABLED'
61+
)
62+
5863
var exeSuffix string = func() string {
5964
if runtime.GOOS == "windows" {
6065
return ".exe"
@@ -96,6 +101,8 @@ func TestMain(m *testing.M) {
96101
// run the main func exported via export_test.go, and exit.
97102
// We set CMDGO_TEST_RUN_MAIN via os.Setenv and testScript.setup.
98103
if os.Getenv("CMDGO_TEST_RUN_MAIN") != "" {
104+
cfg.SetGOROOT(cfg.GOROOT, true)
105+
99106
if v := os.Getenv("TESTGO_VERSION"); v != "" {
100107
work.RuntimeVersion = v
101108
}
@@ -204,13 +211,16 @@ func TestMain(m *testing.M) {
204211
// which will cause many tests to do unnecessary rebuilds and some
205212
// tests to attempt to overwrite the installed standard library.
206213
// Bail out entirely in this case.
207-
hostGOOS := goEnv("GOHOSTOS")
208-
hostGOARCH := goEnv("GOHOSTARCH")
209-
if hostGOOS != runtime.GOOS || hostGOARCH != runtime.GOARCH {
210-
fmt.Fprintf(os.Stderr, "testing: warning: no tests to run\n") // magic string for cmd/go
211-
fmt.Printf("cmd/go test is not compatible with GOOS/GOARCH != GOHOSTOS/GOHOSTARCH (%s/%s != %s/%s)\n", runtime.GOOS, runtime.GOARCH, hostGOOS, hostGOARCH)
212-
fmt.Printf("SKIP\n")
213-
return
214+
goHostOS = goEnv("GOHOSTOS")
215+
os.Setenv("TESTGO_GOHOSTOS", goHostOS)
216+
goHostArch = goEnv("GOHOSTARCH")
217+
os.Setenv("TESTGO_GOHOSTARCH", goHostArch)
218+
219+
cgoEnabled = goEnv("CGO_ENABLED")
220+
canCgo, err = strconv.ParseBool(cgoEnabled)
221+
if err != nil {
222+
fmt.Fprintf(os.Stderr, "can't parse go env CGO_ENABLED output: %q\n", strings.TrimSpace(cgoEnabled))
223+
os.Exit(2)
214224
}
215225

216226
// Duplicate the test executable into the path at testGo, for $PATH.
@@ -241,18 +251,6 @@ func TestMain(m *testing.M) {
241251
}
242252
}
243253

244-
cmd := exec.Command(testGo, "env", "CGO_ENABLED")
245-
cmd.Stderr = new(strings.Builder)
246-
if out, err := cmd.Output(); err != nil {
247-
fmt.Fprintf(os.Stderr, "running testgo failed: %v\n%s", err, cmd.Stderr)
248-
os.Exit(2)
249-
} else {
250-
canCgo, err = strconv.ParseBool(strings.TrimSpace(string(out)))
251-
if err != nil {
252-
fmt.Fprintf(os.Stderr, "can't parse go env CGO_ENABLED output: %v\n", strings.TrimSpace(string(out)))
253-
}
254-
}
255-
256254
out, err := exec.Command(gotool, "env", "GOCACHE").CombinedOutput()
257255
if err != nil {
258256
fmt.Fprintf(os.Stderr, "could not find testing GOCACHE: %v\n%s", err, out)
@@ -272,6 +270,7 @@ func TestMain(m *testing.M) {
272270
canFuzz = sys.FuzzSupported(runtime.GOOS, runtime.GOARCH)
273271
fuzzInstrumented = sys.FuzzInstrumented(runtime.GOOS, runtime.GOARCH)
274272
}
273+
275274
// Don't let these environment variables confuse the test.
276275
os.Setenv("GOENV", "off")
277276
os.Unsetenv("GOFLAGS")
@@ -886,7 +885,7 @@ func TestNewReleaseRebuildsStalePackagesInGOPATH(t *testing.T) {
886885
"src/math/bits",
887886
"src/unsafe",
888887
filepath.Join("pkg", runtime.GOOS+"_"+runtime.GOARCH),
889-
filepath.Join("pkg/tool", runtime.GOOS+"_"+runtime.GOARCH),
888+
filepath.Join("pkg/tool", goHostOS+"_"+goHostArch),
890889
"pkg/include",
891890
} {
892891
srcdir := filepath.Join(testGOROOT, copydir)
@@ -2377,6 +2376,8 @@ func TestIssue22588(t *testing.T) {
23772376
defer tg.cleanup()
23782377
tg.parallel()
23792378

2379+
tg.wantNotStale("runtime", "", "must be non-stale to compare staleness under -toolexec")
2380+
23802381
if _, err := os.Stat("/usr/bin/time"); err != nil {
23812382
t.Skip(err)
23822383
}

src/cmd/go/internal/base/tool.go

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,14 @@ import (
99
"go/build"
1010
"os"
1111
"path/filepath"
12-
"runtime"
1312

1413
"cmd/go/internal/cfg"
1514
)
1615

17-
// Configuration for finding tool binaries.
18-
var (
19-
ToolGOOS = runtime.GOOS
20-
ToolGOARCH = runtime.GOARCH
21-
ToolIsWindows = ToolGOOS == "windows"
22-
ToolDir = build.ToolDir
23-
)
24-
25-
const ToolWindowsExtension = ".exe"
26-
2716
// Tool returns the path to the named tool (for example, "vet").
2817
// If the tool cannot be found, Tool exits the process.
2918
func Tool(toolName string) string {
30-
toolPath := filepath.Join(ToolDir, toolName)
31-
if ToolIsWindows {
32-
toolPath += ToolWindowsExtension
33-
}
19+
toolPath := filepath.Join(build.ToolDir, toolName) + cfg.ToolExeSuffix()
3420
if len(cfg.BuildToolexec) > 0 {
3521
return toolPath
3622
}

src/cmd/go/internal/cfg/cfg.go

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,25 @@ func exeSuffix() string {
4242
return ""
4343
}
4444

45+
// Configuration for tools installed to GOROOT/bin.
46+
// Normally these match runtime.GOOS and runtime.GOARCH,
47+
// but when testing a cross-compiled cmd/go they will
48+
// indicate the GOOS and GOARCH of the installed cmd/go
49+
// rather than the test binary.
50+
var (
51+
installedGOOS string
52+
installedGOARCH string
53+
)
54+
55+
// ToolExeSuffix returns the suffix for executables installed
56+
// in build.ToolDir.
57+
func ToolExeSuffix() string {
58+
if installedGOOS == "windows" {
59+
return ".exe"
60+
}
61+
return ""
62+
}
63+
4564
// These are general "build flags" used by build and other commands.
4665
var (
4766
BuildA bool // -a flag
@@ -141,12 +160,17 @@ func defaultContext() build.Context {
141160
}
142161

143162
func init() {
144-
SetGOROOT(findGOROOT())
163+
SetGOROOT(findGOROOT(), false)
145164
BuildToolchainCompiler = func() string { return "missing-compiler" }
146165
BuildToolchainLinker = func() string { return "missing-linker" }
147166
}
148167

149-
func SetGOROOT(goroot string) {
168+
// SetGOROOT sets GOROOT and associated variables to the given values.
169+
//
170+
// If isTestGo is true, build.ToolDir is set based on the TESTGO_GOHOSTOS and
171+
// TESTGO_GOHOSTARCH environment variables instead of runtime.GOOS and
172+
// runtime.GOARCH.
173+
func SetGOROOT(goroot string, isTestGo bool) {
150174
BuildContext.GOROOT = goroot
151175

152176
GOROOT = goroot
@@ -161,13 +185,33 @@ func SetGOROOT(goroot string) {
161185
}
162186
GOROOT_FINAL = findGOROOT_FINAL(goroot)
163187

164-
if runtime.Compiler != "gccgo" && goroot != "" {
165-
// Note that we must use runtime.GOOS and runtime.GOARCH here,
166-
// as the tool directory does not move based on environment
167-
// variables. This matches the initialization of ToolDir in
168-
// go/build, except for using BuildContext.GOROOT rather than
169-
// runtime.GOROOT.
170-
build.ToolDir = filepath.Join(goroot, "pkg/tool/"+runtime.GOOS+"_"+runtime.GOARCH)
188+
installedGOOS = runtime.GOOS
189+
installedGOARCH = runtime.GOARCH
190+
if isTestGo {
191+
if testOS := os.Getenv("TESTGO_GOHOSTOS"); testOS != "" {
192+
installedGOOS = testOS
193+
}
194+
if testArch := os.Getenv("TESTGO_GOHOSTARCH"); testArch != "" {
195+
installedGOARCH = testArch
196+
}
197+
}
198+
199+
if runtime.Compiler != "gccgo" {
200+
if goroot == "" {
201+
build.ToolDir = ""
202+
} else {
203+
// Note that we must use the installed OS and arch here: the tool
204+
// directory does not move based on environment variables, and even if we
205+
// are testing a cross-compiled cmd/go all of the installed packages and
206+
// tools would have been built using the native compiler and linker (and
207+
// would spuriously appear stale if we used a cross-compiled compiler and
208+
// linker).
209+
//
210+
// This matches the initialization of ToolDir in go/build, except for
211+
// using ctxt.GOROOT and the installed GOOS and GOARCH rather than the
212+
// GOROOT, GOOS, and GOARCH reported by the runtime package.
213+
build.ToolDir = filepath.Join(GOROOTpkg, "tool", installedGOOS+"_"+installedGOARCH)
214+
}
171215
}
172216
}
173217

src/cmd/go/internal/clean/clean.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"io"
1212
"os"
1313
"path/filepath"
14+
"runtime"
1415
"strconv"
1516
"strings"
1617
"time"
@@ -395,7 +396,7 @@ func removeFile(f string) {
395396
return
396397
}
397398
// Windows does not allow deletion of a binary file while it is executing.
398-
if base.ToolIsWindows {
399+
if runtime.GOOS == "windows" {
399400
// Remove lingering ~ file from last attempt.
400401
if _, err2 := os.Stat(f + "~"); err2 == nil {
401402
os.Remove(f + "~")

src/cmd/go/internal/envcmd/env.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func MkEnv() []cfg.EnvVar {
9696
{Name: "GOROOT", Value: cfg.GOROOT},
9797
{Name: "GOSUMDB", Value: cfg.GOSUMDB},
9898
{Name: "GOTMPDIR", Value: cfg.Getenv("GOTMPDIR")},
99-
{Name: "GOTOOLDIR", Value: base.ToolDir},
99+
{Name: "GOTOOLDIR", Value: build.ToolDir},
100100
{Name: "GOVCS", Value: cfg.GOVCS},
101101
{Name: "GOVERSION", Value: runtime.Version()},
102102
}

src/cmd/go/internal/fmtcmd/fmt.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,7 @@ func runFmt(ctx context.Context, cmd *base.Command, args []string) {
9797
}
9898

9999
func gofmtPath() string {
100-
gofmt := "gofmt"
101-
if base.ToolIsWindows {
102-
gofmt += base.ToolWindowsExtension
103-
}
100+
gofmt := "gofmt" + cfg.ToolExeSuffix()
104101

105102
gofmtPath := filepath.Join(cfg.GOBIN, gofmt)
106103
if _, err := os.Stat(gofmtPath); err == nil {

src/cmd/go/internal/load/pkg.go

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"io/fs"
1818
"os"
1919
"os/exec"
20-
"path"
2120
pathpkg "path"
2221
"path/filepath"
2322
"runtime"
@@ -1776,9 +1775,9 @@ func (p *Package) load(ctx context.Context, opts PackageOpts, path string, stk *
17761775
setError(e)
17771776
return
17781777
}
1779-
elem := p.DefaultExecName()
1780-
full := cfg.BuildContext.GOOS + "_" + cfg.BuildContext.GOARCH + "/" + elem
1781-
if cfg.BuildContext.GOOS != base.ToolGOOS || cfg.BuildContext.GOARCH != base.ToolGOARCH {
1778+
elem := p.DefaultExecName() + cfg.ExeSuffix
1779+
full := cfg.BuildContext.GOOS + "_" + cfg.BuildContext.GOARCH + string(filepath.Separator) + elem
1780+
if cfg.BuildContext.GOOS != runtime.GOOS || cfg.BuildContext.GOARCH != runtime.GOARCH {
17821781
// Install cross-compiled binaries to subdirectories of bin.
17831782
elem = full
17841783
}
@@ -1788,7 +1787,7 @@ func (p *Package) load(ctx context.Context, opts PackageOpts, path string, stk *
17881787
if p.Internal.Build.BinDir != "" {
17891788
// Install to GOBIN or bin of GOPATH entry.
17901789
p.Target = filepath.Join(p.Internal.Build.BinDir, elem)
1791-
if !p.Goroot && strings.Contains(elem, "/") && cfg.GOBIN != "" {
1790+
if !p.Goroot && strings.Contains(elem, string(filepath.Separator)) && cfg.GOBIN != "" {
17921791
// Do not create $GOBIN/goos_goarch/elem.
17931792
p.Target = ""
17941793
p.Internal.GobinSubdir = true
@@ -1798,14 +1797,11 @@ func (p *Package) load(ctx context.Context, opts PackageOpts, path string, stk *
17981797
// This is for 'go tool'.
17991798
// Override all the usual logic and force it into the tool directory.
18001799
if cfg.BuildToolchainName == "gccgo" {
1801-
p.Target = filepath.Join(base.ToolDir, elem)
1800+
p.Target = filepath.Join(build.ToolDir, elem)
18021801
} else {
18031802
p.Target = filepath.Join(cfg.GOROOTpkg, "tool", full)
18041803
}
18051804
}
1806-
if p.Target != "" && cfg.BuildContext.GOOS == "windows" {
1807-
p.Target += ".exe"
1808-
}
18091805
} else if p.Internal.Local {
18101806
// Local import turned into absolute path.
18111807
// No permanent install target.
@@ -2071,7 +2067,7 @@ func resolveEmbed(pkgdir string, patterns []string) (files []string, pmap map[st
20712067
glob = pattern[len("all:"):]
20722068
}
20732069
// Check pattern is valid for //go:embed.
2074-
if _, err := path.Match(glob, ""); err != nil || !validEmbedPattern(glob) {
2070+
if _, err := pathpkg.Match(glob, ""); err != nil || !validEmbedPattern(glob) {
20752071
return nil, nil, fmt.Errorf("invalid pattern syntax")
20762072
}
20772073

@@ -3112,7 +3108,7 @@ func PackagesAndErrorsOutsideModule(ctx context.Context, opts PackageOpts, args
31123108
return nil, fmt.Errorf("%s: argument must be a package path, not an absolute path", arg)
31133109
case search.IsMetaPackage(p):
31143110
return nil, fmt.Errorf("%s: argument must be a package path, not a meta-package", arg)
3115-
case path.Clean(p) != p:
3111+
case pathpkg.Clean(p) != p:
31163112
return nil, fmt.Errorf("%s: argument must be a clean package path", arg)
31173113
case !strings.Contains(p, "...") && search.IsStandardImportPath(p) && modindex.IsStandardPackage(cfg.GOROOT, cfg.BuildContext.Compiler, p):
31183114
return nil, fmt.Errorf("%s: argument must not be a package in the standard library", arg)

src/cmd/go/internal/test/flagdefs_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
)
1616

1717
func TestMain(m *testing.M) {
18-
cfg.SetGOROOT(testenv.GOROOT(nil))
18+
cfg.SetGOROOT(testenv.GOROOT(nil), false)
1919
}
2020

2121
func TestPassFlagToTestIncludesAllTestFlags(t *testing.T) {

src/cmd/go/internal/tool/tool.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package tool
88
import (
99
"context"
1010
"fmt"
11+
"go/build"
1112
"os"
1213
"os/exec"
1314
"os/signal"
@@ -115,7 +116,7 @@ func runTool(ctx context.Context, cmd *base.Command, args []string) {
115116

116117
// listTools prints a list of the available tools in the tools directory.
117118
func listTools() {
118-
f, err := os.Open(base.ToolDir)
119+
f, err := os.Open(build.ToolDir)
119120
if err != nil {
120121
fmt.Fprintf(os.Stderr, "go: no tool directory: %s\n", err)
121122
base.SetExitStatus(2)
@@ -132,11 +133,9 @@ func listTools() {
132133
sort.Strings(names)
133134
for _, name := range names {
134135
// Unify presentation by going to lower case.
135-
name = strings.ToLower(name)
136136
// If it's windows, don't show the .exe suffix.
137-
if base.ToolIsWindows && strings.HasSuffix(name, base.ToolWindowsExtension) {
138-
name = name[:len(name)-len(base.ToolWindowsExtension)]
139-
}
137+
name = strings.TrimSuffix(strings.ToLower(name), cfg.ToolExeSuffix())
138+
140139
// The tool directory used by gccgo will have other binaries
141140
// in addition to go tools. Only display go tools here.
142141
if cfg.BuildToolchainName == "gccgo" && !isGccgoTool(name) {

src/cmd/go/internal/work/exec.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1813,15 +1813,15 @@ func (b *Builder) copyFile(dst, src string, perm fs.FileMode, force bool) error
18131813
}
18141814

18151815
// On Windows, remove lingering ~ file from last attempt.
1816-
if base.ToolIsWindows {
1816+
if runtime.GOOS == "windows" {
18171817
if _, err := os.Stat(dst + "~"); err == nil {
18181818
os.Remove(dst + "~")
18191819
}
18201820
}
18211821

18221822
mayberemovefile(dst)
18231823
df, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
1824-
if err != nil && base.ToolIsWindows {
1824+
if err != nil && runtime.GOOS == "windows" {
18251825
// Windows does not allow deletion of a binary file
18261826
// while it is executing. Try to move it out of the way.
18271827
// If the move fails, which is likely, we'll try again the

0 commit comments

Comments
 (0)