1
0
mirror of https://github.com/golang/go synced 2024-11-22 09:04:42 -07:00

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 <matloob@golang.org>
Reviewed-by: Sam Thanawalla <samthanawalla@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Conrad Irwin 2024-07-18 21:50:15 -06:00 committed by Michael Matloob
parent 297081eb02
commit 5165f54167
3 changed files with 138 additions and 15 deletions

View File

@ -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/<command>'.
// For more about each builtin tool command, see 'go doc cmd/<command>'.
//
// # Print Go version
//

View 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]

View 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
}