|
8 | 8 | "errors" |
9 | 9 | "fmt" |
10 | 10 | "io" |
| 11 | + "os" |
11 | 12 | "os/exec" |
12 | 13 | "path/filepath" |
13 | 14 | "strings" |
@@ -50,13 +51,124 @@ func FromUTF16leToString(r io.Reader) (string, error) { |
50 | 51 | return string(out), nil |
51 | 52 | } |
52 | 53 |
|
53 | | -func WindowsSubsystemPath(ctx context.Context, orig string) (string, error) { |
54 | | - out, err := exec.CommandContext(ctx, "cygpath", filepath.ToSlash(orig)).CombinedOutput() |
55 | | - if err != nil { |
56 | | - logrus.WithError(err).Errorf("failed to convert path to mingw, maybe not using Git ssh?") |
57 | | - return "", err |
| 54 | +// WindowsSubsystemPath converts a Windows path into the form expected by |
| 55 | +// whichever SSH client Lima is about to invoke. It replaces a former |
| 56 | +// cygpath.exe subprocess; the conversion is now pure-Go and deterministic, |
| 57 | +// so it no longer depends on Cygwin/MSYS2 being installed on the host. |
| 58 | +// |
| 59 | +// Three target styles are picked from the environment: |
| 60 | +// - Win32-OpenSSH (default) → native path, slashes normalized. |
| 61 | +// - MSYS2 / Git-Bash (MSYSTEM set) → "/c/Users/me/...". |
| 62 | +// - Cygwin (CYGWIN set) → "/cygdrive/c/Users/me/...". |
| 63 | +// |
| 64 | +// The ctx parameter is accepted for signature compatibility with the |
| 65 | +// previous subprocess-based implementation; no syscall is performed. |
| 66 | +func WindowsSubsystemPath(_ context.Context, orig string) (string, error) { |
| 67 | + return convertWindowsSubsystemPath(detectSubsystemStyle(os.Getenv, exec.LookPath), orig) |
| 68 | +} |
| 69 | + |
| 70 | +// subsystemStyle is the target SSH-client path namespace. |
| 71 | +type subsystemStyle int |
| 72 | + |
| 73 | +const ( |
| 74 | + subsystemNative subsystemStyle = iota // Win32-OpenSSH |
| 75 | + subsystemMSYS // MSYS2 / MSYS / Git-Bash |
| 76 | + subsystemCygwin // Cygwin |
| 77 | +) |
| 78 | + |
| 79 | +// detectSubsystemStyle picks the style the downstream ssh binary expects. |
| 80 | +// Order: explicit env vars first (user-asserted intent), then a heuristic |
| 81 | +// over the resolved ssh binary path. getenv and lookPath are injected so |
| 82 | +// tests can drive every branch without touching the real environment. |
| 83 | +func detectSubsystemStyle(getenv func(string) string, lookPath func(string) (string, error)) subsystemStyle { |
| 84 | + if getenv("MSYSTEM") != "" { |
| 85 | + return subsystemMSYS |
58 | 86 | } |
59 | | - return strings.TrimSpace(string(out)), nil |
| 87 | + if getenv("CYGWIN") != "" { |
| 88 | + return subsystemCygwin |
| 89 | + } |
| 90 | + sshPath := getenv("SSH") |
| 91 | + if sshPath == "" && lookPath != nil { |
| 92 | + if p, err := lookPath("ssh"); err == nil { |
| 93 | + sshPath = p |
| 94 | + } |
| 95 | + } |
| 96 | + if sshPath == "" { |
| 97 | + return subsystemNative |
| 98 | + } |
| 99 | + low := strings.ToLower(strings.ReplaceAll(sshPath, `\`, `/`)) |
| 100 | + switch { |
| 101 | + case strings.Contains(low, "/cygwin"): |
| 102 | + return subsystemCygwin |
| 103 | + case strings.Contains(low, "/git/usr/bin/"), |
| 104 | + strings.Contains(low, "/msys64/"), |
| 105 | + strings.Contains(low, "/msys32/"), |
| 106 | + strings.Contains(low, "/mingw64/"), |
| 107 | + strings.Contains(low, "/mingw32/"): |
| 108 | + return subsystemMSYS |
| 109 | + default: |
| 110 | + // Win32-OpenSSH typically lives under |
| 111 | + // C:\Windows\System32\OpenSSH\ssh.exe. |
| 112 | + return subsystemNative |
| 113 | + } |
| 114 | +} |
| 115 | + |
| 116 | +// convertWindowsSubsystemPath translates an absolute Windows-style path |
| 117 | +// into the requested style. Pure string logic, no path/filepath calls, so |
| 118 | +// this is testable on any host (filepath's Windows semantics only kick in |
| 119 | +// when GOOS=windows). UNC inputs pass through with slashes normalized, |
| 120 | +// matching cygpath -u's behavior for UNC paths. |
| 121 | +func convertWindowsSubsystemPath(style subsystemStyle, orig string) (string, error) { |
| 122 | + vol := windowsVolumeName(orig) |
| 123 | + |
| 124 | + // UNC (\\server\share\...): preserve structure, normalize slashes. |
| 125 | + if strings.HasPrefix(vol, `\\`) || strings.HasPrefix(vol, `//`) { |
| 126 | + return strings.ReplaceAll(orig, `\`, `/`), nil |
| 127 | + } |
| 128 | + |
| 129 | + // Not a drive-letter path: return as-is (slash-normalized). |
| 130 | + if len(vol) < 2 { |
| 131 | + return strings.ReplaceAll(orig, `\`, `/`), nil |
| 132 | + } |
| 133 | + |
| 134 | + drive := strings.ToLower(string(vol[0])) |
| 135 | + rest := strings.ReplaceAll(orig[len(vol):], `\`, `/`) |
| 136 | + if !strings.HasPrefix(rest, "/") { |
| 137 | + rest = "/" + rest |
| 138 | + } |
| 139 | + |
| 140 | + switch style { |
| 141 | + case subsystemMSYS: |
| 142 | + return "/" + drive + rest, nil |
| 143 | + case subsystemCygwin: |
| 144 | + return "/cygdrive/" + drive + rest, nil |
| 145 | + default: |
| 146 | + return strings.ToUpper(string(vol[0])) + ":" + rest, nil |
| 147 | + } |
| 148 | +} |
| 149 | + |
| 150 | +// windowsVolumeName mirrors filepath.VolumeName's behavior for Windows |
| 151 | +// inputs, but works regardless of GOOS. Recognizes drive letters ("C:") |
| 152 | +// and UNC prefixes (\\server\share). Returns "" for anything else. |
| 153 | +func windowsVolumeName(p string) string { |
| 154 | + if len(p) >= 2 && p[1] == ':' && |
| 155 | + ((p[0] >= 'A' && p[0] <= 'Z') || (p[0] >= 'a' && p[0] <= 'z')) { |
| 156 | + return p[:2] |
| 157 | + } |
| 158 | + if len(p) >= 2 && (p[0] == '\\' || p[0] == '/') && (p[1] == '\\' || p[1] == '/') { |
| 159 | + rest := p[2:] |
| 160 | + serverEnd := strings.IndexAny(rest, `\/`) |
| 161 | + if serverEnd < 0 { |
| 162 | + return p |
| 163 | + } |
| 164 | + shareRest := rest[serverEnd+1:] |
| 165 | + shareEnd := strings.IndexAny(shareRest, `\/`) |
| 166 | + if shareEnd < 0 { |
| 167 | + return p |
| 168 | + } |
| 169 | + return p[:2+serverEnd+1+shareEnd] |
| 170 | + } |
| 171 | + return "" |
60 | 172 | } |
61 | 173 |
|
62 | 174 | func WindowsSubsystemPathForLinux(ctx context.Context, orig, distro string) (string, error) { |
|
0 commit comments