Skip to content

Commit 2f2f8fe

Browse files
committed
cmd/go: change go tool to build tools missing from GOROOT/pkg/tool
If a tool in cmd is not installed in $GOROOT/pkg/tool/${GOOS}_${GOARCH}, go tool will build (if it's not cached) and run it in a similar way (with some changes) to how tools declared with tool directives are built and run. The main change in how builtin tools are run as compared to mod tools is that they are built "in host mode" using the running go command's GOOS and GOARCH. The "-exec" flag is also ignored and we don't add GOROOT/bin to the PATH. A ForceHost function has been added to the cfg package to force the configuration to runtime.GOOS/runtime.GOARCH. It has to recompute the BuildContext because it's normally determined at init time but we're changing it after we realize we're running a builtin tool. (Detecting that we're running a builtin tool at init time would mean replicating the cmd line parsing logic so recomputing BuildContext sounds like the smaller change.) For #71867 Change-Id: I3b2edf2cb985c1dcf5f845fbf39b7dc11dea4df7 Reviewed-on: https://go-review.googlesource.com/c/go/+/666476 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Michael Pratt <[email protected]> Reviewed-by: Michael Matloob <[email protected]>
1 parent d73aa53 commit 2f2f8fe

File tree

7 files changed

+193
-64
lines changed

7 files changed

+193
-64
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func Tool(toolName string) string {
3030
// ToolPath returns the path at which we expect to find the named tool
3131
// (for example, "vet"), and the error (if any) from statting that path.
3232
func ToolPath(toolName string) (string, error) {
33-
if !validToolName(toolName) {
33+
if !ValidToolName(toolName) {
3434
return "", fmt.Errorf("bad tool name: %q", toolName)
3535
}
3636
toolPath := filepath.Join(build.ToolDir, toolName) + cfg.ToolExeSuffix()
@@ -41,7 +41,7 @@ func ToolPath(toolName string) (string, error) {
4141
return toolPath, err
4242
}
4343

44-
func validToolName(toolName string) bool {
44+
func ValidToolName(toolName string) bool {
4545
for _, c := range toolName {
4646
switch {
4747
case 'a' <= c && c <= 'z', '0' <= c && c <= '9', c == '_':

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,32 @@ func init() {
207207
SetGOROOT(Getenv("GOROOT"), false)
208208
}
209209

210+
// ForceHost forces GOOS and GOARCH to runtime.GOOS and runtime.GOARCH.
211+
// This is used by go tool to build tools for the go command's own
212+
// GOOS and GOARCH.
213+
func ForceHost() {
214+
Goos = runtime.GOOS
215+
Goarch = runtime.GOARCH
216+
ExeSuffix = exeSuffix()
217+
GO386 = buildcfg.DefaultGO386
218+
GOAMD64 = buildcfg.DefaultGOAMD64
219+
GOARM = buildcfg.DefaultGOARM
220+
GOARM64 = buildcfg.DefaultGOARM64
221+
GOMIPS = buildcfg.DefaultGOMIPS
222+
GOMIPS64 = buildcfg.DefaultGOMIPS64
223+
GOPPC64 = buildcfg.DefaultGOPPC64
224+
GORISCV64 = buildcfg.DefaultGORISCV64
225+
GOWASM = ""
226+
227+
// Recompute the build context using Goos and Goarch to
228+
// set the correct value for ctx.CgoEnabled.
229+
BuildContext = defaultContext()
230+
// Recompute experiments: the settings determined depend on GOOS and GOARCH.
231+
// This will also update the BuildContext's tool tags to include the new
232+
// experiment tags.
233+
computeExperiment()
234+
}
235+
210236
// SetGOROOT sets GOROOT and associated variables to the given values.
211237
//
212238
// If isTestGo is true, build.ToolDir is set based on the TESTGO_GOHOSTOS and
@@ -269,6 +295,10 @@ var (
269295
)
270296

271297
func init() {
298+
computeExperiment()
299+
}
300+
301+
func computeExperiment() {
272302
Experiment, ExperimentErr = buildcfg.ParseGOEXPERIMENT(Goos, Goarch, RawGOEXPERIMENT)
273303
if ExperimentErr != nil {
274304
return

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

Lines changed: 77 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ import (
1818
"os"
1919
"os/exec"
2020
"os/signal"
21-
"path/filepath"
21+
"path"
2222
"slices"
2323
"sort"
2424
"strings"
2525

2626
"cmd/go/internal/base"
2727
"cmd/go/internal/cfg"
2828
"cmd/go/internal/load"
29+
"cmd/go/internal/modindex"
2930
"cmd/go/internal/modload"
3031
"cmd/go/internal/str"
3132
"cmd/go/internal/work"
@@ -101,9 +102,20 @@ func runTool(ctx context.Context, cmd *base.Command, args []string) {
101102
}
102103
}
103104

105+
// See if tool can be a builtin tool. If so, try to build and run it.
106+
// buildAndRunBuiltinTool will fail if the install target of the loaded package is not
107+
// the tool directory.
108+
if tool := loadBuiltinTool(toolName); tool != "" {
109+
// Increment a counter for the tool subcommand with the tool name.
110+
counter.Inc("go/subcommand:tool-" + toolName)
111+
buildAndRunBuiltinTool(ctx, toolName, tool, args[1:])
112+
return
113+
}
114+
115+
// Try to build and run mod tool.
104116
tool := loadModTool(ctx, toolName)
105117
if tool != "" {
106-
buildAndRunModtool(ctx, tool, args[1:])
118+
buildAndRunModtool(ctx, toolName, tool, args[1:])
107119
return
108120
}
109121

@@ -116,47 +128,7 @@ func runTool(ctx context.Context, cmd *base.Command, args []string) {
116128
counter.Inc("go/subcommand:tool-" + toolName)
117129
}
118130

119-
if toolN {
120-
cmd := toolPath
121-
if len(args) > 1 {
122-
cmd += " " + strings.Join(args[1:], " ")
123-
}
124-
fmt.Printf("%s\n", cmd)
125-
return
126-
}
127-
args[0] = toolPath // in case the tool wants to re-exec itself, e.g. cmd/dist
128-
toolCmd := &exec.Cmd{
129-
Path: toolPath,
130-
Args: args,
131-
Stdin: os.Stdin,
132-
Stdout: os.Stdout,
133-
Stderr: os.Stderr,
134-
}
135-
err = toolCmd.Start()
136-
if err == nil {
137-
c := make(chan os.Signal, 100)
138-
signal.Notify(c)
139-
go func() {
140-
for sig := range c {
141-
toolCmd.Process.Signal(sig)
142-
}
143-
}()
144-
err = toolCmd.Wait()
145-
signal.Stop(c)
146-
close(c)
147-
}
148-
if err != nil {
149-
// Only print about the exit status if the command
150-
// didn't even run (not an ExitError) or it didn't exit cleanly
151-
// or we're printing command lines too (-x mode).
152-
// Assume if command exited cleanly (even with non-zero status)
153-
// it printed any messages it wanted to print.
154-
if e, ok := err.(*exec.ExitError); !ok || !e.Exited() || cfg.BuildX {
155-
fmt.Fprintf(os.Stderr, "go tool %s: %s\n", toolName, err)
156-
}
157-
base.SetExitStatus(1)
158-
return
159-
}
131+
runBuiltTool(toolName, nil, append([]string{toolPath}, args[1:]...))
160132
}
161133

162134
// listTools prints a list of the available tools in the tools directory.
@@ -262,6 +234,23 @@ func defaultExecName(importPath string) string {
262234
return p.DefaultExecName()
263235
}
264236

237+
func loadBuiltinTool(toolName string) string {
238+
if !base.ValidToolName(toolName) {
239+
return ""
240+
}
241+
cmdTool := path.Join("cmd", toolName)
242+
if !modindex.IsStandardPackage(cfg.GOROOT, cfg.BuildContext.Compiler, cmdTool) {
243+
return ""
244+
}
245+
// Create a fake package and check to see if it would be installed to the tool directory.
246+
// If not, it's not a builtin tool.
247+
p := &load.Package{PackagePublic: load.PackagePublic{Name: "main", ImportPath: cmdTool, Goroot: true}}
248+
if load.InstallTargetDir(p) != load.ToTool {
249+
return ""
250+
}
251+
return cmdTool
252+
}
253+
265254
func loadModTool(ctx context.Context, name string) string {
266255
modload.InitWorkfile()
267256
modload.LoadModFile(ctx)
@@ -288,7 +277,42 @@ func loadModTool(ctx context.Context, name string) string {
288277
return ""
289278
}
290279

291-
func buildAndRunModtool(ctx context.Context, tool string, args []string) {
280+
func buildAndRunBuiltinTool(ctx context.Context, toolName, tool string, args []string) {
281+
// Override GOOS and GOARCH for the build to build the tool using
282+
// the same GOOS and GOARCH as this go command.
283+
cfg.ForceHost()
284+
285+
// Ignore go.mod and go.work: we don't need them, and we want to be able
286+
// to run the tool even if there's an issue with the module or workspace the
287+
// user happens to be in.
288+
modload.RootMode = modload.NoRoot
289+
290+
runFunc := func(b *work.Builder, ctx context.Context, a *work.Action) error {
291+
cmdline := str.StringList(a.Deps[0].BuiltTarget(), a.Args)
292+
return runBuiltTool(toolName, nil, cmdline)
293+
}
294+
295+
buildAndRunTool(ctx, tool, args, runFunc)
296+
}
297+
298+
func buildAndRunModtool(ctx context.Context, toolName, tool string, args []string) {
299+
runFunc := func(b *work.Builder, ctx context.Context, a *work.Action) error {
300+
// Use the ExecCmd to run the binary, as go run does. ExecCmd allows users
301+
// to provide a runner to run the binary, for example a simulator for binaries
302+
// that are cross-compiled to a different platform.
303+
cmdline := str.StringList(work.FindExecCmd(), a.Deps[0].BuiltTarget(), a.Args)
304+
// Use same environment go run uses to start the executable:
305+
// the original environment with cfg.GOROOTbin added to the path.
306+
env := slices.Clip(cfg.OrigEnv)
307+
env = base.AppendPATH(env)
308+
309+
return runBuiltTool(toolName, env, cmdline)
310+
}
311+
312+
buildAndRunTool(ctx, tool, args, runFunc)
313+
}
314+
315+
func buildAndRunTool(ctx context.Context, tool string, args []string, runTool work.ActorFunc) {
292316
work.BuildInit()
293317
b := work.NewBuilder("")
294318
defer func() {
@@ -304,23 +328,16 @@ func buildAndRunModtool(ctx context.Context, tool string, args []string) {
304328

305329
a1 := b.LinkAction(work.ModeBuild, work.ModeBuild, p)
306330
a1.CacheExecutable = true
307-
a := &work.Action{Mode: "go tool", Actor: work.ActorFunc(runBuiltTool), Args: args, Deps: []*work.Action{a1}}
331+
a := &work.Action{Mode: "go tool", Actor: runTool, Args: args, Deps: []*work.Action{a1}}
308332
b.Do(ctx, a)
309333
}
310334

311-
func runBuiltTool(b *work.Builder, ctx context.Context, a *work.Action) error {
312-
cmdline := str.StringList(work.FindExecCmd(), a.Deps[0].BuiltTarget(), a.Args)
313-
335+
func runBuiltTool(toolName string, env, cmdline []string) error {
314336
if toolN {
315337
fmt.Println(strings.Join(cmdline, " "))
316338
return nil
317339
}
318340

319-
// Use same environment go run uses to start the executable:
320-
// the original environment with cfg.GOROOTbin added to the path.
321-
env := slices.Clip(cfg.OrigEnv)
322-
env = base.AppendPATH(env)
323-
324341
toolCmd := &exec.Cmd{
325342
Path: cmdline[0],
326343
Args: cmdline,
@@ -344,13 +361,17 @@ func runBuiltTool(b *work.Builder, ctx context.Context, a *work.Action) error {
344361
}
345362
if err != nil {
346363
// Only print about the exit status if the command
347-
// didn't even run (not an ExitError)
364+
// didn't even run (not an ExitError) or if it didn't exit cleanly
365+
// or we're printing command lines too (-x mode).
348366
// Assume if command exited cleanly (even with non-zero status)
349367
// it printed any messages it wanted to print.
350-
if e, ok := err.(*exec.ExitError); ok {
368+
e, ok := err.(*exec.ExitError)
369+
if !ok || !e.Exited() || cfg.BuildX {
370+
fmt.Fprintf(os.Stderr, "go tool %s: %s\n", toolName, err)
371+
}
372+
if ok {
351373
base.SetExitStatus(e.ExitCode())
352374
} else {
353-
fmt.Fprintf(os.Stderr, "go tool %s: %s\n", filepath.Base(a.Deps[0].Target), err)
354375
base.SetExitStatus(1)
355376
}
356377
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2758,6 +2758,7 @@ func (b *Builder) cgo(a *Action, cgoExe, objdir string, pcCFLAGS, pcLDFLAGS, cgo
27582758
// consists of the original $CGO_LDFLAGS (unchecked) and all the
27592759
// flags put together from source code (checked).
27602760
cgoenv := b.cCompilerEnv()
2761+
cgoenv = append(cgoenv, cfgChangedEnv...)
27612762
var ldflagsOption []string
27622763
if len(cgoLDFLAGS) > 0 {
27632764
flags := make([]string, len(cgoLDFLAGS))

src/cmd/go/internal/work/gc.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,7 @@ func (gcToolchain) gc(b *Builder, a *Action, archive string, importcfg, embedcfg
174174
// code that uses those values to expect absolute paths.
175175
args = append(args, fsys.Actual(f))
176176
}
177-
178-
output, err = sh.runOut(base.Cwd(), nil, args...)
177+
output, err = sh.runOut(base.Cwd(), cfgChangedEnv, args...)
179178
return ofile, output, err
180179
}
181180

@@ -397,7 +396,7 @@ func (gcToolchain) asm(b *Builder, a *Action, sfiles []string) ([]string, error)
397396
ofile := a.Objdir + sfile[:len(sfile)-len(".s")] + ".o"
398397
ofiles = append(ofiles, ofile)
399398
args1 := append(args, "-o", ofile, fsys.Actual(mkAbs(p.Dir, sfile)))
400-
if err := b.Shell(a).run(p.Dir, p.ImportPath, nil, args1...); err != nil {
399+
if err := b.Shell(a).run(p.Dir, p.ImportPath, cfgChangedEnv, args1...); err != nil {
401400
return nil, err
402401
}
403402
}
@@ -424,7 +423,7 @@ func (gcToolchain) symabis(b *Builder, a *Action, sfiles []string) (string, erro
424423
return err
425424
}
426425

427-
return sh.run(p.Dir, p.ImportPath, nil, args...)
426+
return sh.run(p.Dir, p.ImportPath, cfgChangedEnv, args...)
428427
}
429428

430429
var symabis string // Only set if we actually create the file
@@ -673,7 +672,7 @@ func (gcToolchain) ld(b *Builder, root *Action, targetPath, importcfg, mainpkg s
673672
dir, targetPath = filepath.Split(targetPath)
674673
}
675674

676-
env := []string{}
675+
env := cfgChangedEnv
677676
// When -trimpath is used, GOROOT is cleared
678677
if cfg.BuildTrimpath {
679678
env = append(env, "GOROOT=")
@@ -728,7 +727,7 @@ func (gcToolchain) ldShared(b *Builder, root *Action, toplevelactions []*Action,
728727
// the output file path is recorded in the .gnu.version_d section.
729728
dir, targetPath := filepath.Split(targetPath)
730729

731-
return b.Shell(root).run(dir, targetPath, nil, cfg.BuildToolexec, base.Tool("link"), "-o", targetPath, "-importcfg", importcfg, ldflags)
730+
return b.Shell(root).run(dir, targetPath, cfgChangedEnv, cfg.BuildToolexec, base.Tool("link"), "-o", targetPath, "-importcfg", importcfg, ldflags)
732731
}
733732

734733
func (gcToolchain) cc(b *Builder, a *Action, ofile, cfile string) error {

src/cmd/go/internal/work/init.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,36 @@ import (
2020
"path/filepath"
2121
"regexp"
2222
"runtime"
23+
"slices"
2324
"strconv"
2425
"sync"
2526
)
2627

2728
var buildInitStarted = false
2829

30+
// makeCfgChangedEnv is the environment to set to
31+
// override the current environment for GOOS, GOARCH, and the GOARCH-specific
32+
// architecture environment variable to the configuration used by
33+
// the go command. They may be different because go tool <tool> for builtin
34+
// tools need to be built using the host configuration, so the configuration
35+
// used will be changed from that set in the environment. It is clipped
36+
// so its can append to it without changing it.
37+
var cfgChangedEnv []string
38+
39+
func makeCfgChangedEnv() []string {
40+
var env []string
41+
if cfg.Getenv("GOOS") != cfg.Goos {
42+
env = append(env, "GOOS="+cfg.Goos)
43+
}
44+
if cfg.Getenv("GOARCH") != cfg.Goarch {
45+
env = append(env, "GOARCH="+cfg.Goarch)
46+
}
47+
if archenv, val, changed := cfg.GetArchEnv(); changed {
48+
env = append(env, archenv+"="+val)
49+
}
50+
return slices.Clip(env)
51+
}
52+
2953
func BuildInit() {
3054
if buildInitStarted {
3155
base.Fatalf("go: internal error: work.BuildInit called more than once")
@@ -36,6 +60,8 @@ func BuildInit() {
3660
modload.Init()
3761
instrumentInit()
3862
buildModeInit()
63+
cfgChangedEnv = makeCfgChangedEnv()
64+
3965
if err := fsys.Init(); err != nil {
4066
base.Fatal(err)
4167
}

0 commit comments

Comments
 (0)