163 lines
5.4 KiB
Go
163 lines
5.4 KiB
Go
package windsurf
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
// ErrBinaryNotFound is returned when no Windsurf LS binary can be located
|
|
// via any configured source (env, explicit config, or platform candidates).
|
|
var ErrBinaryNotFound = fmt.Errorf("windsurf: language server binary not found")
|
|
|
|
// binaryStatFn reports whether the given path exists and is executable for
|
|
// the current platform. It is a package variable so tests can replace it
|
|
// with a map-backed implementation that does not touch the filesystem.
|
|
var binaryStatFn = defaultBinaryStat
|
|
|
|
// userHomeFn returns the user's home directory. Replaced in tests.
|
|
var userHomeFn = defaultUserHome
|
|
|
|
func defaultBinaryStat(path string) bool {
|
|
info, err := os.Stat(path)
|
|
if err != nil || info.IsDir() {
|
|
return false
|
|
}
|
|
if runtime.GOOS == "windows" {
|
|
// Windows ignores the Unix execute bit — rely on the .exe suffix.
|
|
return strings.HasSuffix(strings.ToLower(path), ".exe")
|
|
}
|
|
return info.Mode()&0o111 != 0
|
|
}
|
|
|
|
func defaultUserHome() string {
|
|
if dir, err := os.UserHomeDir(); err == nil {
|
|
return dir
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// DiscoverBinary resolves the Windsurf LS binary path for the current
|
|
// platform. Resolution order:
|
|
//
|
|
// 1. LS_BINARY_PATH environment variable (explicit override — user intent
|
|
// wins even if the path doesn't exist, so we can surface a clear error)
|
|
// 2. cfg.Binary (explicit config override)
|
|
// 3. Platform-specific candidate list (official install locations)
|
|
//
|
|
// Returns ErrBinaryNotFound when none of the sources yield an executable
|
|
// file; the error message directs the user to LS_BINARY_PATH or ls_mode=docker.
|
|
func DiscoverBinary(cfg LSPoolConfig) (string, error) {
|
|
return discoverBinaryFor(DetectPlatform(), os.Getenv("LS_BINARY_PATH"), cfg.Binary)
|
|
}
|
|
|
|
func discoverBinaryFor(p Platform, envPath, cfgPath string) (string, error) {
|
|
if envPath != "" {
|
|
return validateBinaryPath(envPath, p, "LS_BINARY_PATH")
|
|
}
|
|
if cfgPath != "" {
|
|
return validateBinaryPath(cfgPath, p, "cfg.Binary")
|
|
}
|
|
|
|
candidates, err := platformCandidates(p)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, path := range candidates {
|
|
if binaryStatFn(path) {
|
|
return path, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("%w for %s; searched %d paths (%s); set LS_BINARY_PATH or use ls_mode=docker",
|
|
ErrBinaryNotFound, p, len(candidates), strings.Join(candidates, ", "))
|
|
}
|
|
|
|
func validateBinaryPath(path string, p Platform, source string) (string, error) {
|
|
if binaryStatFn(path) {
|
|
return path, nil
|
|
}
|
|
hint := "file does not exist or is not executable"
|
|
if p.OS == "windows" && !strings.HasSuffix(strings.ToLower(path), ".exe") {
|
|
hint = "Windows LS binaries must end in .exe"
|
|
} else if p.OS != "windows" {
|
|
hint += " (try chmod +x)"
|
|
}
|
|
return "", fmt.Errorf("%w: %s=%q — %s; set LS_BINARY_PATH to a valid path or use ls_mode=docker",
|
|
ErrBinaryNotFound, source, path, hint)
|
|
}
|
|
|
|
// platformCandidates returns the ordered list of paths where the official
|
|
// Windsurf LS binary may be installed on the given platform. Paths are
|
|
// ordered by preference — the first existing+executable path wins.
|
|
func platformCandidates(p Platform) ([]string, error) {
|
|
filename, err := BinaryFilename(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch p.OS {
|
|
case "darwin":
|
|
return darwinCandidates(filename), nil
|
|
case "linux":
|
|
return linuxCandidates(filename), nil
|
|
case "windows":
|
|
return windowsCandidates(filename), nil
|
|
}
|
|
// BinaryFilename would have errored first, so this is defensive.
|
|
return nil, fmt.Errorf("%w: %s (no candidate list)", ErrUnsupportedPlatform, p)
|
|
}
|
|
|
|
func darwinCandidates(filename string) []string {
|
|
const bundleSubpath = "Contents/Resources/app/extensions/windsurf/bin"
|
|
candidates := []string{
|
|
filepath.Join("/Applications/Windsurf.app", bundleSubpath, filename),
|
|
}
|
|
if home := userHomeFn(); home != "" {
|
|
candidates = append(candidates,
|
|
filepath.Join(home, "Applications/Windsurf.app", bundleSubpath, filename),
|
|
)
|
|
}
|
|
// Legacy sub2api install (pre-cross-platform).
|
|
candidates = append(candidates, filepath.Join("/opt/windsurf", filename))
|
|
return candidates
|
|
}
|
|
|
|
func linuxCandidates(filename string) []string {
|
|
candidates := []string{
|
|
// Legacy sub2api install (pre-cross-platform) — matches existing deployments.
|
|
filepath.Join("/opt/windsurf", filename),
|
|
// Official Debian/RPM install locations.
|
|
filepath.Join("/usr/share/windsurf/resources/app/extensions/windsurf/bin", filename),
|
|
filepath.Join("/usr/lib/windsurf/resources/app/extensions/windsurf/bin", filename),
|
|
}
|
|
// User-local install (Flatpak/AppImage unpacked).
|
|
if home := userHomeFn(); home != "" {
|
|
candidates = append(candidates,
|
|
filepath.Join(home, ".local/share/windsurf/resources/app/extensions/windsurf/bin", filename),
|
|
)
|
|
}
|
|
return candidates
|
|
}
|
|
|
|
func windowsCandidates(filename string) []string {
|
|
// Split the install subpath into its components so filepath.Join produces
|
|
// a platform-native path on whichever OS this runs (Windows '\\', Unix '/').
|
|
// This matters for tests running on non-Windows builders.
|
|
installSubpath := []string{"resources", "app", "extensions", "windsurf", "bin", filename}
|
|
var candidates []string
|
|
if localAppData := os.Getenv("LOCALAPPDATA"); localAppData != "" {
|
|
candidates = append(candidates,
|
|
filepath.Join(append([]string{localAppData, "Programs", "Windsurf"}, installSubpath...)...),
|
|
)
|
|
}
|
|
if programFiles := os.Getenv("PROGRAMFILES"); programFiles != "" {
|
|
candidates = append(candidates,
|
|
filepath.Join(append([]string{programFiles, "Windsurf"}, installSubpath...)...),
|
|
)
|
|
}
|
|
return candidates
|
|
}
|