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

View as plain text