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  
    26  	"cmd/go/internal/base"
    27  	"cmd/go/internal/cfg"
    28  	"cmd/go/internal/load"
    29  	"cmd/go/internal/modindex"
    30  	"cmd/go/internal/modload"
    31  	"cmd/go/internal/str"
    32  	"cmd/go/internal/work"
    33  )
    34  
    35  var CmdTool = &base.Command{
    36  	Run:       runTool,
    37  	UsageLine: "go tool [-n] command [args...]",
    38  	Short:     "run specified go tool",
    39  	Long: `
    40  Tool runs the go tool command identified by the arguments.
    41  
    42  Go ships with a number of builtin tools, and additional tools
    43  may be defined in the go.mod of the current module.
    44  
    45  With no arguments it prints the list of known tools.
    46  
    47  The -n flag causes tool to print the command that would be
    48  executed but not execute it.
    49  
    50  The -modfile=file.mod build flag causes tool to use an alternate file
    51  instead of the go.mod in the module root directory.
    52  
    53  Tool also provides the -C, -overlay, and -modcacherw build flags.
    54  
    55  For more about build flags, see 'go help build'.
    56  
    57  For more about each builtin tool command, see 'go doc cmd/<command>'.
    58  `,
    59  }
    60  
    61  var toolN bool
    62  
    63  // Return whether tool can be expected in the gccgo tool directory.
    64  // Other binaries could be in the same directory so don't
    65  // show those with the 'go tool' command.
    66  func isGccgoTool(tool string) bool {
    67  	switch tool {
    68  	case "cgo", "fix", "cover", "godoc", "vet":
    69  		return true
    70  	}
    71  	return false
    72  }
    73  
    74  func init() {
    75  	base.AddChdirFlag(&CmdTool.Flag)
    76  	base.AddModCommonFlags(&CmdTool.Flag)
    77  	CmdTool.Flag.BoolVar(&toolN, "n", false, "")
    78  }
    79  
    80  func runTool(ctx context.Context, cmd *base.Command, args []string) {
    81  	if len(args) == 0 {
    82  		counter.Inc("go/subcommand:tool")
    83  		listTools(ctx)
    84  		return
    85  	}
    86  	toolName := args[0]
    87  
    88  	toolPath, err := base.ToolPath(toolName)
    89  	if err != nil {
    90  		if toolName == "dist" && len(args) > 1 && args[1] == "list" {
    91  			// cmd/distpack removes the 'dist' tool from the toolchain to save space,
    92  			// since it is normally only used for building the toolchain in the first
    93  			// place. However, 'go tool dist list' is useful for listing all supported
    94  			// platforms.
    95  			//
    96  			// If the dist tool does not exist, impersonate this command.
    97  			if impersonateDistList(args[2:]) {
    98  				// If it becomes necessary, we could increment an additional counter to indicate
    99  				// that we're impersonating dist list if knowing that becomes important?
   100  				counter.Inc("go/subcommand:tool-dist")
   101  				return
   102  			}
   103  		}
   104  
   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.
   116  		tool := loadModTool(ctx, toolName)
   117  		if tool != "" {
   118  			buildAndRunModtool(ctx, toolName, tool, args[1:])
   119  			return
   120  		}
   121  
   122  		counter.Inc("go/subcommand:tool-unknown")
   123  
   124  		// Emit the usual error for the missing tool.
   125  		_ = base.Tool(toolName)
   126  	} else {
   127  		// Increment a counter for the tool subcommand with the tool name.
   128  		counter.Inc("go/subcommand:tool-" + toolName)
   129  	}
   130  
   131  	runBuiltTool(toolName, nil, append([]string{toolPath}, args[1:]...))
   132  }
   133  
   134  // listTools prints a list of the available tools in the tools directory.
   135  func listTools(ctx context.Context) {
   136  	f, err := os.Open(build.ToolDir)
   137  	if err != nil {
   138  		fmt.Fprintf(os.Stderr, "go: no tool directory: %s\n", err)
   139  		base.SetExitStatus(2)
   140  		return
   141  	}
   142  	defer f.Close()
   143  	names, err := f.Readdirnames(-1)
   144  	if err != nil {
   145  		fmt.Fprintf(os.Stderr, "go: can't read tool directory: %s\n", err)
   146  		base.SetExitStatus(2)
   147  		return
   148  	}
   149  
   150  	sort.Strings(names)
   151  	for _, name := range names {
   152  		// Unify presentation by going to lower case.
   153  		// If it's windows, don't show the .exe suffix.
   154  		name = strings.TrimSuffix(strings.ToLower(name), cfg.ToolExeSuffix())
   155  
   156  		// The tool directory used by gccgo will have other binaries
   157  		// in addition to go tools. Only display go tools here.
   158  		if cfg.BuildToolchainName == "gccgo" && !isGccgoTool(name) {
   159  			continue
   160  		}
   161  		fmt.Println(name)
   162  	}
   163  
   164  	modload.InitWorkfile()
   165  	modload.LoadModFile(ctx)
   166  	modTools := slices.Sorted(maps.Keys(modload.MainModules.Tools()))
   167  	for _, tool := range modTools {
   168  		fmt.Println(tool)
   169  	}
   170  }
   171  
   172  func impersonateDistList(args []string) (handled bool) {
   173  	fs := flag.NewFlagSet("go tool dist list", flag.ContinueOnError)
   174  	jsonFlag := fs.Bool("json", false, "produce JSON output")
   175  	brokenFlag := fs.Bool("broken", false, "include broken ports")
   176  
   177  	// The usage for 'go tool dist' claims that
   178  	// “All commands take -v flags to emit extra information”,
   179  	// but list -v appears not to have any effect.
   180  	_ = fs.Bool("v", false, "emit extra information")
   181  
   182  	if err := fs.Parse(args); err != nil || len(fs.Args()) > 0 {
   183  		// Unrecognized flag or argument.
   184  		// Force fallback to the real 'go tool dist'.
   185  		return false
   186  	}
   187  
   188  	if !*jsonFlag {
   189  		for _, p := range platform.List {
   190  			if !*brokenFlag && platform.Broken(p.GOOS, p.GOARCH) {
   191  				continue
   192  			}
   193  			fmt.Println(p)
   194  		}
   195  		return true
   196  	}
   197  
   198  	type jsonResult struct {
   199  		GOOS         string
   200  		GOARCH       string
   201  		CgoSupported bool
   202  		FirstClass   bool
   203  		Broken       bool `json:",omitempty"`
   204  	}
   205  
   206  	var results []jsonResult
   207  	for _, p := range platform.List {
   208  		broken := platform.Broken(p.GOOS, p.GOARCH)
   209  		if broken && !*brokenFlag {
   210  			continue
   211  		}
   212  		if *jsonFlag {
   213  			results = append(results, jsonResult{
   214  				GOOS:         p.GOOS,
   215  				GOARCH:       p.GOARCH,
   216  				CgoSupported: platform.CgoSupported(p.GOOS, p.GOARCH),
   217  				FirstClass:   platform.FirstClass(p.GOOS, p.GOARCH),
   218  				Broken:       broken,
   219  			})
   220  		}
   221  	}
   222  	out, err := json.MarshalIndent(results, "", "\t")
   223  	if err != nil {
   224  		return false
   225  	}
   226  
   227  	os.Stdout.Write(out)
   228  	return true
   229  }
   230  
   231  func defaultExecName(importPath string) string {
   232  	var p load.Package
   233  	p.ImportPath = importPath
   234  	return p.DefaultExecName()
   235  }
   236  
   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  
   254  func loadModTool(ctx context.Context, name string) string {
   255  	modload.InitWorkfile()
   256  	modload.LoadModFile(ctx)
   257  
   258  	matches := []string{}
   259  	for tool := range modload.MainModules.Tools() {
   260  		if tool == name || defaultExecName(tool) == name {
   261  			matches = append(matches, tool)
   262  		}
   263  	}
   264  
   265  	if len(matches) == 1 {
   266  		return matches[0]
   267  	}
   268  
   269  	if len(matches) > 1 {
   270  		message := fmt.Sprintf("tool %q is ambiguous; choose one of:\n\t", name)
   271  		for _, tool := range matches {
   272  			message += tool + "\n\t"
   273  		}
   274  		base.Fatal(errors.New(message))
   275  	}
   276  
   277  	return ""
   278  }
   279  
   280  func builtTool(runAction *work.Action) string {
   281  	linkAction := runAction.Deps[0]
   282  	if toolN {
   283  		// #72824: If -n is set, use the cached path if we can.
   284  		// This is only necessary if the binary wasn't cached
   285  		// before this invocation of the go command: if the binary
   286  		// was cached, BuiltTarget() will be the cached executable.
   287  		// It's only in the "first run", where we actually do the build
   288  		// and save the result to the cache that BuiltTarget is not
   289  		// the cached binary. Ideally, we would set BuiltTarget
   290  		// to the cached path even in the first run, but if we
   291  		// copy the binary to the cached path, and try to run it
   292  		// in the same process, we'll run into the dreaded #22315
   293  		// resulting in occasional ETXTBSYs. Instead of getting the
   294  		// ETXTBSY and then retrying just don't use the cached path
   295  		// on the first run if we're going to actually run the binary.
   296  		if cached := linkAction.CachedExecutable(); cached != "" {
   297  			return cached
   298  		}
   299  	}
   300  	return linkAction.BuiltTarget()
   301  }
   302  
   303  func buildAndRunBuiltinTool(ctx context.Context, toolName, tool string, args []string) {
   304  	// Override GOOS and GOARCH for the build to build the tool using
   305  	// the same GOOS and GOARCH as this go command.
   306  	cfg.ForceHost()
   307  
   308  	// Ignore go.mod and go.work: we don't need them, and we want to be able
   309  	// to run the tool even if there's an issue with the module or workspace the
   310  	// user happens to be in.
   311  	modload.RootMode = modload.NoRoot
   312  
   313  	runFunc := func(b *work.Builder, ctx context.Context, a *work.Action) error {
   314  		cmdline := str.StringList(builtTool(a), a.Args)
   315  		return runBuiltTool(toolName, nil, cmdline)
   316  	}
   317  
   318  	buildAndRunTool(ctx, tool, args, runFunc)
   319  }
   320  
   321  func buildAndRunModtool(ctx context.Context, toolName, tool string, args []string) {
   322  	runFunc := func(b *work.Builder, ctx context.Context, a *work.Action) error {
   323  		// Use the ExecCmd to run the binary, as go run does. ExecCmd allows users
   324  		// to provide a runner to run the binary, for example a simulator for binaries
   325  		// that are cross-compiled to a different platform.
   326  		cmdline := str.StringList(work.FindExecCmd(), builtTool(a), a.Args)
   327  		// Use same environment go run uses to start the executable:
   328  		// the original environment with cfg.GOROOTbin added to the path.
   329  		env := slices.Clip(cfg.OrigEnv)
   330  		env = base.AppendPATH(env)
   331  
   332  		return runBuiltTool(toolName, env, cmdline)
   333  	}
   334  
   335  	buildAndRunTool(ctx, tool, args, runFunc)
   336  }
   337  
   338  func buildAndRunTool(ctx context.Context, tool string, args []string, runTool work.ActorFunc) {
   339  	work.BuildInit()
   340  	b := work.NewBuilder("")
   341  	defer func() {
   342  		if err := b.Close(); err != nil {
   343  			base.Fatal(err)
   344  		}
   345  	}()
   346  
   347  	pkgOpts := load.PackageOpts{MainOnly: true}
   348  	p := load.PackagesAndErrors(ctx, pkgOpts, []string{tool})[0]
   349  	p.Internal.OmitDebug = true
   350  	p.Internal.ExeName = p.DefaultExecName()
   351  
   352  	a1 := b.LinkAction(work.ModeBuild, work.ModeBuild, p)
   353  	a1.CacheExecutable = true
   354  	a := &work.Action{Mode: "go tool", Actor: runTool, Args: args, Deps: []*work.Action{a1}}
   355  	b.Do(ctx, a)
   356  }
   357  
   358  func runBuiltTool(toolName string, env, cmdline []string) error {
   359  	if toolN {
   360  		fmt.Println(strings.Join(cmdline, " "))
   361  		return nil
   362  	}
   363  
   364  	toolCmd := &exec.Cmd{
   365  		Path:   cmdline[0],
   366  		Args:   cmdline,
   367  		Stdin:  os.Stdin,
   368  		Stdout: os.Stdout,
   369  		Stderr: os.Stderr,
   370  		Env:    env,
   371  	}
   372  	err := toolCmd.Start()
   373  	if err == nil {
   374  		c := make(chan os.Signal, 100)
   375  		signal.Notify(c)
   376  		go func() {
   377  			for sig := range c {
   378  				toolCmd.Process.Signal(sig)
   379  			}
   380  		}()
   381  		err = toolCmd.Wait()
   382  		signal.Stop(c)
   383  		close(c)
   384  	}
   385  	if err != nil {
   386  		// Only print about the exit status if the command
   387  		// didn't even run (not an ExitError) or if it didn't exit cleanly
   388  		// or we're printing command lines too (-x mode).
   389  		// Assume if command exited cleanly (even with non-zero status)
   390  		// it printed any messages it wanted to print.
   391  		e, ok := err.(*exec.ExitError)
   392  		if !ok || !e.Exited() || cfg.BuildX {
   393  			fmt.Fprintf(os.Stderr, "go tool %s: %s\n", toolName, err)
   394  		}
   395  		if ok {
   396  			base.SetExitStatus(e.ExitCode())
   397  		} else {
   398  			base.SetExitStatus(1)
   399  		}
   400  	}
   401  
   402  	return nil
   403  }
   404  

View as plain text