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

View as plain text