// SPDX-License-Identifier: AGPL-3.0-only package process import ( "bytes" "context" "fmt" "os" "os/exec" "sort" "strings" ) const maxOutput = 4 << 20 type Runner interface { Run(ctx context.Context, dir string, env map[string]string, name string, args ...string) ([]byte, error) } type ExecRunner struct{} func (ExecRunner) Run(ctx context.Context, dir string, env map[string]string, name string, args ...string) ([]byte, error) { cmd := exec.CommandContext(ctx, name, args...) cmd.Dir = dir cmd.Env = mergeEnv(os.Environ(), env) var output limitedBuffer cmd.Stdout = &output cmd.Stderr = &output err := cmd.Run() if err != nil { return output.Bytes(), fmt.Errorf("%s failed: %w: %s", name, err, strings.TrimSpace(output.String())) } return output.Bytes(), nil } type limitedBuffer struct{ bytes.Buffer } func (b *limitedBuffer) Write(p []byte) (int, error) { written := len(p) remaining := maxOutput - b.Len() if remaining > 0 { if len(p) > remaining { p = p[:remaining] } _, _ = b.Buffer.Write(p) } return written, nil } func mergeEnv(base []string, extra map[string]string) []string { values := make(map[string]string, len(base)+len(extra)) for _, pair := range base { key, value, ok := strings.Cut(pair, "=") if ok { values[key] = value } } for key, value := range extra { values[key] = value } keys := make([]string, 0, len(values)) for key := range values { keys = append(keys, key) } sort.Strings(keys) out := make([]string, 0, len(keys)) for _, key := range keys { out = append(out, key+"="+values[key]) } return out }