Files
ReleaseKit/internal/gotool/owner.go

123 lines
2.8 KiB
Go

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
}