]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/go: add support for mod tools
authorConrad Irwin <conrad.irwin@gmail.com>
Fri, 19 Jul 2024 03:50:15 +0000 (21:50 -0600)
committerMichael Matloob <matloob@golang.org>
Wed, 20 Nov 2024 17:58:35 +0000 (17:58 +0000)
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 <matloob@golang.org>
Reviewed-by: Sam Thanawalla <samthanawalla@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>

src/cmd/go/alldocs.go
src/cmd/go/internal/base/tool.go
src/cmd/go/internal/tool/tool.go

index 5b7b2abebbb2b2acb8b63ba7432ce33b532764ea..4f0108b5ab2a6c91deecd9961273cdc8a2387471 100644 (file)
 //     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/<command>'.
+// For more about each builtin tool command, see 'go doc cmd/<command>'.
 //
 // # Print Go version
 //
index 4b3202033f97f3a4e0002aadce89aaaf62229ad4..1d864aa2cc00683b6183a851eb09fdcd53c3a7c3 100644 (file)
@@ -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]
index 77cee564b3d557f95907a5cb8ba1740b163ef1c7..7cba3596a4c1371b4ef8b3a36b80f2a5a23b528d 100644 (file)
@@ -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/<command>'.
+For more about each builtin tool command, see 'go doc cmd/<command>'.
 `,
 }
 
@@ -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
+}