Files
2026-08-05 10:57:24 +01:00

184 lines
4.8 KiB
Go

package installer
import (
"context"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)
const windowsOS = "windows"
func resolveCommand(binary string, runner CommandRunner) (string, map[string]string, error) {
env, overrides, err := commandEnvironment(runner)
if err != nil {
return "", nil, err
}
path, err := lookPathInEnvironment(binary, env)
return path, overrides, err
}
// FindCommand resolves a binary with the same task environment used by the
// supplied command runner.
func FindCommand(binary string, runner CommandRunner) (string, error) {
env, _, err := commandEnvironment(runner)
if err != nil {
return "", err
}
return lookPathInEnvironment(binary, env)
}
// FindCommandInDirectory resolves a binary in one trusted directory with the
// task environment's platform-specific executable extensions.
func FindCommandInDirectory(binary, directory string, runner CommandRunner) (string, error) {
env, _, err := commandEnvironment(runner)
if err != nil {
return "", err
}
return findCommandInDirectory(binary, directory, env)
}
// CommandEnvironmentValue reads one value from the same merged process and
// task environment used for command lookup and execution.
func CommandEnvironmentValue(runner CommandRunner, key string) (string, error) {
env, _, err := commandEnvironment(runner)
if err != nil {
return "", err
}
return environmentValue(env, key), nil
}
func commandEnvironment(runner CommandRunner) (map[string]string, map[string]string, error) {
env := processEnvironment()
if runner == nil {
return env, nil, nil
}
overrides, err := runner.CommandEnvironment()
if err != nil {
return nil, nil, err
}
for key, value := range overrides {
setEnvironmentValue(env, key, value)
}
return env, overrides, nil
}
func processEnvironment() map[string]string {
env := make(map[string]string, len(os.Environ()))
for _, entry := range os.Environ() {
if separator := strings.IndexByte(entry, '='); separator > 0 {
setEnvironmentValue(env, entry[:separator], entry[separator+1:])
}
}
return env
}
func setEnvironmentValue(env map[string]string, key, value string) {
if runtime.GOOS == windowsOS {
for existing := range env {
if strings.EqualFold(existing, key) {
delete(env, existing)
}
}
}
env[key] = value
}
func environmentValue(env map[string]string, key string) string {
if value, ok := env[key]; ok {
return value
}
if runtime.GOOS == windowsOS {
for existing, value := range env {
if strings.EqualFold(existing, key) {
return value
}
}
}
return ""
}
func lookPathInEnvironment(binary string, env map[string]string) (string, error) {
if filepath.IsAbs(binary) || strings.ContainsRune(binary, filepath.Separator) {
if isExecutableFile(binary) {
return filepath.Abs(binary)
}
return "", &exec.Error{Name: binary, Err: exec.ErrNotFound}
}
for _, directory := range filepath.SplitList(environmentValue(env, "PATH")) {
if directory == "" || !filepath.IsAbs(directory) {
continue
}
if candidate, err := findCommandInDirectory(binary, directory, env); err == nil {
return candidate, nil
}
}
return "", &exec.Error{Name: binary, Err: exec.ErrNotFound}
}
func findCommandInDirectory(binary, directory string, env map[string]string) (string, error) {
if !filepath.IsAbs(directory) {
return "", &exec.Error{Name: binary, Err: exec.ErrNotFound}
}
for _, name := range executableNames(binary, env) {
candidate := filepath.Join(directory, name)
if isExecutableFile(candidate) {
return candidate, nil
}
}
return "", &exec.Error{Name: binary, Err: exec.ErrNotFound}
}
func executableNames(binary string, env map[string]string) []string {
if runtime.GOOS != windowsOS || filepath.Ext(binary) != "" {
return []string{binary}
}
extensions := environmentValue(env, "PATHEXT")
if extensions == "" {
extensions = ".COM;.EXE;.BAT;.CMD"
}
names := make([]string, 0, 4)
for _, extension := range strings.Split(extensions, ";") {
if extension != "" {
names = append(names, binary+strings.ToLower(extension))
}
}
return names
}
func isExecutableFile(path string) bool {
info, err := os.Stat(path)
if err != nil || info.IsDir() {
return false
}
return runtime.GOOS == windowsOS || info.Mode().Perm()&0o111 != 0
}
func combinedOutput(ctx context.Context, runner CommandRunner, spec CommandSpec) ([]byte, error) {
if runner != nil {
return runner.CombinedOutput(ctx, spec)
}
cmd := exec.CommandContext(ctx, spec.Path, spec.Args...)
cmd.Dir = spec.Dir
if len(spec.Env) > 0 {
cmd.Env = os.Environ()
for key, value := range spec.Env {
cmd.Env = upsertCommandEnv(cmd.Env, key, value)
}
}
return cmd.CombinedOutput()
}
func upsertCommandEnv(env []string, key, value string) []string {
prefix := key + "="
for i, entry := range env {
if strings.HasPrefix(entry, prefix) {
env[i] = prefix + value
return env
}
}
return append(env, prefix+value)
}