(installDir string)
| 104 | } |
| 105 | |
| 106 | func installAt(installDir string) (string, error) { |
| 107 | if err := os.MkdirAll(installDir, 0755); err != nil { |
| 108 | return "", fmt.Errorf("creating install directory: %w", err) |
| 109 | } |
| 110 | version := sanitizeVersion(config.Version) |
| 111 | lockName := ".copilot-cli.lock" |
| 112 | if version != "" { |
| 113 | lockName = fmt.Sprintf(".copilot-cli-%s.lock", version) |
| 114 | } |
| 115 | |
| 116 | // Best effort to prevent concurrent installs. |
| 117 | if release, _ := flock.Acquire(filepath.Join(installDir, lockName)); release != nil { |
| 118 | defer release() |
| 119 | } |
| 120 | |
| 121 | binaryName := "copilot" |
| 122 | if runtime.GOOS == "windows" { |
| 123 | binaryName += ".exe" |
| 124 | } |
| 125 | finalPath := versionedBinaryPath(installDir, binaryName, version) |
| 126 | |
| 127 | if _, err := os.Stat(finalPath); err == nil { |
| 128 | existingHash, err := hashFile(finalPath) |
| 129 | if err != nil { |
| 130 | return "", fmt.Errorf("hashing existing binary: %w", err) |
| 131 | } |
| 132 | if !bytes.Equal(existingHash, config.CliHash) { |
| 133 | return "", fmt.Errorf("existing binary hash mismatch") |
| 134 | } |
| 135 | return finalPath, nil |
| 136 | } |
| 137 | |
| 138 | f, err := os.OpenFile(finalPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) |
| 139 | if err != nil { |
| 140 | return "", fmt.Errorf("creating binary file: %w", err) |
| 141 | } |
| 142 | _, err = io.Copy(f, config.Cli) |
| 143 | if err1 := f.Close(); err1 != nil && err == nil { |
| 144 | err = err1 |
| 145 | } |
| 146 | if closer, ok := config.Cli.(io.Closer); ok { |
| 147 | closer.Close() |
| 148 | } |
| 149 | if err != nil { |
| 150 | return "", fmt.Errorf("writing binary file: %w", err) |
| 151 | } |
| 152 | if len(config.License) > 0 { |
| 153 | licensePath := finalPath + ".license" |
| 154 | if err := os.WriteFile(licensePath, config.License, 0644); err != nil { |
| 155 | return "", fmt.Errorf("writing license file: %w", err) |
| 156 | } |
| 157 | } |
| 158 | return finalPath, nil |
| 159 | } |
| 160 | |
| 161 | // versionedBinaryPath builds the unpacked binary filename with an optional version suffix. |
| 162 | func versionedBinaryPath(dir, binaryName, version string) string { |
searching dependent graphs…