228 lines
6.9 KiB
Go
228 lines
6.9 KiB
Go
// Package gotool owns Go configuration discovery and ReleaseKit's Go command
|
|
// environments.
|
|
package gotool
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"sync"
|
|
|
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/process"
|
|
)
|
|
|
|
// Mode selects the target environment for a Go command.
|
|
type Mode uint8
|
|
|
|
const (
|
|
// Native runs Go for the toolchain host and keeps its effective cgo settings.
|
|
Native Mode = iota
|
|
// Static runs Go for the selected target with cgo disabled.
|
|
Static
|
|
)
|
|
|
|
// Target selects a Go operating system and architecture.
|
|
type Target struct {
|
|
OS string
|
|
Arch string
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
var operationalSettings = []string{
|
|
"CC", "CXX", "CGO_CFLAGS", "CGO_CPPFLAGS", "CGO_CXXFLAGS", "CGO_ENABLED", "CGO_LDFLAGS",
|
|
"GOCACHE", "GOCACHEPROG", "GOMODCACHE", "GOPATH", "GOROOT", "GOTMPDIR", "PKG_CONFIG",
|
|
}
|
|
|
|
var architectureFeatures = []string{
|
|
"GOAMD64", "GOARM", "GOARM64", "GO386", "GOMIPS", "GOMIPS64", "GOPPC64", "GORISCV64", "GOWASM",
|
|
}
|
|
|
|
// New discovers the Go toolchain host and its effective cache, tool, and cgo
|
|
// settings. Discovery uses the selected local Go command and neutralizes
|
|
// ambient flags, experiments, and architecture tuning so they cannot prevent
|
|
// configuration from being read.
|
|
func New(ctx context.Context, directory string) (*Tool, error) {
|
|
var host struct {
|
|
OS string `json:"GOHOSTOS"`
|
|
Arch string `json:"GOHOSTARCH"`
|
|
}
|
|
output, err := discover(ctx, directory, discoveryEnvironment(), "go", "env", "-json", "GOHOSTOS", "GOHOSTARCH")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("identify Go host: %w", err)
|
|
}
|
|
if err := json.Unmarshal(output, &host); err != nil {
|
|
return nil, fmt.Errorf("decode Go host settings: %w", err)
|
|
}
|
|
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}}
|
|
|
|
environment := discoveryEnvironment()
|
|
environment["GOOS"] = tool.host.OS
|
|
environment["GOARCH"] = tool.host.Arch
|
|
args := append([]string{"go", "env", "-json"}, operationalSettings...)
|
|
output, err = discover(ctx, directory, environment, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve effective Go settings: %w", err)
|
|
}
|
|
var settings map[string]string
|
|
if err := json.Unmarshal(output, &settings); err != nil {
|
|
return nil, fmt.Errorf("decode effective Go settings: %w", err)
|
|
}
|
|
for _, key := range operationalSettings {
|
|
if _, ok := settings[key]; !ok {
|
|
return nil, fmt.Errorf("Go did not report effective setting %s", key)
|
|
}
|
|
}
|
|
tool.settings = settings
|
|
return tool, nil
|
|
}
|
|
|
|
// PrepareBuildDependencies downloads module dependencies with the available
|
|
// local Go toolchain. It preserves caller module-network settings and caches,
|
|
// while disabling workspace discovery and automatic toolchain downloads.
|
|
func PrepareBuildDependencies(ctx context.Context, directory string) error {
|
|
captured := &combinedWriter{}
|
|
err := process.Run(ctx, process.Invocation{
|
|
Args: []string{"go", "mod", "download"},
|
|
Dir: directory,
|
|
Env: map[string]string{
|
|
"GOWORK": "off",
|
|
"GOTOOLCHAIN": "local",
|
|
},
|
|
Stdout: captured,
|
|
Stderr: captured,
|
|
})
|
|
if err != nil {
|
|
if output := strings.TrimSpace(string(captured.Bytes())); output != "" {
|
|
return fmt.Errorf("go mod download: %w: %s", err, output)
|
|
}
|
|
return fmt.Errorf("go mod download: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HostTarget reports the Go command's host operating system and architecture.
|
|
func (tool *Tool) HostTarget() Target {
|
|
return tool.host
|
|
}
|
|
|
|
// Run executes a Go command with ReleaseKit's shared offline baseline. Native
|
|
// commands use the host target and effective cgo configuration. Static commands
|
|
// use target and disable cgo. Output, when supplied, receives stdout and stderr
|
|
// as the command runs; the returned bytes retain both streams for diagnostics.
|
|
func (tool *Tool) Run(ctx context.Context, directory string, mode Mode, target Target, output io.Writer, args ...string) ([]byte, error) {
|
|
if len(args) == 0 {
|
|
return nil, errors.New("Go command requires arguments")
|
|
}
|
|
environment := tool.environment(mode, target)
|
|
captured := &combinedWriter{output: output}
|
|
commandArgs := append([]string{"go"}, args...)
|
|
err := process.Run(ctx, process.Invocation{
|
|
Args: commandArgs,
|
|
Dir: directory,
|
|
Env: environment,
|
|
Stdout: captured,
|
|
Stderr: captured,
|
|
})
|
|
contents := captured.Bytes()
|
|
if err != nil {
|
|
if len(contents) != 0 {
|
|
return contents, fmt.Errorf("go %s: %w: %s", strings.Join(args, " "), err, strings.TrimSpace(string(contents)))
|
|
}
|
|
return contents, fmt.Errorf("go %s: %w", strings.Join(args, " "), err)
|
|
}
|
|
return contents, nil
|
|
}
|
|
|
|
func (tool *Tool) environment(mode Mode, target Target) map[string]string {
|
|
if mode == Native || target.OS == "" || target.Arch == "" {
|
|
target = tool.host
|
|
}
|
|
values := make(map[string]string, len(tool.settings)+len(architectureFeatures)+12)
|
|
for key, value := range tool.settings {
|
|
values[key] = value
|
|
}
|
|
values["GOENV"] = "off"
|
|
values["GOFLAGS"] = "-mod=readonly"
|
|
values["GOWORK"] = "off"
|
|
values["GOTOOLCHAIN"] = "local"
|
|
values["GOPROXY"] = "off"
|
|
values["GONOPROXY"] = "none"
|
|
values["GOSUMDB"] = "off"
|
|
values["GOOS"] = target.OS
|
|
values["GOARCH"] = target.Arch
|
|
values["GOEXPERIMENT"] = ""
|
|
for _, key := range architectureFeatures {
|
|
values[key] = ""
|
|
}
|
|
if mode == Static {
|
|
values["CGO_ENABLED"] = "0"
|
|
}
|
|
return values
|
|
}
|
|
|
|
func discoveryEnvironment() map[string]string {
|
|
environment := map[string]string{
|
|
"GOFLAGS": "-mod=readonly",
|
|
"GOTOOLCHAIN": "local",
|
|
"GOWORK": "off",
|
|
"GOEXPERIMENT": "none",
|
|
}
|
|
for _, key := range architectureFeatures {
|
|
environment[key] = ""
|
|
}
|
|
return environment
|
|
}
|
|
|
|
func discover(ctx context.Context, directory string, environment map[string]string, args ...string) ([]byte, error) {
|
|
var configuration, diagnostics bytes.Buffer
|
|
err := process.Run(ctx, process.Invocation{
|
|
Args: args,
|
|
Dir: directory,
|
|
Env: environment,
|
|
Stdout: &configuration,
|
|
Stderr: &diagnostics,
|
|
})
|
|
contents := configuration.Bytes()
|
|
if err != nil {
|
|
if output := strings.TrimSpace(configuration.String() + diagnostics.String()); output != "" {
|
|
return contents, fmt.Errorf("%s: %w: %s", strings.Join(args, " "), err, output)
|
|
}
|
|
return contents, fmt.Errorf("%s: %w", strings.Join(args, " "), err)
|
|
}
|
|
return contents, nil
|
|
}
|
|
|
|
type combinedWriter struct {
|
|
mu sync.Mutex
|
|
output io.Writer
|
|
buffer bytes.Buffer
|
|
}
|
|
|
|
func (writer *combinedWriter) Write(contents []byte) (int, error) {
|
|
writer.mu.Lock()
|
|
defer writer.mu.Unlock()
|
|
if writer.output == nil {
|
|
return writer.buffer.Write(contents)
|
|
}
|
|
return io.MultiWriter(&writer.buffer, writer.output).Write(contents)
|
|
}
|
|
|
|
func (writer *combinedWriter) Bytes() []byte {
|
|
writer.mu.Lock()
|
|
defer writer.mu.Unlock()
|
|
return append([]byte(nil), writer.buffer.Bytes()...)
|
|
}
|