]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/go: impersonate 'go tool dist list' if 'go tool dist' is not present
authorBryan C. Mills <bcmills@google.com>
Thu, 22 Jun 2023 15:56:55 +0000 (11:56 -0400)
committerGopher Robot <gobot@golang.org>
Thu, 22 Jun 2023 19:52:13 +0000 (19:52 +0000)
Fixes #60939.

Change-Id: I6a15db558a8e80e242818cccd642899aba47e596
Reviewed-on: https://go-review.googlesource.com/c/go/+/505176
Reviewed-by: Russ Cox <rsc@golang.org>
Run-TryBot: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Auto-Submit: Bryan Mills <bcmills@google.com>

src/cmd/go/internal/base/tool.go
src/cmd/go/internal/tool/tool.go
src/cmd/go/testdata/script/dist_list_missing.txt [new file with mode: 0644]

index 202e314b9442e4a126f49c09716c31494b6edaf4..ab623da4264866a2641d5236f31ecb83043fdfe9 100644 (file)
@@ -11,20 +11,31 @@ import (
        "path/filepath"
 
        "cmd/go/internal/cfg"
+       "cmd/go/internal/par"
 )
 
 // Tool returns the path to the named tool (for example, "vet").
 // If the tool cannot be found, Tool exits the process.
 func Tool(toolName string) string {
-       toolPath := filepath.Join(build.ToolDir, toolName) + cfg.ToolExeSuffix()
-       if len(cfg.BuildToolexec) > 0 {
-               return toolPath
-       }
-       // Give a nice message if there is no tool with that name.
-       if _, err := os.Stat(toolPath); err != nil {
+       toolPath, err := ToolPath(toolName)
+       if err != nil && len(cfg.BuildToolexec) == 0 {
+               // Give a nice message if there is no tool with that name.
                fmt.Fprintf(os.Stderr, "go: no such tool %q\n", toolName)
                SetExitStatus(2)
                Exit()
        }
        return toolPath
 }
+
+// Tool 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) {
+       toolPath := filepath.Join(build.ToolDir, toolName) + cfg.ToolExeSuffix()
+       err := toolStatCache.Do(toolPath, func() error {
+               _, err := os.Stat(toolPath)
+               return err
+       })
+       return toolPath, err
+}
+
+var toolStatCache par.Cache[string, error]
index 069968b1b6efa416e50e82fa99489004ffd443d6..ebe189bb819a37dc28e5ebe1da4365d87b369abe 100644 (file)
@@ -7,8 +7,11 @@ package tool
 
 import (
        "context"
+       "encoding/json"
+       "flag"
        "fmt"
        "go/build"
+       "internal/platform"
        "os"
        "os/exec"
        "os/signal"
@@ -68,10 +71,25 @@ func runTool(ctx context.Context, cmd *base.Command, args []string) {
                        return
                }
        }
-       toolPath := base.Tool(toolName)
-       if toolPath == "" {
-               return
+
+       toolPath, err := base.ToolPath(toolName)
+       if err != nil {
+               if toolName == "dist" && len(args) > 1 && args[1] == "list" {
+                       // cmd/distpack removes the 'dist' tool from the toolchain to save space,
+                       // since it is normally only used for building the toolchain in the first
+                       // place. However, 'go tool dist list' is useful for listing all supported
+                       // platforms.
+                       //
+                       // If the dist tool does not exist, impersonate this command.
+                       if impersonateDistList(args[2:]) {
+                               return
+                       }
+               }
+
+               // Emit the usual error for the missing tool.
+               _ = base.Tool(toolName)
        }
+
        if toolN {
                cmd := toolPath
                if len(args) > 1 {
@@ -88,7 +106,7 @@ func runTool(ctx context.Context, cmd *base.Command, args []string) {
                Stdout: os.Stdout,
                Stderr: os.Stderr,
        }
-       err := toolCmd.Start()
+       err = toolCmd.Start()
        if err == nil {
                c := make(chan os.Signal, 100)
                signal.Notify(c)
@@ -145,3 +163,62 @@ func listTools() {
                fmt.Println(name)
        }
 }
+
+func impersonateDistList(args []string) (handled bool) {
+       fs := flag.NewFlagSet("go tool dist list", flag.ContinueOnError)
+       jsonFlag := fs.Bool("json", false, "produce JSON output")
+       brokenFlag := fs.Bool("broken", false, "include broken ports")
+
+       // The usage for 'go tool dist' claims that
+       // “All commands take -v flags to emit extra information”,
+       // but list -v appears not to have any effect.
+       _ = fs.Bool("v", false, "emit extra information")
+
+       if err := fs.Parse(args); err != nil || len(fs.Args()) > 0 {
+               // Unrecognized flag or argument.
+               // Force fallback to the real 'go tool dist'.
+               return false
+       }
+
+       if !*jsonFlag {
+               for _, p := range platform.List {
+                       if !*brokenFlag && platform.Broken(p.GOOS, p.GOARCH) {
+                               continue
+                       }
+                       fmt.Println(p)
+               }
+               return true
+       }
+
+       type jsonResult struct {
+               GOOS         string
+               GOARCH       string
+               CgoSupported bool
+               FirstClass   bool
+               Broken       bool `json:",omitempty"`
+       }
+
+       var results []jsonResult
+       for _, p := range platform.List {
+               broken := platform.Broken(p.GOOS, p.GOARCH)
+               if broken && !*brokenFlag {
+                       continue
+               }
+               if *jsonFlag {
+                       results = append(results, jsonResult{
+                               GOOS:         p.GOOS,
+                               GOARCH:       p.GOARCH,
+                               CgoSupported: platform.CgoSupported(p.GOOS, p.GOARCH),
+                               FirstClass:   platform.FirstClass(p.GOOS, p.GOARCH),
+                               Broken:       broken,
+                       })
+               }
+       }
+       out, err := json.MarshalIndent(results, "", "\t")
+       if err != nil {
+               return false
+       }
+
+       os.Stdout.Write(out)
+       return true
+}
diff --git a/src/cmd/go/testdata/script/dist_list_missing.txt b/src/cmd/go/testdata/script/dist_list_missing.txt
new file mode 100644 (file)
index 0000000..affaa00
--- /dev/null
@@ -0,0 +1,57 @@
+# Regression test for #60939: when 'go tool dist' is missing,
+# 'go tool dist list' should inject its output.
+
+
+# Set GOROOT to a directory that definitely does not include
+# a compiled 'dist' tool. 'go tool dist list' should still
+# work, because 'cmd/go' itself can impersonate this command.
+
+mkdir $WORK/goroot/bin
+mkdir $WORK/goroot/pkg/tool/${GOOS}_${GOARCH}
+env GOROOT=$WORK/goroot
+
+! go tool -n dist
+stderr 'go: no such tool "dist"'
+
+go tool dist list
+stdout linux/amd64
+cp stdout tool.txt
+
+go tool dist list -v
+stdout linux/amd64
+cp stdout tool-v.txt
+
+go tool dist list -broken
+stdout $GOOS/$GOARCH
+cp stdout tool-broken.txt
+
+go tool dist list -json
+stdout '"GOOS": "linux",\n\s*"GOARCH": "amd64",\n'
+cp stdout tool-json.txt
+
+go tool dist list -json -broken
+stdout '"GOOS": "'$GOOS'",\n\s*"GOARCH": "'$GOARCH'",\n'
+cp stdout tool-json-broken.txt
+
+[short] stop
+
+
+# Check against the real cmd/dist as the source of truth.
+
+env GOROOT=$TESTGO_GOROOT
+go build -o dist.exe cmd/dist
+
+exec ./dist.exe list
+cmp stdout tool.txt
+
+exec ./dist.exe list -v
+cmp stdout tool-v.txt
+
+exec ./dist.exe list -broken
+cmp stdout tool-broken.txt
+
+exec ./dist.exe list -json
+cmp stdout tool-json.txt
+
+exec ./dist.exe list -json -broken
+cmp stdout tool-json-broken.txt