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 buildAndRunBuiltinTool(ctx context.Context, toolName, tool string, args []string) {
   281  	// Override GOOS and GOARCH for the build to build the tool using
   282  	// the same GOOS and GOARCH as this go command.
   283  	cfg.ForceHost()
   284  
   285  	// Ignore go.mod and go.work: we don't need them, and we want to be able
   286  	// to run the tool even if there's an issue with the module or workspace the
   287  	// user happens to be in.
   288  	modload.RootMode = modload.NoRoot
   289  
   290  	runFunc := func(b *work.Builder, ctx context.Context, a *work.Action) error {
   291  		cmdline := str.StringList(a.Deps[0].BuiltTarget(), a.Args)
   292  		return runBuiltTool(toolName, nil, cmdline)
   293  	}
   294  
   295  	buildAndRunTool(ctx, tool, args, runFunc)
   296  }
   297  
   298  func buildAndRunModtool(ctx context.Context, toolName, tool string, args []string) {
   299  	runFunc := func(b *work.Builder, ctx context.Context, a *work.Action) error {
   300  		// Use the ExecCmd to run the binary, as go run does. ExecCmd allows users
   301  		// to provide a runner to run the binary, for example a simulator for binaries
   302  		// that are cross-compiled to a different platform.
   303  		cmdline := str.StringList(work.FindExecCmd(), a.Deps[0].BuiltTarget(), a.Args)
   304  		// Use same environment go run uses to start the executable:
   305  		// the original environment with cfg.GOROOTbin added to the path.
   306  		env := slices.Clip(cfg.OrigEnv)
   307  		env = base.AppendPATH(env)
   308  
   309  		return runBuiltTool(toolName, env, cmdline)
   310  	}
   311  
   312  	buildAndRunTool(ctx, tool, args, runFunc)
   313  }
   314  
   315  func buildAndRunTool(ctx context.Context, tool string, args []string, runTool work.ActorFunc) {
   316  	work.BuildInit()
   317  	b := work.NewBuilder("")
   318  	defer func() {
   319  		if err := b.Close(); err != nil {
   320  			base.Fatal(err)
   321  		}
   322  	}()
   323  
   324  	pkgOpts := load.PackageOpts{MainOnly: true}
   325  	p := load.PackagesAndErrors(ctx, pkgOpts, []string{tool})[0]
   326  	p.Internal.OmitDebug = true
   327  	p.Internal.ExeName = p.DefaultExecName()
   328  
   329  	a1 := b.LinkAction(work.ModeBuild, work.ModeBuild, p)
   330  	a1.CacheExecutable = true
   331  	a := &work.Action{Mode: "go tool", Actor: runTool, Args: args, Deps: []*work.Action{a1}}
   332  	b.Do(ctx, a)
   333  }
   334  
   335  func runBuiltTool(toolName string, env, cmdline []string) error {
   336  	if toolN {
   337  		fmt.Println(strings.Join(cmdline, " "))
   338  		return nil
   339  	}
   340  
   341  	toolCmd := &exec.Cmd{
   342  		Path:   cmdline[0],
   343  		Args:   cmdline,
   344  		Stdin:  os.Stdin,
   345  		Stdout: os.Stdout,
   346  		Stderr: os.Stderr,
   347  		Env:    env,
   348  	}
   349  	err := toolCmd.Start()
   350  	if err == nil {
   351  		c := make(chan os.Signal, 100)
   352  		signal.Notify(c)
   353  		go func() {
   354  			for sig := range c {
   355  				toolCmd.Process.Signal(sig)
   356  			}
   357  		}()
   358  		err = toolCmd.Wait()
   359  		signal.Stop(c)
   360  		close(c)
   361  	}
   362  	if err != nil {
   363  		// Only print about the exit status if the command
   364  		// didn't even run (not an ExitError) or if it didn't exit cleanly
   365  		// or we're printing command lines too (-x mode).
   366  		// Assume if command exited cleanly (even with non-zero status)
   367  		// it printed any messages it wanted to print.
   368  		e, ok := err.(*exec.ExitError)
   369  		if !ok || !e.Exited() || cfg.BuildX {
   370  			fmt.Fprintf(os.Stderr, "go tool %s: %s\n", toolName, err)
   371  		}
   372  		if ok {
   373  			base.SetExitStatus(e.ExitCode())
   374  		} else {
   375  			base.SetExitStatus(1)
   376  		}
   377  	}
   378  
   379  	return nil
   380  }
   381  

View as plain text