package gotool import ( "bytes" "context" "errors" "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 snapshotAvailable 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 && owner.snapshotAvailable { 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 } func (owner *Owner) discover(ctx context.Context) error { environment := inheritedEnvironment() tool, err := New(ctx, owner.directory) if err != nil { return err } settingsFile, snapshotErr := resolveSettingsFile(owner.directory, tool.settingsFile) var contents []byte var found bool if snapshotErr == nil { contents, found, snapshotErr = readSettingsFile(settingsFile) } owner.tool = tool owner.env = environment owner.file = settingsFile owner.contents = contents owner.fileFound = found owner.snapshotAvailable = snapshotErr == nil 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 }