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

View as plain text