From 07843364a7ababdead393d37b5c6e402b9a67328 Mon Sep 17 00:00:00 2001 From: Prince Roshan Date: Fri, 24 Oct 2025 17:25:03 +0530 Subject: [PATCH] cmd/go/internal/tool: include dynamically buildable tools `go tool` only listed binaries already built into `pkg/tool`, so users couldn't discover builtin tools that can be built on demand. Extend the listing to include cmd/ tools that can be built with the current toolchain while avoiding duplicates and keeping the output sorted. For golang/go#75960 Change-Id: I32b0f5ae6cd2c67cf3af465ffb5b4ac3d446ded4 --- src/cmd/go/internal/tool/tool.go | 43 ++++++++- src/cmd/go/internal/tool/tool_test.go | 124 ++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 src/cmd/go/internal/tool/tool_test.go diff --git a/src/cmd/go/internal/tool/tool.go b/src/cmd/go/internal/tool/tool.go index 92e8a803105f8d..a5e65ff7170bbb 100644 --- a/src/cmd/go/internal/tool/tool.go +++ b/src/cmd/go/internal/tool/tool.go @@ -19,6 +19,7 @@ import ( "os/exec" "os/signal" "path" + "path/filepath" "slices" "sort" "strings" @@ -148,6 +149,16 @@ func listTools(loaderstate *modload.State, ctx context.Context) { return } + toolSet := make(map[string]bool) + var toolNames []string + addTool := func(name string) { + if toolSet[name] { + return + } + toolSet[name] = true + toolNames = append(toolNames, name) + } + sort.Strings(names) for _, name := range names { // Unify presentation by going to lower case. @@ -159,7 +170,37 @@ func listTools(loaderstate *modload.State, ctx context.Context) { if cfg.BuildToolchainName == "gccgo" && !isGccgoTool(name) { continue } - fmt.Println(name) + addTool(name) + } + + // Also list builtin tools that can be built on demand. + // These are packages in cmd/ that would be installed to the tool directory. + cmdDir := filepath.Join(cfg.GOROOT, "src", "cmd") + entries, err := os.ReadDir(cmdDir) + if err == nil { + for _, entry := range entries { + if !entry.IsDir() { + continue + } + toolName := entry.Name() + // Skip packages that are not tools. + if toolName == "internal" || toolName == "vendor" { + continue + } + // Check if this tool is already in the tool directory. + if toolSet[toolName] { + continue + } + // Check if it's a valid builtin tool; add if so to keep deduped set. + if tool := loadBuiltinTool(toolName); tool != "" { + addTool(toolName) + } + } + } + + sort.Strings(toolNames) + for _, tool := range toolNames { + fmt.Println(tool) } loaderstate.InitWorkfile() diff --git a/src/cmd/go/internal/tool/tool_test.go b/src/cmd/go/internal/tool/tool_test.go new file mode 100644 index 00000000000000..d4a9186cc589af --- /dev/null +++ b/src/cmd/go/internal/tool/tool_test.go @@ -0,0 +1,124 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tool + +import ( + "os" + "path/filepath" + "testing" +) + +func TestListToolsBuiltinDiscovery(t *testing.T) { + // Test the directory scanning logic that was added to listTools + // This tests that we correctly identify directories and skip non-directories + + // Create a temporary directory structure to simulate cmd/ directory + tempDir := t.TempDir() + cmdDir := filepath.Join(tempDir, "cmd") + if err := os.MkdirAll(cmdDir, 0755); err != nil { + t.Fatal(err) + } + + // Create some tool directories + tools := []string{"vet", "cgo", "cover", "fix", "godoc"} + for _, tool := range tools { + toolDir := filepath.Join(cmdDir, tool) + if err := os.MkdirAll(toolDir, 0755); err != nil { + t.Fatal(err) + } + } + + // Create some non-tool directories that should be skipped + nonTools := []string{"internal", "vendor"} + for _, nonTool := range nonTools { + nonToolDir := filepath.Join(cmdDir, nonTool) + if err := os.MkdirAll(nonToolDir, 0755); err != nil { + t.Fatal(err) + } + } + + // Create a regular file (should be skipped) + filePath := filepath.Join(cmdDir, "not-a-directory.txt") + if err := os.WriteFile(filePath, []byte("test"), 0644); err != nil { + t.Fatal(err) + } + + // Test directory reading logic (simulating the logic from listTools) + entries, err := os.ReadDir(cmdDir) + if err != nil { + t.Fatal(err) + } + + var foundTools []string + for _, entry := range entries { + // Skip non-directories (this is the logic we added) + if !entry.IsDir() { + continue + } + + toolName := entry.Name() + // Skip packages that are not tools (this is the logic we added) + if toolName == "internal" || toolName == "vendor" { + continue + } + + foundTools = append(foundTools, toolName) + } + + // Sort for consistent comparison + // (In the real code, this happens via the toolSet map and final output) + for i := 0; i < len(foundTools)-1; i++ { + for j := i + 1; j < len(foundTools); j++ { + if foundTools[i] > foundTools[j] { + foundTools[i], foundTools[j] = foundTools[j], foundTools[i] + } + } + } + + // Verify we found the expected tools + expectedTools := []string{"cgo", "cover", "fix", "godoc", "vet"} + if len(foundTools) != len(expectedTools) { + t.Errorf("Found %d tools, expected %d: %v", len(foundTools), len(expectedTools), foundTools) + } + + for i, expected := range expectedTools { + if i >= len(foundTools) || foundTools[i] != expected { + t.Errorf("Expected tool %q at position %d, got %q", expected, i, foundTools[i]) + } + } +} + +func TestToolSetTracking(t *testing.T) { + // Test the toolSet map logic that prevents duplicates + // This tests part of the new functionality in listTools + + // Simulate the toolSet map logic + toolSet := make(map[string]bool) + + // Add some tools to the set (simulating tools found in tool directory) + existingTools := []string{"vet", "cgo"} + for _, tool := range existingTools { + toolSet[tool] = true + } + + // Test that existing tools are marked as present + for _, tool := range existingTools { + if !toolSet[tool] { + t.Errorf("Expected tool %q to be in toolSet", tool) + } + } + + // Test that new tools can be added and checked + newTools := []string{"cover", "fix"} + for _, tool := range newTools { + if toolSet[tool] { + t.Errorf("Expected new tool %q to not be in toolSet initially", tool) + } + toolSet[tool] = true + if !toolSet[tool] { + t.Errorf("Expected tool %q to be in toolSet after adding", tool) + } + } +}