"cmd/internal/par"
)
-// Tool returns the path to the named tool (for example, "vet").
+// Tool returns the path to the named builtin tool (for example, "vet").
// If the tool cannot be found, Tool exits the process.
func Tool(toolName string) string {
toolPath, err := ToolPath(toolName)
// ToolPath returns the path at which we expect to find the named tool
// (for example, "vet"), and the error (if any) from statting that path.
func ToolPath(toolName string) (string, error) {
+ if !validToolName(toolName) {
+ return "", fmt.Errorf("bad tool name: %q", toolName)
+ }
toolPath := filepath.Join(build.ToolDir, toolName) + cfg.ToolExeSuffix()
err := toolStatCache.Do(toolPath, func() error {
_, err := os.Stat(toolPath)
return toolPath, err
}
+func validToolName(toolName string) bool {
+ for _, c := range toolName {
+ switch {
+ case 'a' <= c && c <= 'z', '0' <= c && c <= '9', c == '_':
+ default:
+ return false
+ }
+ }
+ return true
+}
+
var toolStatCache par.Cache[string, error]
"cmd/internal/telemetry/counter"
"context"
"encoding/json"
+ "errors"
"flag"
"fmt"
"go/build"
"internal/platform"
+ "maps"
"os"
"os/exec"
"os/signal"
+ "path"
+ "path/filepath"
+ "slices"
"sort"
"strings"
"cmd/go/internal/base"
"cmd/go/internal/cfg"
+ "cmd/go/internal/load"
+ "cmd/go/internal/modload"
+ "cmd/go/internal/str"
+ "cmd/go/internal/work"
)
var CmdTool = &base.Command{
Short: "run specified go tool",
Long: `
Tool runs the go tool command identified by the arguments.
+
+Go ships with a number of builtin tools, and additional tools
+may be defined in the go.mod of the current module.
+
With no arguments it prints the list of known tools.
The -n flag causes tool to print the command that would be
executed but not execute it.
-For more about each tool command, see 'go doc cmd/<command>'.
+For more about each builtin tool command, see 'go doc cmd/<command>'.
`,
}
func runTool(ctx context.Context, cmd *base.Command, args []string) {
if len(args) == 0 {
counter.Inc("go/subcommand:tool")
- listTools()
+ listTools(ctx)
return
}
toolName := args[0]
- // The tool name must be lower-case letters, numbers or underscores.
- for _, c := range toolName {
- switch {
- case 'a' <= c && c <= 'z', '0' <= c && c <= '9', c == '_':
- default:
- fmt.Fprintf(os.Stderr, "go: bad tool name %q\n", toolName)
- base.SetExitStatus(2)
- return
- }
- }
toolPath, err := base.ToolPath(toolName)
if err != nil {
}
}
+ tool := loadModTool(ctx, toolName)
+ if tool != "" {
+ buildAndRunModtool(ctx, tool, args[1:])
+ return
+ }
+
counter.Inc("go/subcommand:tool-unknown")
+
// Emit the usual error for the missing tool.
_ = base.Tool(toolName)
} else {
}
// listTools prints a list of the available tools in the tools directory.
-func listTools() {
+func listTools(ctx context.Context) {
f, err := os.Open(build.ToolDir)
if err != nil {
fmt.Fprintf(os.Stderr, "go: no tool directory: %s\n", err)
}
fmt.Println(name)
}
+
+ modload.InitWorkfile()
+ modload.LoadModFile(ctx)
+ modTools := slices.Sorted(maps.Keys(modload.MainModules.Tools()))
+ for _, tool := range modTools {
+ fmt.Println(tool)
+ }
}
func impersonateDistList(args []string) (handled bool) {
os.Stdout.Write(out)
return true
}
+
+func loadModTool(ctx context.Context, name string) string {
+ modload.InitWorkfile()
+ modload.LoadModFile(ctx)
+
+ matches := []string{}
+ for tool := range modload.MainModules.Tools() {
+ if tool == name || path.Base(tool) == name {
+ matches = append(matches, tool)
+ }
+ }
+
+ if len(matches) == 1 {
+ return matches[0]
+ }
+
+ if len(matches) > 1 {
+ message := fmt.Sprintf("tool %q is ambiguous; choose one of:\n\t", name)
+ for _, tool := range matches {
+ message += tool + "\n\t"
+ }
+ base.Fatal(errors.New(message))
+ }
+
+ return ""
+}
+
+func buildAndRunModtool(ctx context.Context, tool string, args []string) {
+ work.BuildInit()
+ b := work.NewBuilder("")
+ defer func() {
+ if err := b.Close(); err != nil {
+ base.Fatal(err)
+ }
+ }()
+
+ pkgOpts := load.PackageOpts{MainOnly: true}
+ p := load.PackagesAndErrors(ctx, pkgOpts, []string{tool})[0]
+ p.Internal.OmitDebug = true
+
+ a1 := b.LinkAction(work.ModeInstall, work.ModeBuild, p)
+ a := &work.Action{Mode: "go tool", Actor: work.ActorFunc(runBuiltTool), Args: args, Deps: []*work.Action{a1}}
+ b.Do(ctx, a)
+}
+
+func runBuiltTool(b *work.Builder, ctx context.Context, a *work.Action) error {
+ cmdline := str.StringList(work.FindExecCmd(), a.Deps[0].Target, a.Args)
+
+ if toolN {
+ fmt.Println(strings.Join(cmdline, " "))
+ return nil
+ }
+
+ toolCmd := &exec.Cmd{
+ Path: cmdline[0],
+ Args: cmdline[1:],
+ Stdin: os.Stdin,
+ Stdout: os.Stdout,
+ Stderr: os.Stderr,
+ }
+ err := toolCmd.Start()
+ if err == nil {
+ c := make(chan os.Signal, 100)
+ signal.Notify(c)
+ go func() {
+ for sig := range c {
+ toolCmd.Process.Signal(sig)
+ }
+ }()
+ err = toolCmd.Wait()
+ signal.Stop(c)
+ close(c)
+ }
+ if err != nil {
+ // Only print about the exit status if the command
+ // didn't even run (not an ExitError)
+ // Assume if command exited cleanly (even with non-zero status)
+ // it printed any messages it wanted to print.
+ if e, ok := err.(*exec.ExitError); ok {
+ base.SetExitStatus(e.ExitCode())
+ } else {
+ fmt.Fprintf(os.Stderr, "go tool %s: %s\n", filepath.Base(a.Deps[0].Target), err)
+ base.SetExitStatus(1)
+ }
+ }
+
+ return nil
+}