From 5165f54167a404985428cfd87aecc80595af491a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 18 Jul 2024 21:50:15 -0600 Subject: [PATCH] cmd/go: add support for mod tools Running `go tool` with no arguments will now list built in tools followed by module defined tools. Running `go tool X` where X matches either the full package path, or the last segment of the package path, of a defined tool will build the tool to a known location and immediately execute it. For golang/go#48429 Change-Id: I02249df8dad12fb74aa244002f82a81af20e732f Reviewed-on: https://go-review.googlesource.com/c/go/+/534817 Reviewed-by: Michael Matloob Reviewed-by: Sam Thanawalla LUCI-TryBot-Result: Go LUCI --- src/cmd/go/alldocs.go | 6 +- src/cmd/go/internal/base/tool.go | 16 +++- src/cmd/go/internal/tool/tool.go | 131 ++++++++++++++++++++++++++++--- 3 files changed, 138 insertions(+), 15 deletions(-) diff --git a/src/cmd/go/alldocs.go b/src/cmd/go/alldocs.go index 5b7b2abebbb..4f0108b5ab2 100644 --- a/src/cmd/go/alldocs.go +++ b/src/cmd/go/alldocs.go @@ -1949,12 +1949,16 @@ // go tool [-n] command [args...] // // 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/'. +// For more about each builtin tool command, see 'go doc cmd/'. // // # Print Go version // diff --git a/src/cmd/go/internal/base/tool.go b/src/cmd/go/internal/base/tool.go index 4b3202033f9..1d864aa2cc0 100644 --- a/src/cmd/go/internal/base/tool.go +++ b/src/cmd/go/internal/base/tool.go @@ -14,7 +14,7 @@ import ( "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) @@ -30,6 +30,9 @@ func Tool(toolName string) string { // 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) @@ -38,4 +41,15 @@ func ToolPath(toolName string) (string, error) { 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] diff --git a/src/cmd/go/internal/tool/tool.go b/src/cmd/go/internal/tool/tool.go index 77cee564b3d..7cba3596a4c 100644 --- a/src/cmd/go/internal/tool/tool.go +++ b/src/cmd/go/internal/tool/tool.go @@ -9,18 +9,27 @@ import ( "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{ @@ -29,12 +38,16 @@ 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/'. +For more about each builtin tool command, see 'go doc cmd/'. `, } @@ -59,20 +72,10 @@ func init() { 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 { @@ -91,7 +94,14 @@ func runTool(ctx context.Context, cmd *base.Command, args []string) { } } + 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 { @@ -143,7 +153,7 @@ func runTool(ctx context.Context, cmd *base.Command, args []string) { } // 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) @@ -171,6 +181,13 @@ func listTools() { } 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) { @@ -231,3 +248,91 @@ 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 +}