Source file src/cmd/go/internal/tool/tool.go

     1  // Copyright 2011 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package tool implements the “go tool” command.
     6  package tool
     7  
     8  import (
     9  	"cmd/internal/telemetry/counter"
    10  	"context"
    11  	"encoding/json"
    12  	"errors"
    13  	"flag"
    14  	"fmt"
    15  	"go/build"
    16  	"internal/platform"
    17  	"maps"
    18  	"os"
    19  	"os/exec"
    20  	"os/signal"
    21  	"path"
    22  	"slices"
    23  	"sort"
    24  	"strings"
    25  	"time"
    26  
    27  	"cmd/go/internal/base"
    28  	"cmd/go/internal/cfg"
    29  	"cmd/go/internal/load"
    30  	"cmd/go/internal/modindex"
    31  	"cmd/go/internal/modload"
    32  	"cmd/go/internal/str"
    33  	"cmd/go/internal/work"
    34  )
    35  
    36  var CmdTool = &base.Command{
    37  	Run:       runTool,
    38  	UsageLine: "go tool [-n] command [args...]",
    39  	Short:     "run specified go tool",
    40  	Long: `
    41  Tool runs the go tool command identified by the arguments.
    42  
    43  Go ships with a number of builtin tools, and additional tools
    44  may be defined in the go.mod of the current module. 'go get -tool'
    45  can be used to define additional tools in the current module's
    46  go.mod file. See 'go help get' for more information.
    47  
    48  The command can be specified using the full package path to the tool declared with
    49  a tool directive. The default binary name of the tool, which is the last component of
    50  the package path, excluding the major version suffix, can also be used if it is unique
    51  among declared tools.
    52  
    53  With no arguments it prints the list of known tools.
    54  
    55  The -n flag causes tool to print the command that would be
    56  executed but not execute it.
    57  
    58  The -modfile=file.mod build flag causes tool to use an alternate file
    59  instead of the go.mod in the module root directory.
    60  
    61  Tool also provides the -C, -overlay, and -modcacherw build flags.
    62  
    63  The go command places $GOROOT/bin at the beginning of $PATH in the
    64  environment of commands run via tool directives, so that they use the
    65  same 'go' as the parent 'go tool'.
    66  
    67  For more about build flags, see 'go help build'.
    68  
    69  For more about each builtin tool command, see 'go doc cmd/<command>'.
    70  `,
    71  }
    72  
    73  var toolN bool
    74  
    75  // Return whether tool can be expected in the gccgo tool directory.
    76  // Other binaries could be in the same directory so don't
    77  // show those with the 'go tool' command.
    78  func isGccgoTool(tool string) bool {
    79  	switch tool {
    80  	case "cgo", "fix", "cover", "godoc", "vet":
    81  		return true
    82  	}
    83  	return false
    84  }
    85  
    86  func init() {
    87  	base.AddChdirFlag(&CmdTool.Flag)
    88  	base.AddModCommonFlags(&CmdTool.Flag)
    89  	CmdTool.Flag.BoolVar(&toolN, "n", false, "")
    90  }
    91  
    92  func runTool(ctx context.Context, cmd *base.Command, args []string) {
    93  	moduleLoader := modload.NewLoader()
    94  	if len(args) == 0 {
    95  		counter.Inc("go/subcommand:tool")
    96  		listTools(moduleLoader, ctx)
    97  		return
    98  	}
    99  	toolName := args[0]
   100  
   101  	toolPath, err := base.ToolPath(toolName)
   102  	if err != nil {
   103  		if toolName == "dist" && len(args) > 1 && args[1] == "list" {
   104  			// cmd/distpack removes the 'dist' tool from the toolchain to save space,
   105  			// since it is normally only used for building the toolchain in the first
   106  			// place. However, 'go tool dist list' is useful for listing all supported
   107  			// platforms.
   108  			//
   109  			// If the dist tool does not exist, impersonate this command.
   110  			if impersonateDistList(args[2:]) {
   111  				// If it becomes necessary, we could increment an additional counter to indicate
   112  				// that we're impersonating dist list if knowing that becomes important?
   113  				counter.Inc("go/subcommand:tool-dist")
   114  				return
   115  			}
   116  		}
   117  
   118  		// See if tool can be a builtin tool. If so, try to build and run it.
   119  		// buildAndRunBuiltinTool will fail if the install target of the loaded package is not
   120  		// the tool directory.
   121  		if tool := loadBuiltinTool(toolName); tool != "" {
   122  			// Increment a counter for the tool subcommand with the tool name.
   123  			counter.Inc("go/subcommand:tool-" + toolName)
   124  			buildAndRunBuiltinTool(moduleLoader, ctx, toolName, tool, args[1:])
   125  			return
   126  		}
   127  
   128  		// Try to build and run mod tool.
   129  		tool := loadModTool(moduleLoader, ctx, toolName)
   130  		if tool != "" {
   131  			buildAndRunModtool(moduleLoader, ctx, toolName, tool, args[1:])
   132  			return
   133  		}
   134  
   135  		counter.Inc("go/subcommand:tool-unknown")
   136  
   137  		// Emit the usual error for the missing tool.
   138  		_ = base.Tool(toolName)
   139  	} else {
   140  		// Increment a counter for the tool subcommand with the tool name.
   141  		counter.Inc("go/subcommand:tool-" + toolName)
   142  	}
   143  
   144  	runBuiltTool(toolName, nil, append([]string{toolPath}, args[1:]...))
   145  }
   146  
   147  // listTools prints a list of the available tools in the tools directory.
   148  func listTools(ld *modload.Loader, ctx context.Context) {
   149  	f, err := os.Open(build.ToolDir)
   150  	if err != nil {
   151  		fmt.Fprintf(os.Stderr, "go: no tool directory: %s\n", err)
   152  		base.SetExitStatus(2)
   153  		return
   154  	}
   155  	defer f.Close()
   156  	names, err := f.Readdirnames(-1)
   157  	if err != nil {
   158  		fmt.Fprintf(os.Stderr, "go: can't read tool directory: %s\n", err)
   159  		base.SetExitStatus(2)
   160  		return
   161  	}
   162  
   163  	ambiguous := make(map[string]bool) // names that can't be used as aliases because they are ambiguous
   164  	sort.Strings(names)
   165  	for _, name := range names {
   166  		ambiguous[name] = true
   167  
   168  		// Unify presentation by going to lower case.
   169  		// If it's windows, don't show the .exe suffix.
   170  		name = strings.TrimSuffix(strings.ToLower(name), cfg.ToolExeSuffix())
   171  
   172  		// The tool directory used by gccgo will have other binaries
   173  		// in addition to go tools. Only display go tools here.
   174  		if cfg.BuildToolchainName == "gccgo" && !isGccgoTool(name) {
   175  			continue
   176  		}
   177  		fmt.Println(name)
   178  	}
   179  
   180  	ld.InitWorkfile()
   181  	modload.LoadModFile(ld, ctx)
   182  	modTools := slices.Sorted(maps.Keys(ld.MainModules.Tools()))
   183  	seen := make(map[string]bool) // aliases we've seen already
   184  	for _, tool := range modTools {
   185  		alias := defaultExecName(tool)
   186  		switch {
   187  		case ambiguous[alias]:
   188  			continue
   189  		case seen[alias]:
   190  			ambiguous[alias] = true
   191  		default:
   192  			seen[alias] = true
   193  		}
   194  	}
   195  	for _, tool := range modTools {
   196  		if alias := defaultExecName(tool); !ambiguous[alias] {
   197  			fmt.Printf("%s (%s)\n", alias, tool)
   198  			continue
   199  		}
   200  		fmt.Println(tool)
   201  	}
   202  }
   203  
   204  func impersonateDistList(args []string) (handled bool) {
   205  	fs := flag.NewFlagSet("go tool dist list", flag.ContinueOnError)
   206  	jsonFlag := fs.Bool("json", false, "produce JSON output")
   207  	brokenFlag := fs.Bool("broken", false, "include broken ports")
   208  
   209  	// The usage for 'go tool dist' claims that
   210  	// “All commands take -v flags to emit extra information”,
   211  	// but list -v appears not to have any effect.
   212  	_ = fs.Bool("v", false, "emit extra information")
   213  
   214  	if err := fs.Parse(args); err != nil || len(fs.Args()) > 0 {
   215  		// Unrecognized flag or argument.
   216  		// Force fallback to the real 'go tool dist'.
   217  		return false
   218  	}
   219  
   220  	if !*jsonFlag {
   221  		for _, p := range platform.List {
   222  			if !*brokenFlag && platform.Broken(p.GOOS, p.GOARCH) {
   223  				continue
   224  			}
   225  			fmt.Println(p)
   226  		}
   227  		return true
   228  	}
   229  
   230  	type jsonResult struct {
   231  		GOOS         string
   232  		GOARCH       string
   233  		CgoSupported bool
   234  		FirstClass   bool
   235  		Broken       bool `json:",omitempty"`
   236  	}
   237  
   238  	var results []jsonResult
   239  	for _, p := range platform.List {
   240  		broken := platform.Broken(p.GOOS, p.GOARCH)
   241  		if broken && !*brokenFlag {
   242  			continue
   243  		}
   244  		if *jsonFlag {
   245  			results = append(results, jsonResult{
   246  				GOOS:         p.GOOS,
   247  				GOARCH:       p.GOARCH,
   248  				CgoSupported: platform.CgoSupported(p.GOOS, p.GOARCH),
   249  				FirstClass:   platform.FirstClass(p.GOOS, p.GOARCH),
   250  				Broken:       broken,
   251  			})
   252  		}
   253  	}
   254  	out, err := json.MarshalIndent(results, "", "\t")
   255  	if err != nil {
   256  		return false
   257  	}
   258  
   259  	os.Stdout.Write(out)
   260  	return true
   261  }
   262  
   263  func defaultExecName(importPath string) string {
   264  	var p load.Package
   265  	p.ImportPath = importPath
   266  	return p.DefaultExecName()
   267  }
   268  
   269  func loadBuiltinTool(toolName string) string {
   270  	if !base.ValidToolName(toolName) {
   271  		return ""
   272  	}
   273  	cmdTool := path.Join("cmd", toolName)
   274  	if !modindex.IsStandardPackage(cfg.GOROOT, cfg.BuildContext.Compiler, cmdTool) {
   275  		return ""
   276  	}
   277  	// Create a fake package and check to see if it would be installed to the tool directory.
   278  	// If not, it's not a builtin tool.
   279  	p := &load.Package{PackagePublic: load.PackagePublic{Name: "main", ImportPath: cmdTool, Goroot: true}}
   280  	if load.InstallTargetDir(p) != load.ToTool {
   281  		return ""
   282  	}
   283  	return cmdTool
   284  }
   285  
   286  func loadModTool(ld *modload.Loader, ctx context.Context, name string) string {
   287  	ld.InitWorkfile()
   288  	modload.LoadModFile(ld, ctx)
   289  
   290  	matches := []string{}
   291  	for tool := range ld.MainModules.Tools() {
   292  		if tool == name || defaultExecName(tool) == name {
   293  			matches = append(matches, tool)
   294  		}
   295  	}
   296  
   297  	if len(matches) == 1 {
   298  		return matches[0]
   299  	}
   300  
   301  	if len(matches) > 1 {
   302  		message := fmt.Sprintf("tool %q is ambiguous; choose one of:\n\t", name)
   303  		for _, tool := range matches {
   304  			message += tool + "\n\t"
   305  		}
   306  		base.Fatal(errors.New(message))
   307  	}
   308  
   309  	return ""
   310  }
   311  
   312  func builtTool(runAction *work.Action) string {
   313  	linkAction := runAction.Deps[0]
   314  	if toolN {
   315  		// #72824: If -n is set, use the cached path if we can.
   316  		// This is only necessary if the binary wasn't cached
   317  		// before this invocation of the go command: if the binary
   318  		// was cached, BuiltTarget() will be the cached executable.
   319  		// It's only in the "first run", where we actually do the build
   320  		// and save the result to the cache that BuiltTarget is not
   321  		// the cached binary. Ideally, we would set BuiltTarget
   322  		// to the cached path even in the first run, but if we
   323  		// copy the binary to the cached path, and try to run it
   324  		// in the same process, we'll run into the dreaded #22315
   325  		// resulting in occasional ETXTBSYs. Instead of getting the
   326  		// ETXTBSY and then retrying just don't use the cached path
   327  		// on the first run if we're going to actually run the binary.
   328  		if cached := linkAction.CachedExecutable(); cached != "" {
   329  			return cached
   330  		}
   331  	}
   332  	return linkAction.BuiltTarget()
   333  }
   334  
   335  func buildAndRunBuiltinTool(ld *modload.Loader, ctx context.Context, toolName, tool string, args []string) {
   336  	// Override GOOS and GOARCH for the build to build the tool using
   337  	// the same GOOS and GOARCH as this go command.
   338  	cfg.ForceHost()
   339  
   340  	// Ignore go.mod and go.work: we don't need them, and we want to be able
   341  	// to run the tool even if there's an issue with the module or workspace the
   342  	// user happens to be in.
   343  	ld.RootMode = modload.NoRoot
   344  
   345  	runFunc := func(b *work.Builder, ctx context.Context, a *work.Action) error {
   346  		cmdline := str.StringList(builtTool(a), a.Args)
   347  		return runBuiltTool(toolName, nil, cmdline)
   348  	}
   349  
   350  	buildAndRunTool(ld, ctx, tool, args, runFunc)
   351  }
   352  
   353  func buildAndRunModtool(ld *modload.Loader, ctx context.Context, toolName, tool string, args []string) {
   354  	runFunc := func(b *work.Builder, ctx context.Context, a *work.Action) error {
   355  		// Use the ExecCmd to run the binary, as go run does. ExecCmd allows users
   356  		// to provide a runner to run the binary, for example a simulator for binaries
   357  		// that are cross-compiled to a different platform.
   358  		cmdline := str.StringList(work.FindExecCmd(), builtTool(a), a.Args)
   359  		// Use same environment go run uses to start the executable:
   360  		// the original environment with cfg.GOROOTbin added to the path.
   361  		env := slices.Clip(cfg.OrigEnv)
   362  		env = base.AppendPATH(env)
   363  
   364  		return runBuiltTool(toolName, env, cmdline)
   365  	}
   366  
   367  	buildAndRunTool(ld, ctx, tool, args, runFunc)
   368  }
   369  
   370  func buildAndRunTool(ld *modload.Loader, ctx context.Context, tool string, args []string, runTool work.ActorFunc) {
   371  	work.BuildInit(ld)
   372  	b := work.NewBuilder("", ld.VendorDirOrEmpty)
   373  	defer func() {
   374  		if err := b.Close(); err != nil {
   375  			base.Fatal(err)
   376  		}
   377  	}()
   378  
   379  	pkgOpts := load.PackageOpts{MainOnly: true}
   380  	p := load.PackagesAndErrors(ld, ctx, pkgOpts, []string{tool})[0]
   381  	p.Internal.OmitDebug = true
   382  	p.Internal.ExeName = p.DefaultExecName()
   383  
   384  	a1 := b.LinkAction(ld, work.ModeBuild, work.ModeBuild, p)
   385  	a1.CacheExecutable = true
   386  	a := &work.Action{Mode: "go tool", Actor: runTool, Args: args, Deps: []*work.Action{a1}}
   387  	b.Do(ctx, a)
   388  }
   389  
   390  func runBuiltTool(toolName string, env, cmdline []string) error {
   391  	if toolN {
   392  		fmt.Println(strings.Join(cmdline, " "))
   393  		return nil
   394  	}
   395  
   396  	// The tool was just linked and cached into $GOCACHE (CacheExecutable), and
   397  	// is executed from there. A concurrent go process may still hold a writable
   398  	// descriptor to the same cached file, so the exec can fail with ETXTBSY
   399  	// ("text file busy"). Retry a few times with backoff, matching base.RunStdin
   400  	// and (*runTestActor).Act in cmd/go/internal/test. See #22220, #22315, #78204.
   401  	var toolCmd *exec.Cmd
   402  	var err error
   403  	for try := range 3 {
   404  		toolCmd = &exec.Cmd{
   405  			Path:   cmdline[0],
   406  			Args:   cmdline,
   407  			Stdin:  os.Stdin,
   408  			Stdout: os.Stdout,
   409  			Stderr: os.Stderr,
   410  			Env:    env,
   411  		}
   412  		err = toolCmd.Start()
   413  		if err == nil || !base.IsETXTBSY(err) {
   414  			break
   415  		}
   416  		// Another go process likely still has the cached file open for
   417  		// writing; it will close it shortly. Sleep and retry.
   418  		time.Sleep(100 * time.Millisecond << uint(try))
   419  	}
   420  	if err == nil {
   421  		c := make(chan os.Signal, 100)
   422  		signal.Notify(c, signalsToForward...)
   423  		go func() {
   424  			for sig := range c {
   425  				toolCmd.Process.Signal(sig)
   426  			}
   427  		}()
   428  		err = toolCmd.Wait()
   429  		signal.Stop(c)
   430  		close(c)
   431  	}
   432  	if err != nil {
   433  		// Only print about the exit status if the command
   434  		// didn't even run (not an ExitError) or if it didn't exit cleanly
   435  		// or we're printing command lines too (-x mode).
   436  		// Assume if command exited cleanly (even with non-zero status)
   437  		// it printed any messages it wanted to print.
   438  		e, ok := err.(*exec.ExitError)
   439  		if !ok || !e.Exited() || cfg.BuildX {
   440  			fmt.Fprintf(os.Stderr, "go tool %s: %s\n", toolName, err)
   441  		}
   442  		if ok {
   443  			n := e.ExitCode()
   444  			if n == -1 {
   445  				// If the tool was terminated by a signal,
   446  				// set a non-zero exit status. See go.dev/issue/79540.
   447  				n = 1
   448  			}
   449  			base.SetExitStatus(n)
   450  		} else {
   451  			base.SetExitStatus(1)
   452  		}
   453  	}
   454  
   455  	return nil
   456  }
   457  

View as plain text