mirror of
https://github.com/cheikh2shift/godex.git
synced 2026-10-21 02:29:56 -05:00
detect./install llama on startup
This commit is contained in:
1 parent
94f3174abd
commit
4f9e8ab45c
3 files changed
+397
-427
No files matched your search
@@ -571,7 +571,13 @@ func newLlamaProviderWithProgress(cfg *config.Provider, downloadProgress func(Do
|
||||
} else {
|
||||
serverPath, err = detectLlamaServer()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("llama.cpp provider requires llama-server: %w", err)
|
||||
if installErr := CheckOrInstallLlamaServer(bufio.NewReader(os.Stdin)); installErr != nil {
|
||||
return nil, fmt.Errorf("llama.cpp provider requires llama-server: %w", installErr)
|
||||
}
|
||||
serverPath, err = detectLlamaServer()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("llama.cpp provider requires llama-server: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
tokenizePath, _ = detectLlamaTokenize()
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type llamaServerProgressWriter struct {
|
||||
total int64
|
||||
downloaded int64
|
||||
lastPercent int
|
||||
}
|
||||
|
||||
func (pw *llamaServerProgressWriter) Write(p []byte) (int, error) {
|
||||
n := len(p)
|
||||
pw.downloaded += int64(n)
|
||||
if pw.total > 0 {
|
||||
percent := int(float64(pw.downloaded) / float64(pw.total) * 100)
|
||||
if percent != pw.lastPercent && percent <= 100 {
|
||||
pw.lastPercent = percent
|
||||
barWidth := 40
|
||||
filled := (barWidth * percent) / 100
|
||||
bar := strings.Repeat("=", filled) + strings.Repeat(" ", barWidth-filled)
|
||||
fmt.Printf("\r[%s] %d%% (%.1f MB)", bar, percent, float64(pw.downloaded)/1024/1024)
|
||||
if percent == 100 {
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// CheckOrInstallLlamaServer ensures llama-server is available, prompting to download if missing.
|
||||
func CheckOrInstallLlamaServer(reader *bufio.Reader) error {
|
||||
if _, err := detectLlamaServer(); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("llama-server not found in PATH or ~/.godex/")
|
||||
fmt.Println("To use llama.cpp, you need to install llama-server.")
|
||||
fmt.Println()
|
||||
|
||||
install := promptYesNo(reader, "Would you like to download and install llama-server now? (y/N)", false)
|
||||
if !install {
|
||||
return fmt.Errorf("llama-server not installed")
|
||||
}
|
||||
|
||||
fmt.Println("Fetching available llama-server releases...")
|
||||
assets, err := fetchLlamaReleases()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch releases: %w", err)
|
||||
}
|
||||
|
||||
if len(assets) == 0 {
|
||||
return fmt.Errorf("no llama-server releases found")
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("Available downloads:")
|
||||
fmt.Println()
|
||||
|
||||
osGroups := make(map[string][]LlamaAsset)
|
||||
for _, asset := range assets {
|
||||
dispName := getOSDisplayName(asset.OS)
|
||||
osGroups[dispName] = append(osGroups[dispName], asset)
|
||||
}
|
||||
|
||||
var options []string
|
||||
optionToAsset := make(map[int]LlamaAsset)
|
||||
idx := 1
|
||||
|
||||
currentAssetOS := goOSToAssetOS()
|
||||
currentOSGroup := getOSDisplayName(currentAssetOS)
|
||||
|
||||
if assets, ok := osGroups[currentOSGroup]; ok {
|
||||
fmt.Printf("%s:\n", currentOSGroup)
|
||||
for _, asset := range assets {
|
||||
fmt.Printf(" %d. %s (current OS)\n", idx, asset.FileName)
|
||||
optionToAsset[idx] = asset
|
||||
options = append(options, fmt.Sprintf("%s: %s", currentOSGroup, asset.FileName))
|
||||
idx++
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
fmt.Print("Select an option (number)")
|
||||
var defaultChoice int
|
||||
for i := range options {
|
||||
assetObj := optionToAsset[i+1]
|
||||
if getOSKey(assetObj.OS) == getOSKey(currentAssetOS) {
|
||||
defaultChoice = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
if defaultChoice == 0 {
|
||||
defaultChoice = 1
|
||||
}
|
||||
|
||||
fmt.Printf(" [default: %d]: ", defaultChoice)
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
input = strconv.Itoa(defaultChoice)
|
||||
}
|
||||
|
||||
choice, err := strconv.Atoi(input)
|
||||
if err != nil || choice < 1 || choice > len(options) {
|
||||
return fmt.Errorf("invalid selection")
|
||||
}
|
||||
|
||||
selectedAsset := optionToAsset[choice]
|
||||
downloadURL := selectedAsset.URL
|
||||
isZip := strings.HasSuffix(selectedAsset.FileName, ".zip")
|
||||
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
godexDir := filepath.Join(homeDir, ".godex")
|
||||
installDir := filepath.Join(godexDir, "bin")
|
||||
|
||||
if err := os.MkdirAll(installDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create install directory: %w", err)
|
||||
}
|
||||
|
||||
installPath := filepath.Join(installDir, "llama-server")
|
||||
|
||||
resp, err := http.Get(downloadURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return fmt.Errorf("download failed: asset not found (404)")
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("download failed with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
totalSize := resp.ContentLength
|
||||
fmt.Printf("Downloading %s...\n", downloadURL)
|
||||
if totalSize > 0 {
|
||||
fmt.Printf("Total size: %.1f MB\n", float64(totalSize)/1024/1024)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
ext := ".tar.gz"
|
||||
if isZip {
|
||||
ext = ".zip"
|
||||
}
|
||||
tmpFile := filepath.Join(os.TempDir(), "llama-server"+ext)
|
||||
outFile, err := os.Create(tmpFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
|
||||
writer := &llamaServerProgressWriter{total: totalSize}
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
n, err := resp.Body.Read(buf)
|
||||
if n > 0 {
|
||||
if _, werr := outFile.Write(buf[:n]); werr != nil {
|
||||
outFile.Close()
|
||||
return fmt.Errorf("failed to write: %w", werr)
|
||||
}
|
||||
writer.Write(buf[:n])
|
||||
}
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
outFile.Close()
|
||||
return fmt.Errorf("failed to read: %w", err)
|
||||
}
|
||||
}
|
||||
outFile.Close()
|
||||
|
||||
fmt.Println("Download complete. Extracting...")
|
||||
|
||||
if isZip {
|
||||
extractCmd := exec.Command("unzip", "-o", tmpFile, "-d", installDir)
|
||||
if err := extractCmd.Run(); err != nil {
|
||||
os.Remove(tmpFile)
|
||||
return fmt.Errorf("failed to extract: %w", err)
|
||||
}
|
||||
} else {
|
||||
extractCmd := exec.Command("tar", "-xzf", tmpFile, "-C", installDir)
|
||||
if err := extractCmd.Run(); err != nil {
|
||||
os.Remove(tmpFile)
|
||||
return fmt.Errorf("failed to extract: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
os.Remove(tmpFile)
|
||||
|
||||
entries, err := os.ReadDir(installDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list extracted files: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if strings.Contains(name, "llama-server") && !entry.IsDir() {
|
||||
extractedPath := filepath.Join(installDir, name)
|
||||
if installPath != extractedPath {
|
||||
if err := os.Rename(extractedPath, installPath); err != nil {
|
||||
if err := os.Chmod(extractedPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to set permissions: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := os.Chmod(installPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to set permissions: %w", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("llama-server installed to %s\n", installPath)
|
||||
fmt.Println("Make sure ~/.godex/bin/llama-server is in your PATH")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type LlamaAsset struct {
|
||||
OS string
|
||||
Arch string
|
||||
FileName string
|
||||
URL string
|
||||
}
|
||||
|
||||
func parseOSFromFilename(filename string) (os, arch string) {
|
||||
binMarker := "-bin-"
|
||||
|
||||
binIdx := strings.Index(filename, binMarker)
|
||||
if binIdx == -1 {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
suffixIdx := -1
|
||||
for _, s := range []string{".tar.gz", ".zip"} {
|
||||
idx := strings.Index(filename, s)
|
||||
if idx != -1 && (suffixIdx == -1 || idx < suffixIdx) {
|
||||
suffixIdx = idx
|
||||
}
|
||||
}
|
||||
if suffixIdx == -1 {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
middle := filename[binIdx+len(binMarker) : suffixIdx]
|
||||
|
||||
lastDash := strings.LastIndex(middle, "-")
|
||||
if lastDash == -1 {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
os = middle[:lastDash]
|
||||
arch = middle[lastDash+1:]
|
||||
|
||||
return os, arch
|
||||
}
|
||||
|
||||
type GitHubReleaseAsset struct {
|
||||
Name string `json:"name"`
|
||||
DownloadURL string `json:"browser_download_url"`
|
||||
}
|
||||
|
||||
type GitHubRelease struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Assets []GitHubReleaseAsset `json:"assets"`
|
||||
}
|
||||
|
||||
var httpDo = func(req *http.Request) (*http.Response, error) {
|
||||
client := &http.Client{}
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
func fetchLlamaReleases() ([]LlamaAsset, error) {
|
||||
req, err := http.NewRequest("GET", "https://api.github.com/repos/ggml-org/llama.cpp/releases", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
req.Header.Set("User-Agent", "godex")
|
||||
|
||||
resp, err := httpDo(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch releases: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("failed to fetch releases: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
var releases []GitHubRelease
|
||||
if err := json.Unmarshal(body, &releases); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse releases data: %w", err)
|
||||
}
|
||||
|
||||
if len(releases) == 0 {
|
||||
return nil, fmt.Errorf("no releases found")
|
||||
}
|
||||
|
||||
release := releases[0]
|
||||
|
||||
var assets []LlamaAsset
|
||||
for _, asset := range release.Assets {
|
||||
isValidExtension := strings.HasSuffix(asset.Name, ".tar.gz") || strings.HasSuffix(asset.Name, ".zip")
|
||||
if isValidExtension && strings.Contains(asset.Name, "-bin-") {
|
||||
osName, arch := parseOSFromFilename(asset.Name)
|
||||
if osName != "" && arch != "" {
|
||||
assets = append(assets, LlamaAsset{
|
||||
OS: osName,
|
||||
Arch: arch,
|
||||
FileName: asset.Name,
|
||||
URL: asset.DownloadURL,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return assets, nil
|
||||
}
|
||||
|
||||
func getOSDisplayName(osName string) string {
|
||||
switch {
|
||||
case osName == "linux" || strings.HasPrefix(osName, "ubuntu"):
|
||||
return "Linux"
|
||||
case osName == "darwin" || osName == "macos":
|
||||
return "macOS"
|
||||
case osName == "windows" || strings.HasPrefix(osName, "win"):
|
||||
return "Windows"
|
||||
default:
|
||||
return osName
|
||||
}
|
||||
}
|
||||
|
||||
func goOSToAssetOS() string {
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return "macos"
|
||||
case "linux":
|
||||
return "ubuntu"
|
||||
case "windows":
|
||||
return "win"
|
||||
default:
|
||||
return runtime.GOOS
|
||||
}
|
||||
}
|
||||
|
||||
func getOSKey(osName string) string {
|
||||
lower := strings.ToLower(osName)
|
||||
switch {
|
||||
case lower == "linux" || strings.HasPrefix(lower, "ubuntu"):
|
||||
return "linux"
|
||||
case lower == "darwin" || lower == "macos":
|
||||
return "darwin"
|
||||
case strings.HasPrefix(lower, "win"):
|
||||
return "windows"
|
||||
default:
|
||||
return lower
|
||||
}
|
||||
}
|
||||
|
||||
func promptYesNo(reader *bufio.Reader, question string, def bool) bool {
|
||||
fmt.Print(question + " ")
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.ToLower(strings.TrimSpace(input))
|
||||
if input == "" {
|
||||
return def
|
||||
}
|
||||
return strings.HasPrefix(input, "y")
|
||||
}
|
||||
+1
-426
@@ -158,7 +158,7 @@ func RunWizard(destination string) error {
|
||||
provider.Params["backend"] = "ollama"
|
||||
} else if provider.Type == "llama" {
|
||||
provider.Params["backend"] = "llama"
|
||||
if err := checkOrInstallLlamaServer(reader); err != nil {
|
||||
if err := providers.CheckOrInstallLlamaServer(reader); err != nil {
|
||||
fmt.Printf("Warning: %v\n", err)
|
||||
fmt.Println("You can still configure the provider, but it may not work until llama-server is available.")
|
||||
}
|
||||
@@ -671,428 +671,3 @@ func exchangeCodeForKey(code, codeVerifier string) (string, error) {
|
||||
|
||||
return result.Key, nil
|
||||
}
|
||||
|
||||
type progressWriter struct {
|
||||
total int64
|
||||
downloaded int64
|
||||
lastPercent int
|
||||
}
|
||||
|
||||
func (pw *progressWriter) Write(p []byte) (int, error) {
|
||||
n := len(p)
|
||||
pw.downloaded += int64(n)
|
||||
if pw.total > 0 {
|
||||
percent := int(float64(pw.downloaded) / float64(pw.total) * 100)
|
||||
if percent != pw.lastPercent && percent <= 100 {
|
||||
pw.lastPercent = percent
|
||||
barWidth := 40
|
||||
filled := (barWidth * percent) / 100
|
||||
bar := strings.Repeat("=", filled) + strings.Repeat(" ", barWidth-filled)
|
||||
fmt.Printf("\r[%s] %d%% (%.1f MB)", bar, percent, float64(pw.downloaded)/1024/1024)
|
||||
if percent == 100 {
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func checkOrInstallLlamaServer(reader *bufio.Reader) error {
|
||||
paths := []string{}
|
||||
|
||||
godexDir := os.Getenv("GODEX_DIR")
|
||||
if godexDir != "" {
|
||||
paths = append(paths,
|
||||
filepath.Join(godexDir, "llama-server"),
|
||||
filepath.Join(godexDir, "bin", "llama-server"),
|
||||
)
|
||||
}
|
||||
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
if homeDir != "" {
|
||||
paths = append(paths,
|
||||
filepath.Join(homeDir, ".godex", "llama-server"),
|
||||
filepath.Join(homeDir, ".godex", "bin", "llama-server"),
|
||||
)
|
||||
}
|
||||
|
||||
for _, p := range paths {
|
||||
if fi, err := os.Stat(p); err == nil && !fi.IsDir() {
|
||||
if err := os.Chmod(p, 0755); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := exec.LookPath("llama-server"); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
searchDirs := []string{}
|
||||
if godexDir != "" {
|
||||
searchDirs = append(searchDirs, filepath.Join(godexDir, "bin"))
|
||||
}
|
||||
if homeDir != "" {
|
||||
searchDirs = append(searchDirs, filepath.Join(homeDir, ".godex", "bin"))
|
||||
}
|
||||
|
||||
for _, dir := range searchDirs {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() && strings.HasPrefix(strings.ToLower(entry.Name()), "llama") {
|
||||
subDir := filepath.Join(dir, entry.Name())
|
||||
if subEntries, err := os.ReadDir(subDir); err == nil {
|
||||
for _, subEntry := range subEntries {
|
||||
if !subEntry.IsDir() && strings.Contains(strings.ToLower(subEntry.Name()), "llama-server") {
|
||||
llamaPath := filepath.Join(subDir, subEntry.Name())
|
||||
os.Chmod(llamaPath, 0755)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("llama-server not found in PATH or ~/.godex/")
|
||||
fmt.Println("To use llama.cpp, you need to install llama-server.")
|
||||
fmt.Println()
|
||||
|
||||
install := promptYesNo(reader, "Would you like to download and install llama-server now? (y/N)", false)
|
||||
if !install {
|
||||
return fmt.Errorf("llama-server not installed")
|
||||
}
|
||||
|
||||
fmt.Println("Fetching available llama-server releases...")
|
||||
assets, err := fetchLlamaReleases()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch releases: %w", err)
|
||||
}
|
||||
|
||||
if len(assets) == 0 {
|
||||
return fmt.Errorf("no llama-server releases found")
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("Available downloads:")
|
||||
fmt.Println()
|
||||
|
||||
osGroups := make(map[string][]LlamaAsset)
|
||||
for _, asset := range assets {
|
||||
dispName := getOSDisplayName(asset.OS)
|
||||
osGroups[dispName] = append(osGroups[dispName], asset)
|
||||
}
|
||||
|
||||
var options []string
|
||||
optionToAsset := make(map[int]LlamaAsset)
|
||||
idx := 1
|
||||
|
||||
currentAssetOS := goOSToAssetOS()
|
||||
currentOSGroup := getOSDisplayName(currentAssetOS)
|
||||
|
||||
if assets, ok := osGroups[currentOSGroup]; ok {
|
||||
fmt.Printf("%s:\n", currentOSGroup)
|
||||
for _, asset := range assets {
|
||||
fmt.Printf(" %d. %s (current OS)\n", idx, asset.FileName)
|
||||
optionToAsset[idx] = asset
|
||||
options = append(options, fmt.Sprintf("%s: %s", currentOSGroup, asset.FileName))
|
||||
idx++
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
fmt.Print("Select an option (number)")
|
||||
var defaultChoice int
|
||||
for i := range options {
|
||||
assetObj := optionToAsset[i+1]
|
||||
if getOSKey(assetObj.OS) == getOSKey(currentAssetOS) {
|
||||
defaultChoice = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
if defaultChoice == 0 {
|
||||
defaultChoice = 1
|
||||
}
|
||||
|
||||
fmt.Printf(" [default: %d]: ", defaultChoice)
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
input = strconv.Itoa(defaultChoice)
|
||||
}
|
||||
|
||||
choice, err := strconv.Atoi(input)
|
||||
if err != nil || choice < 1 || choice > len(options) {
|
||||
return fmt.Errorf("invalid selection")
|
||||
}
|
||||
|
||||
selectedAsset := optionToAsset[choice]
|
||||
downloadURL := selectedAsset.URL
|
||||
isZip := strings.HasSuffix(selectedAsset.FileName, ".zip")
|
||||
|
||||
godexDir = filepath.Join(homeDir, ".godex")
|
||||
installDir := filepath.Join(godexDir, "bin")
|
||||
|
||||
if err := os.MkdirAll(installDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create install directory: %w", err)
|
||||
}
|
||||
|
||||
installPath := filepath.Join(installDir, "llama-server")
|
||||
|
||||
resp, err := http.Get(downloadURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return fmt.Errorf("download failed: asset not found (404)")
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("download failed with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
totalSize := resp.ContentLength
|
||||
fmt.Printf("Downloading %s...\n", downloadURL)
|
||||
if totalSize > 0 {
|
||||
fmt.Printf("Total size: %.1f MB\n", float64(totalSize)/1024/1024)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
ext := ".tar.gz"
|
||||
if isZip {
|
||||
ext = ".zip"
|
||||
}
|
||||
tmpFile := filepath.Join(os.TempDir(), "llama-server"+ext)
|
||||
outFile, err := os.Create(tmpFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
|
||||
writer := &progressWriter{total: totalSize}
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
n, err := resp.Body.Read(buf)
|
||||
if n > 0 {
|
||||
if _, werr := outFile.Write(buf[:n]); werr != nil {
|
||||
outFile.Close()
|
||||
return fmt.Errorf("failed to write: %w", werr)
|
||||
}
|
||||
writer.Write(buf[:n])
|
||||
}
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
outFile.Close()
|
||||
return fmt.Errorf("failed to read: %w", err)
|
||||
}
|
||||
}
|
||||
outFile.Close()
|
||||
|
||||
fmt.Println("Download complete. Extracting...")
|
||||
|
||||
if isZip {
|
||||
extractCmd := exec.Command("unzip", "-o", tmpFile, "-d", installDir)
|
||||
if err := extractCmd.Run(); err != nil {
|
||||
os.Remove(tmpFile)
|
||||
return fmt.Errorf("failed to extract: %w", err)
|
||||
}
|
||||
} else {
|
||||
extractCmd := exec.Command("tar", "-xzf", tmpFile, "-C", installDir)
|
||||
if err := extractCmd.Run(); err != nil {
|
||||
os.Remove(tmpFile)
|
||||
return fmt.Errorf("failed to extract: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
os.Remove(tmpFile)
|
||||
|
||||
entries, err := os.ReadDir(installDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list extracted files: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if strings.Contains(name, "llama-server") && !entry.IsDir() {
|
||||
extractedPath := filepath.Join(installDir, name)
|
||||
if installPath != extractedPath {
|
||||
if err := os.Rename(extractedPath, installPath); err != nil {
|
||||
if err := os.Chmod(extractedPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to set permissions: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := os.Chmod(installPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to set permissions: %w", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("llama-server installed to %s\n", installPath)
|
||||
fmt.Println("Make sure ~/.godex/bin/llama-server is in your PATH")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type LlamaAsset struct {
|
||||
OS string
|
||||
Arch string
|
||||
FileName string
|
||||
URL string
|
||||
}
|
||||
|
||||
func parseOSFromFilename(filename string) (os, arch string) {
|
||||
binMarker := "-bin-"
|
||||
|
||||
binIdx := strings.Index(filename, binMarker)
|
||||
if binIdx == -1 {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
suffixIdx := -1
|
||||
for _, s := range []string{".tar.gz", ".zip"} {
|
||||
idx := strings.Index(filename, s)
|
||||
if idx != -1 && (suffixIdx == -1 || idx < suffixIdx) {
|
||||
suffixIdx = idx
|
||||
}
|
||||
}
|
||||
if suffixIdx == -1 {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
middle := filename[binIdx+len(binMarker) : suffixIdx]
|
||||
|
||||
lastDash := strings.LastIndex(middle, "-")
|
||||
if lastDash == -1 {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
os = middle[:lastDash]
|
||||
arch = middle[lastDash+1:]
|
||||
|
||||
return os, arch
|
||||
}
|
||||
|
||||
type GitHubReleaseAsset struct {
|
||||
Name string `json:"name"`
|
||||
DownloadURL string `json:"browser_download_url"`
|
||||
}
|
||||
|
||||
type GitHubRelease struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Assets []GitHubReleaseAsset `json:"assets"`
|
||||
}
|
||||
|
||||
var httpDo = func(req *http.Request) (*http.Response, error) {
|
||||
client := &http.Client{}
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
func fetchLlamaReleases() ([]LlamaAsset, error) {
|
||||
req, err := http.NewRequest("GET", "https://api.github.com/repos/ggml-org/llama.cpp/releases", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
req.Header.Set("User-Agent", "godex")
|
||||
|
||||
resp, err := httpDo(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch releases: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("failed to fetch releases: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
var releases []GitHubRelease
|
||||
if err := json.Unmarshal(body, &releases); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse releases data: %w", err)
|
||||
}
|
||||
|
||||
if len(releases) == 0 {
|
||||
return nil, fmt.Errorf("no releases found")
|
||||
}
|
||||
|
||||
release := releases[0]
|
||||
|
||||
var assets []LlamaAsset
|
||||
for _, asset := range release.Assets {
|
||||
isValidExtension := strings.HasSuffix(asset.Name, ".tar.gz") || strings.HasSuffix(asset.Name, ".zip")
|
||||
if isValidExtension && strings.Contains(asset.Name, "-bin-") {
|
||||
osName, arch := parseOSFromFilename(asset.Name)
|
||||
if osName != "" && arch != "" {
|
||||
assets = append(assets, LlamaAsset{
|
||||
OS: osName,
|
||||
Arch: arch,
|
||||
FileName: asset.Name,
|
||||
URL: asset.DownloadURL,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return assets, nil
|
||||
}
|
||||
|
||||
func mustParseURL(rawURL string) *url.URL {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func getOSDisplayName(osName string) string {
|
||||
switch {
|
||||
case osName == "linux" || strings.HasPrefix(osName, "ubuntu"):
|
||||
return "Linux"
|
||||
case osName == "darwin" || osName == "macos":
|
||||
return "macOS"
|
||||
case osName == "windows" || strings.HasPrefix(osName, "win"):
|
||||
return "Windows"
|
||||
default:
|
||||
return osName
|
||||
}
|
||||
}
|
||||
|
||||
func goOSToAssetOS() string {
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return "macos"
|
||||
case "linux":
|
||||
return "ubuntu"
|
||||
case "windows":
|
||||
return "win"
|
||||
default:
|
||||
return runtime.GOOS
|
||||
}
|
||||
}
|
||||
|
||||
func getOSKey(osName string) string {
|
||||
lower := strings.ToLower(osName)
|
||||
switch {
|
||||
case lower == "linux" || strings.HasPrefix(lower, "ubuntu"):
|
||||
return "linux"
|
||||
case lower == "darwin" || lower == "macos":
|
||||
return "darwin"
|
||||
case strings.HasPrefix(lower, "win"):
|
||||
return "windows"
|
||||
default:
|
||||
return lower
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user