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

View as plain text