🐛 fix(driver): improve Go PATH detection for optional driver builds

This commit is contained in:
DurianPankek
2026-04-09 16:16:22 +08:00
parent 82369b4070
commit 19da7fc66c
2 changed files with 238 additions and 1 deletions

View File

@@ -2,6 +2,7 @@ package app
import (
"archive/zip"
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
@@ -29,6 +30,86 @@ import (
"golang.org/x/mod/semver"
)
var (
goBinaryLookPath = exec.LookPath
goBinaryStat = os.Stat
goBinaryCommand = func(name string, arg ...string) *exec.Cmd {
return exec.Command(name, arg...)
}
)
// resolveGoBinaryPath 定位 Go 可执行文件,兼容 macOS 图形应用未继承 shell PATH 的场景 by AI.Coding
func resolveGoBinaryPath() (string, error) {
if goPath, err := goBinaryLookPath("go"); err == nil {
return goPath, nil
}
// 修复点GUI 进程常拿不到终端里的 PATH这里补充常见安装位置兜底。
commonCandidates := []string{
"/opt/homebrew/bin/go",
"/usr/local/go/bin/go",
"/usr/local/bin/go",
}
for _, candidate := range commonCandidates {
if info, err := goBinaryStat(candidate); err == nil && !info.IsDir() {
return candidate, nil
}
}
for _, shell := range candidateShellsForCommandLookup() {
cmd := goBinaryCommand(shell, "-lc", "command -v go")
output, err := cmd.Output()
if err != nil {
continue
}
goPath := resolveExistingPathFromCommandOutput(output)
if goPath == "" {
continue
}
if info, err := goBinaryStat(goPath); err == nil && !info.IsDir() {
return goPath, nil
}
}
return "", exec.ErrNotFound
}
// resolveExistingPathFromCommandOutput 从命令输出中提取真实存在的路径,避免 shell 启动脚本输出污染探测结果 by AI.Coding
func resolveExistingPathFromCommandOutput(value []byte) string {
for _, line := range bytes.Split(value, []byte{'\n'}) {
trimmed := strings.TrimSpace(string(line))
if trimmed != "" {
if info, err := goBinaryStat(trimmed); err == nil && !info.IsDir() {
return trimmed
}
}
}
return ""
}
// candidateShellsForCommandLookup 返回可能可用的 shell用于回收用户登录环境中的 PATH by AI.Coding
func candidateShellsForCommandLookup() []string {
seen := make(map[string]struct{}, 4)
result := make([]string, 0, 4)
appendShell := func(value string) {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return
}
if _, ok := seen[trimmed]; ok {
return
}
seen[trimmed] = struct{}{}
result = append(result, trimmed)
}
appendShell(os.Getenv("SHELL"))
appendShell("/bin/zsh")
appendShell("/bin/bash")
appendShell("/bin/sh")
return result
}
type driverDefinition struct {
Type string `json:"type"`
Name string `json:"name"`
@@ -3061,7 +3142,7 @@ func downloadOptionalDriverAgentFromBundle(a *App, definition driverDefinition,
func buildOptionalDriverAgentFromSource(definition driverDefinition, executablePath string, selectedVersion string) (string, error) {
driverType := normalizeDriverType(definition.Type)
displayName := resolveDriverDisplayName(definition)
goPath, lookErr := exec.LookPath("go")
goPath, lookErr := resolveGoBinaryPath()
if lookErr != nil {
return "", fmt.Errorf("当前环境未安装 Go且未找到可用的 %s 预编译代理包", displayName)
}

View File

@@ -0,0 +1,156 @@
package app
import (
"os"
"os/exec"
"strings"
"testing"
"time"
)
func TestCandidateShellsForCommandLookup_DedupesAndPreservesOrder(t *testing.T) {
originalShell := os.Getenv("SHELL")
t.Cleanup(func() {
_ = os.Setenv("SHELL", originalShell)
})
if err := os.Setenv("SHELL", "/bin/zsh"); err != nil {
t.Fatalf("set SHELL: %v", err)
}
got := candidateShellsForCommandLookup()
want := []string{"/bin/zsh", "/bin/bash", "/bin/sh"}
if len(got) != len(want) {
t.Fatalf("unexpected shell count: got %v want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("unexpected shell order: got %v want %v", got, want)
}
}
}
func TestResolveGoBinaryPath_FallsBackToKnownLocation(t *testing.T) {
originalLookPath := goBinaryLookPath
originalStat := goBinaryStat
originalCommand := goBinaryCommand
t.Cleanup(func() {
goBinaryLookPath = originalLookPath
goBinaryStat = originalStat
goBinaryCommand = originalCommand
})
goBinaryLookPath = func(file string) (string, error) {
return "", exec.ErrNotFound
}
goBinaryStat = func(name string) (os.FileInfo, error) {
if name == "/opt/homebrew/bin/go" {
return fakeFileInfo{name: "go"}, nil
}
return nil, os.ErrNotExist
}
goBinaryCommand = func(name string, arg ...string) *exec.Cmd {
t.Fatalf("shell fallback should not run when common path exists")
return nil
}
got, err := resolveGoBinaryPath()
if err != nil {
t.Fatalf("resolveGoBinaryPath returned error: %v", err)
}
if got != "/opt/homebrew/bin/go" {
t.Fatalf("unexpected go path: %s", got)
}
}
func TestResolveGoBinaryPath_FallsBackToShellOutput(t *testing.T) {
originalLookPath := goBinaryLookPath
originalStat := goBinaryStat
originalCommand := goBinaryCommand
originalShell := os.Getenv("SHELL")
t.Cleanup(func() {
goBinaryLookPath = originalLookPath
goBinaryStat = originalStat
goBinaryCommand = originalCommand
_ = os.Setenv("SHELL", originalShell)
})
if err := os.Setenv("SHELL", "/custom/shell"); err != nil {
t.Fatalf("set SHELL: %v", err)
}
goBinaryLookPath = func(file string) (string, error) {
return "", exec.ErrNotFound
}
goBinaryStat = func(name string) (os.FileInfo, error) {
if name == "/Users/test/go/bin/go" {
return fakeFileInfo{name: "go"}, nil
}
return nil, os.ErrNotExist
}
var called []string
goBinaryCommand = func(name string, arg ...string) *exec.Cmd {
called = append(called, name)
if len(called) == 1 {
return exec.Command("/bin/sh", "-c", "printf 'welcome\\n/Users/test/go/bin/go\\n'")
}
return exec.Command("/bin/sh", "-c", "exit 1")
}
got, err := resolveGoBinaryPath()
if err != nil {
t.Fatalf("resolveGoBinaryPath returned error: %v", err)
}
if got != "/Users/test/go/bin/go" {
t.Fatalf("unexpected go path from shell fallback: %s", got)
}
if len(called) == 0 || called[0] != "/custom/shell" {
t.Fatalf("expected shell lookup to start with SHELL env, got %v", called)
}
}
func TestResolveExistingPathFromCommandOutput_SkipsNoiseLines(t *testing.T) {
originalStat := goBinaryStat
t.Cleanup(func() {
goBinaryStat = originalStat
})
goBinaryStat = func(name string) (os.FileInfo, error) {
if name == "/opt/homebrew/bin/go" {
return fakeFileInfo{name: "go"}, nil
}
return nil, os.ErrNotExist
}
got := resolveExistingPathFromCommandOutput([]byte("\n notice \n /opt/homebrew/bin/go \n /usr/local/bin/go \n"))
if got != "/opt/homebrew/bin/go" {
t.Fatalf("unexpected parsed path: %q", got)
}
}
type fakeFileInfo struct {
name string
}
func (f fakeFileInfo) Name() string { return f.name }
func (f fakeFileInfo) Size() int64 { return 0 }
func (f fakeFileInfo) Mode() os.FileMode { return 0o755 }
func (f fakeFileInfo) ModTime() time.Time { return time.Time{} }
func (f fakeFileInfo) IsDir() bool { return false }
func (f fakeFileInfo) Sys() any { return nil }
func TestCandidateShellsForCommandLookup_IgnoresBlankShell(t *testing.T) {
originalShell := os.Getenv("SHELL")
t.Cleanup(func() {
_ = os.Setenv("SHELL", originalShell)
})
if err := os.Setenv("SHELL", " "); err != nil {
t.Fatalf("set SHELL: %v", err)
}
got := candidateShellsForCommandLookup()
joined := strings.Join(got, ",")
if strings.Contains(joined, " ") {
t.Fatalf("expected blank shell to be ignored: %v", got)
}
if len(got) != 3 {
t.Fatalf("unexpected shell list: %v", got)
}
}