Run validation in the selected module

This commit is contained in:
2026-10-10 02:43:23 +00:00
parent 02896b6e12
commit 8350cbf9c9
17 changed files with 577 additions and 55 deletions

View File

@@ -34,8 +34,9 @@ type Target struct {
// Tool holds the host target and effective operational settings resolved from
// the process environment and Go's persistent environment file.
type Tool struct {
host Target
settings map[string]string
host Target
settings map[string]string
settingsFile string
}
var operationalSettings = []string{
@@ -53,10 +54,11 @@ var architectureFeatures = []string{
// configuration from being read.
func New(ctx context.Context, directory string) (*Tool, error) {
var host struct {
OS string `json:"GOHOSTOS"`
Arch string `json:"GOHOSTARCH"`
OS string `json:"GOHOSTOS"`
Arch string `json:"GOHOSTARCH"`
SettingsFile string `json:"GOENV"`
}
output, err := discover(ctx, directory, discoveryEnvironment(), "go", "env", "-json", "GOHOSTOS", "GOHOSTARCH")
output, err := discover(ctx, directory, discoveryEnvironment(), "go", "env", "-json", "GOHOSTOS", "GOHOSTARCH", "GOENV")
if err != nil {
return nil, fmt.Errorf("identify Go host: %w", err)
}
@@ -66,7 +68,7 @@ func New(ctx context.Context, directory string) (*Tool, error) {
if host.OS == "" || host.Arch == "" {
return nil, fmt.Errorf("Go returned an incomplete host target: %s", strings.TrimSpace(string(output)))
}
tool := &Tool{host: Target{OS: host.OS, Arch: host.Arch}}
tool := &Tool{host: Target{OS: host.OS, Arch: host.Arch}, settingsFile: host.SettingsFile}
environment := discoveryEnvironment()
environment["GOOS"] = tool.host.OS

View File

@@ -301,6 +301,72 @@ func TestPersistentSettingsPreserveCachesAndNativeCTools(t *testing.T) {
}
}
func TestOwnerReusesSettingsUntilInheritedEnvironmentChanges(t *testing.T) {
clearGoSettings(t)
t.Setenv("GOENV", "off")
owner := NewOwner(t.TempDir())
first, err := owner.Acquire(context.Background())
if err != nil {
t.Fatal(err)
}
second, err := owner.Acquire(context.Background())
if err != nil {
t.Fatal(err)
}
if first != second {
t.Fatal("unchanged Go inputs caused settings discovery to be repeated")
}
cache := filepath.Join(t.TempDir(), "changed-cache")
t.Setenv("GOCACHE", cache)
if err := owner.Refresh(context.Background()); err != nil {
t.Fatal(err)
}
third, err := owner.Acquire(context.Background())
if err != nil {
t.Fatal(err)
}
if third == first || third.settings["GOCACHE"] != cache {
t.Fatalf("changed inherited environment kept stale settings: tool=%p GOCACHE=%q", third, third.settings["GOCACHE"])
}
}
func TestOwnerRefreshesWhenPersistentSettingsChange(t *testing.T) {
clearGoSettings(t)
firstCache := filepath.Join(t.TempDir(), "first-cache")
secondCache := filepath.Join(t.TempDir(), "second-cache")
settingsFile := filepath.Join(t.TempDir(), "go-env")
writeSettings := func(cache string) {
t.Helper()
if err := os.WriteFile(settingsFile, []byte("GOCACHE="+cache+"\n"), 0o600); err != nil {
t.Fatal(err)
}
}
writeSettings(firstCache)
t.Setenv("GOENV", settingsFile)
owner := NewOwner(t.TempDir())
first, err := owner.Acquire(context.Background())
if err != nil {
t.Fatal(err)
}
if first.settings["GOCACHE"] != firstCache {
t.Fatalf("initial GOCACHE = %q, want %q", first.settings["GOCACHE"], firstCache)
}
writeSettings(secondCache)
if err := owner.Refresh(context.Background()); err != nil {
t.Fatal(err)
}
second, err := owner.Acquire(context.Background())
if err != nil {
t.Fatal(err)
}
if second == first || second.settings["GOCACHE"] != secondCache {
t.Fatalf("changed persistent settings were not rediscovered: tool=%p GOCACHE=%q", second, second.settings["GOCACHE"])
}
}
func readSettings(t *testing.T, tool *Tool, mode Mode, target Target, names ...string) map[string]string {
t.Helper()
args := append([]string{"env", "-json"}, names...)

136
internal/gotool/owner.go Normal file
View File

@@ -0,0 +1,136 @@
package gotool
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
// Owner lazily discovers and reuses one Go tool while its inherited
// environment and persistent settings file remain unchanged.
type Owner struct {
directory string
tool *Tool
env map[string]string
file string
contents []byte
fileFound bool
}
// NewOwner creates an operation-local Go tool owner without running Go.
func NewOwner(directory string) *Owner {
return &Owner{directory: directory}
}
// Acquire returns the operation's Go tool, rediscovering settings when their
// process or persistent inputs have changed.
func (owner *Owner) Acquire(ctx context.Context) (*Tool, error) {
if owner.tool != nil {
changed, err := owner.inputsChanged()
if err == nil && !changed {
return owner.tool, nil
}
}
if err := owner.discover(ctx); err != nil {
return nil, err
}
return owner.tool, nil
}
// Refresh checks the owner's inputs after a consumer callback and rediscovers
// settings when the callback changed the environment or persistent file.
func (owner *Owner) Refresh(ctx context.Context) error {
if owner.tool == nil {
return nil
}
changed, err := owner.inputsChanged()
if err == nil && !changed {
return nil
}
return owner.discover(ctx)
}
func (owner *Owner) discover(ctx context.Context) error {
environment := inheritedEnvironment()
tool, err := New(ctx, owner.directory)
if err != nil {
return err
}
settingsFile, err := resolveSettingsFile(owner.directory, tool.settingsFile)
if err != nil {
return err
}
contents, found, err := readSettingsFile(settingsFile)
if err != nil {
return fmt.Errorf("read Go settings file %q: %w", settingsFile, err)
}
owner.tool = tool
owner.env = environment
owner.file = settingsFile
owner.contents = contents
owner.fileFound = found
return nil
}
func (owner *Owner) inputsChanged() (bool, error) {
if !sameEnvironment(owner.env, inheritedEnvironment()) {
return true, nil
}
contents, found, err := readSettingsFile(owner.file)
if err != nil {
return true, err
}
return found != owner.fileFound || !bytes.Equal(contents, owner.contents), nil
}
func inheritedEnvironment() map[string]string {
values := make(map[string]string)
for _, entry := range os.Environ() {
key, value, ok := strings.Cut(entry, "=")
if ok {
values[key] = value
}
}
return values
}
func sameEnvironment(left, right map[string]string) bool {
if len(left) != len(right) {
return false
}
for key, value := range left {
other, ok := right[key]
if !ok || other != value {
return false
}
}
return true
}
func resolveSettingsFile(directory, filename string) (string, error) {
if filename == "" || filename == "off" {
return "", nil
}
if !filepath.IsAbs(filename) {
filename = filepath.Join(directory, filename)
}
return filepath.Abs(filename)
}
func readSettingsFile(filename string) ([]byte, bool, error) {
if filename == "" {
return nil, false, nil
}
contents, err := os.ReadFile(filename)
if errors.Is(err, os.ErrNotExist) {
return nil, false, nil
}
if err != nil {
return nil, false, err
}
return contents, true, nil
}