From 5a39834033d105c8a31265c8b1cd643002c3bcba 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 in go tool output --- src/cmd/go/internal/tool/tool.go | 30 +++++++ src/cmd/go/internal/tool/tool_test.go | 124 ++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) 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 75a8fab78ad31b..848396105b1b31 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" @@ -147,6 +148,8 @@ func listTools(loaderstate *modload.State, ctx context.Context) { return } + toolSet := make(map[string]bool) + sort.Strings(names) for _, name := range names { // Unify presentation by going to lower case. @@ -158,9 +161,36 @@ func listTools(loaderstate *modload.State, ctx context.Context) { if cfg.BuildToolchainName == "gccgo" && !isGccgoTool(name) { continue } + toolSet[name] = true fmt.Println(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. + if tool := loadBuiltinTool(toolName); tool != "" { + toolSet[toolName] = true + fmt.Println(toolName) + } + } + } + modload.InitWorkfile(loaderstate) modload.LoadModFile(loaderstate, ctx) modTools := slices.Sorted(maps.Keys(loaderstate.MainModules.Tools())) 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) + } + } +}