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

View as plain text