mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
- module github.com/R0m1k3/Loki, cmd/loki, internal/loki (package loki) - LOKI_HOME, LOKI_MODEL_DIRS, LOKI_SERVICE, LOKI_DL_CONNS ; /etc/loki ; units loki-engine / loki-ui ; binaire et aide CLI - updateRepo pointe sur R0m1k3/Loki (l'auto-update ne tirera plus les binaires AJEAN amont) Conservé à l'identique : le domaine ajean.link (service de tunnel amont), les littéraux de migration 0.7.x (migrate_07.go), RELEASE_NOTES.md et LICENSE (historique et licence de l'amont). go build/vet/test : verts.
631 lines
19 KiB
Go
631 lines
19 KiB
Go
// backend_prebuilt.go — backend llama.cpp SANS compilation : télécharge les
|
|
// binaires officiels précompilés publiés à chaque release de ggml-org/llama.cpp
|
|
// (zip Windows, tar.gz macOS/Linux), choisit l'asset adapté à la machine
|
|
// (CUDA / ROCm / Vulkan / CPU), l'extrait dans backends/llama.cpp-prebuilt et
|
|
// pointe BIN dessus. ~2 minutes au lieu d'une compilation complète.
|
|
//
|
|
// Limites assumées : builds génériques (pas de tuning natif), et pas de build
|
|
// CUDA officiel pour Linux (on retombe sur Vulkan — la compilation locale
|
|
// reste la voie CUDA sous Linux). La compilation locale reste indispensable
|
|
// pour les forks (ex. PrismML).
|
|
package loki
|
|
|
|
import (
|
|
"archive/tar"
|
|
"archive/zip"
|
|
"compress/gzip"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const llamaReleasesAPI = "https://api.github.com/repos/ggml-org/llama.cpp/releases/latest"
|
|
|
|
type ghAsset struct {
|
|
Name string `json:"name"`
|
|
URL string `json:"browser_download_url"`
|
|
Size int64 `json:"size"`
|
|
}
|
|
|
|
func prebuiltDir() string {
|
|
return filepath.Join(backendsDir(), "llama.cpp-prebuilt")
|
|
}
|
|
|
|
// prebuiltVersion lit le marqueur VERSION du dossier prebuilt : "tag cudaVer"
|
|
// (cudaVer vide hors CUDA Windows).
|
|
func prebuiltVersion() (tag, cudaVer string) {
|
|
b, err := os.ReadFile(filepath.Join(prebuiltDir(), "VERSION"))
|
|
if err != nil {
|
|
return "", ""
|
|
}
|
|
f := strings.Fields(strings.TrimSpace(string(b)))
|
|
if len(f) > 0 {
|
|
tag = f[0]
|
|
}
|
|
if len(f) > 1 {
|
|
cudaVer = f[1]
|
|
}
|
|
return
|
|
}
|
|
|
|
// prebuiltFormat identifie la façon dont l'archive a été extraite. « fmt2 » =
|
|
// extraction qui recrée les liens des archives (indispensable aux .dylib macOS
|
|
// et .so Linux). Une installation sans ce marqueur est réputée incomplète et
|
|
// sera refaite au lieu d'être déclarée « déjà à jour ».
|
|
const prebuiltFormat = "fmt2"
|
|
|
|
// prebuiltVersionFormat lit le 3e champ du marqueur VERSION ("" si absent).
|
|
func prebuiltVersionFormat() string {
|
|
b, err := os.ReadFile(filepath.Join(prebuiltDir(), "VERSION"))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
f := strings.Fields(strings.TrimSpace(string(b)))
|
|
if len(f) > 2 {
|
|
return f[2]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// prebuiltServerBin localise llama-server(.exe) sous le dossier prebuilt
|
|
// (l'arborescence interne des archives officielles varie : racine, build/bin…).
|
|
//
|
|
// Chaque release s'extrait dans son propre sous-dossier (llama-b10280/…), donc
|
|
// plusieurs versions peuvent cohabiter — notamment des installations anciennes
|
|
// et incomplètes. On retient donc EN PRIORITÉ le binaire de la version notée
|
|
// dans VERSION, et à défaut le plus récent (numéro de build le plus élevé) :
|
|
// prendre « le premier trouvé » renvoyait le plus ANCIEN dossier, donc un
|
|
// binaire périmé voire cassé.
|
|
func prebuiltServerBin() string {
|
|
all := prebuiltServerBins()
|
|
if len(all) == 0 {
|
|
return ""
|
|
}
|
|
if tag, _ := prebuiltVersion(); tag != "" {
|
|
for _, p := range all {
|
|
if strings.Contains(filepath.ToSlash(p), "/llama-"+tag+"/") {
|
|
return p
|
|
}
|
|
}
|
|
}
|
|
return all[len(all)-1] // ordre lexical = ordre des numéros de build
|
|
}
|
|
|
|
// prebuiltServerBins liste tous les llama-server présents sous le dossier
|
|
// prebuilt, triés par chemin (donc par numéro de build croissant).
|
|
func prebuiltServerBins() []string {
|
|
want := "llama-server"
|
|
if runtime.GOOS == "windows" {
|
|
want += ".exe"
|
|
}
|
|
var found []string
|
|
_ = filepath.WalkDir(prebuiltDir(), func(p string, d os.DirEntry, err error) error {
|
|
if err != nil || d.IsDir() {
|
|
return nil
|
|
}
|
|
if d.Name() == want {
|
|
found = append(found, p)
|
|
}
|
|
return nil
|
|
})
|
|
sort.Strings(found)
|
|
return found
|
|
}
|
|
|
|
// prebuiltOwns dit si p désigne un binaire du moteur précompilé, quelle que
|
|
// soit la version installée. Les presets enregistrent un chemin versionné :
|
|
// après une mise à jour ce chemin n'est plus celui du moteur courant, mais le
|
|
// preset utilise bien « le moteur précompilé » et doit être reconnu comme tel.
|
|
func prebuiltOwns(p string) bool {
|
|
if p == "" {
|
|
return false
|
|
}
|
|
norm := func(s string) string {
|
|
return strings.ToLower(filepath.ToSlash(filepath.Clean(s)))
|
|
}
|
|
return strings.HasPrefix(norm(p), norm(prebuiltDir())+"/")
|
|
}
|
|
|
|
// prebuiltResolveBin fait suivre un BIN de preset aux mises à jour du moteur :
|
|
// un chemin versionné qui n'existe plus (release remplacée) est remplacé par le
|
|
// binaire précompilé courant. Tout autre chemin est renvoyé tel quel.
|
|
func prebuiltResolveBin(bin string) string {
|
|
if !prebuiltOwns(bin) {
|
|
return bin
|
|
}
|
|
if _, err := os.Stat(bin); err == nil {
|
|
return bin
|
|
}
|
|
if cur := prebuiltServerBin(); cur != "" {
|
|
return cur
|
|
}
|
|
return bin
|
|
}
|
|
|
|
// prebuiltPrune supprime les extractions d'autres releases : elles ne servent
|
|
// plus à rien (quelques centaines de Mo chacune) et, laissées en place, elles
|
|
// se faisaient élire comme binaire courant.
|
|
func prebuiltPrune(keep string, logf func(string)) {
|
|
keepDir := ""
|
|
if keep != "" {
|
|
keepDir = filepath.ToSlash(filepath.Clean(keep))
|
|
}
|
|
entries, err := os.ReadDir(prebuiltDir())
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, e := range entries {
|
|
if !e.IsDir() || !strings.HasPrefix(e.Name(), "llama-b") {
|
|
continue
|
|
}
|
|
d := filepath.Join(prebuiltDir(), e.Name())
|
|
if keepDir != "" && strings.HasPrefix(keepDir, filepath.ToSlash(filepath.Clean(d))+"/") {
|
|
continue
|
|
}
|
|
if err := os.RemoveAll(d); err == nil && logf != nil {
|
|
logf("ancienne version supprimée : " + e.Name())
|
|
}
|
|
}
|
|
}
|
|
|
|
// fetchLlamaLatest interroge l'API GitHub pour la dernière release officielle.
|
|
func fetchLlamaLatest() (string, []ghAsset, error) {
|
|
req, err := http.NewRequest("GET", llamaReleasesAPI, nil)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
req.Header.Set("Accept", "application/vnd.github+json")
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != 200 {
|
|
return "", nil, fmt.Errorf("GitHub API : HTTP %d", resp.StatusCode)
|
|
}
|
|
var rel struct {
|
|
TagName string `json:"tag_name"`
|
|
Assets []ghAsset `json:"assets"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil {
|
|
return "", nil, err
|
|
}
|
|
if rel.TagName == "" {
|
|
return "", nil, fmt.Errorf("release invalide (tag vide)")
|
|
}
|
|
return rel.TagName, rel.Assets, nil
|
|
}
|
|
|
|
// driverCudaVersion renvoie la version CUDA max supportée par le pilote NVIDIA
|
|
// (bandeau de nvidia-smi : « CUDA Version: 12.8 »), ou 0 si inconnue.
|
|
func driverCudaVersion() float64 {
|
|
out, err := hideCmd(exec.Command("nvidia-smi")).Output()
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
m := regexp.MustCompile(`CUDA Version:\s*([0-9]+\.[0-9]+)`).FindSubmatch(out)
|
|
if m == nil {
|
|
return 0
|
|
}
|
|
v, _ := strconv.ParseFloat(string(m[1]), 64)
|
|
return v
|
|
}
|
|
|
|
// assetMatch garde les assets dont le nom contient TOUS les fragments.
|
|
func assetMatch(assets []ghAsset, frags ...string) []ghAsset {
|
|
var out []ghAsset
|
|
for _, a := range assets {
|
|
ok := true
|
|
for _, f := range frags {
|
|
if !strings.Contains(a.Name, f) {
|
|
ok = false
|
|
break
|
|
}
|
|
}
|
|
if ok {
|
|
out = append(out, a)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
var reCudaAssetVer = regexp.MustCompile(`cuda-([0-9]+\.[0-9]+)`)
|
|
|
|
// pickPrebuilt choisit l'asset principal (+ cudart pour CUDA Windows) adapté à
|
|
// la machine, et renvoie un label lisible du variant retenu.
|
|
func pickPrebuilt(assets []ghAsset) (main *ghAsset, cudart *ghAsset, label, cudaVer string, err error) {
|
|
pickOne := func(list []ghAsset) *ghAsset {
|
|
if len(list) == 0 {
|
|
return nil
|
|
}
|
|
return &list[0]
|
|
}
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
if runtime.GOARCH == "arm64" {
|
|
main = pickOne(assetMatch(assets, "llama-", "bin-win-cpu-arm64"))
|
|
label = "CPU (Windows arm64)"
|
|
break
|
|
}
|
|
if hasNvidiaGPU() {
|
|
// Plusieurs versions CUDA publiées (ex. 12.4 et 13.3) : on prend la plus
|
|
// haute supportée par le pilote (sinon la plus basse, la plus compatible).
|
|
// NB : les archives cudart-llama-bin-win-cuda-… contiennent aussi ces
|
|
// fragments — on les écarte explicitement du choix du binaire principal.
|
|
var cand []ghAsset
|
|
for _, a := range assetMatch(assets, "llama-", "bin-win-cuda-", "-x64.zip") {
|
|
if !strings.HasPrefix(a.Name, "cudart") {
|
|
cand = append(cand, a)
|
|
}
|
|
}
|
|
maxV := driverCudaVersion()
|
|
var best *ghAsset
|
|
bestV := 0.0
|
|
for i := range cand {
|
|
m := reCudaAssetVer.FindStringSubmatch(cand[i].Name)
|
|
if m == nil {
|
|
continue
|
|
}
|
|
v, _ := strconv.ParseFloat(m[1], 64)
|
|
ok := maxV == 0 && (best == nil || v < bestV) || // pilote inconnu → la plus basse
|
|
maxV > 0 && v <= maxV && v > bestV // sinon la plus haute compatible
|
|
if ok {
|
|
best = &cand[i]
|
|
bestV = v
|
|
cudaVer = m[1]
|
|
}
|
|
}
|
|
if best != nil {
|
|
main = best
|
|
cudart = pickOne(assetMatch(assets, "cudart-", "win-cuda-"+cudaVer))
|
|
label = "CUDA " + cudaVer + " (Windows x64)"
|
|
break
|
|
}
|
|
}
|
|
if a := pickOne(assetMatch(assets, "llama-", "bin-win-vulkan-x64")); a != nil {
|
|
main, label = a, "Vulkan (Windows x64)"
|
|
break
|
|
}
|
|
main = pickOne(assetMatch(assets, "llama-", "bin-win-cpu-x64"))
|
|
label = "CPU (Windows x64)"
|
|
case "darwin":
|
|
arch := "x64"
|
|
if runtime.GOARCH == "arm64" {
|
|
arch = "arm64"
|
|
}
|
|
main = pickOne(assetMatch(assets, "llama-", "bin-macos-"+arch))
|
|
label = "Metal (macOS " + arch + ")"
|
|
default: // linux
|
|
arch := "x64"
|
|
if runtime.GOARCH == "arm64" {
|
|
arch = "arm64"
|
|
}
|
|
if hasTool("hipcc") || isDir("/opt/rocm") {
|
|
if a := pickOne(assetMatch(assets, "llama-", "bin-ubuntu-rocm-", arch)); a != nil {
|
|
main, label = a, "ROCm (Linux "+arch+")"
|
|
break
|
|
}
|
|
}
|
|
// Pas de build CUDA officiel pour Linux : sur GPU NVIDIA le variant Vulkan
|
|
// fonctionne via le pilote (moins optimal que le build CUDA local).
|
|
if hasNvidiaGPU() || hasTool("vulkaninfo") {
|
|
if a := pickOne(assetMatch(assets, "llama-", "bin-ubuntu-vulkan-"+arch)); a != nil {
|
|
main, label = a, "Vulkan (Linux "+arch+")"
|
|
if hasNvidiaGPU() {
|
|
label += " — pas de build CUDA officiel Linux ; compile localement pour du CUDA natif"
|
|
}
|
|
break
|
|
}
|
|
}
|
|
main = pickOne(assetMatch(assets, "llama-", "bin-ubuntu-"+arch+".tar.gz"))
|
|
label = "CPU (Linux " + arch + ")"
|
|
}
|
|
if main == nil {
|
|
return nil, nil, "", "", fmt.Errorf("aucun binaire précompilé adapté à cette machine dans la release officielle")
|
|
}
|
|
return main, cudart, label, cudaVer, nil
|
|
}
|
|
|
|
// recommendedMode dit laquelle des deux installations conseiller sur CETTE
|
|
// machine, et pourquoi. Le précompilé convient presque partout — sauf dans un
|
|
// cas qui concerne beaucoup de monde : **Linux avec une carte NVIDIA**.
|
|
// llama.cpp ne publie AUCUN binaire CUDA pour Linux (vérifié release b10299 :
|
|
// seuls Windows a bin-win-cuda-*), donc pickPrebuilt y retombe sur Vulkan, qui
|
|
// marche mais laisse une bonne part de la carte inexploitée. Conseiller le
|
|
// précompilé dans ce cas revenait à pousser vers l'option la plus lente.
|
|
//
|
|
// `backend` est celui qu'une compilation LOCALE produirait (detectBuildPlan).
|
|
// Renvoie {mode: "fast"|"opt", why: "…"} — le libellé est affiché sous la carte
|
|
// conseillée, pour que le choix soit justifié plutôt qu'imposé.
|
|
func recommendedMode(backend string) map[string]any {
|
|
if runtime.GOOS == "linux" && backend == "cuda" {
|
|
return map[string]any{
|
|
"mode": "opt",
|
|
"why": "seule voie vers du CUDA natif : llama.cpp ne publie aucun binaire CUDA pour Linux, le précompilé ne donnerait que du Vulkan. Compte plusieurs minutes de compilation.",
|
|
}
|
|
}
|
|
return map[string]any{"mode": "fast", "why": ""}
|
|
}
|
|
|
|
// prebuiltInstall télécharge et installe (ou met à jour) les binaires
|
|
// précompilés. logf reçoit chaque ligne de log ; phasef la phase courante.
|
|
// Renvoie le chemin du binaire installé.
|
|
func prebuiltInstall(logf, phasef func(string)) (string, error) {
|
|
phasef("récupération de la dernière release llama.cpp…")
|
|
tag, assets, err := fetchLlamaLatest()
|
|
if err != nil {
|
|
return "", fmt.Errorf("impossible d'interroger les releases llama.cpp : %w", err)
|
|
}
|
|
curTag, curCuda := prebuiltVersion()
|
|
main, cudart, label, cudaVer, err := pickPrebuilt(assets)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
logf(fmt.Sprintf("release %s — variant retenu : %s", tag, label))
|
|
|
|
// Réinstaller à l'identique est inutile SAUF si l'extraction date d'une
|
|
// version de Loki qui ignorait les liens des archives (backend installé mais
|
|
// bibliothèques introuvables au lancement) : le marqueur de format force alors
|
|
// une ré-extraction propre au lieu d'un « déjà à jour » trompeur.
|
|
if cur := prebuiltServerBin(); curTag == tag && prebuiltVersionFormat() == prebuiltFormat && cur != "" {
|
|
logf("déjà à jour (" + tag + ")")
|
|
prebuiltPrune(cur, logf) // ménage des versions laissées par les installs précédentes
|
|
return cur, nil
|
|
}
|
|
|
|
dir := prebuiltDir()
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// cudart (DLLs runtime CUDA, ~400 Mo) : seulement si absent ou si la version
|
|
// CUDA du variant a changé depuis la dernière installation.
|
|
if cudart != nil {
|
|
haveDLL, _ := filepath.Glob(filepath.Join(dir, "**", "cudart64*.dll"))
|
|
if len(haveDLL) == 0 {
|
|
haveDLL, _ = filepath.Glob(filepath.Join(dir, "cudart64*.dll"))
|
|
}
|
|
if len(haveDLL) > 0 && curCuda == cudaVer {
|
|
logf("cudart " + cudaVer + " déjà présent — téléchargement évité")
|
|
cudart = nil
|
|
}
|
|
}
|
|
|
|
for _, a := range []*ghAsset{main, cudart} {
|
|
if a == nil {
|
|
continue
|
|
}
|
|
phasef(fmt.Sprintf("téléchargement de %s (%d Mo)…", a.Name, a.Size/1_000_000))
|
|
tmp := filepath.Join(dir, a.Name+".part")
|
|
if err := downloadWithProgress(a.URL, tmp, a.Size, logf); err != nil {
|
|
_ = os.Remove(tmp)
|
|
return "", fmt.Errorf("téléchargement de %s : %w", a.Name, err)
|
|
}
|
|
phasef("extraction de " + a.Name + "…")
|
|
if err := extractArchive(tmp, dir); err != nil {
|
|
_ = os.Remove(tmp)
|
|
return "", fmt.Errorf("extraction de %s : %w", a.Name, err)
|
|
}
|
|
_ = os.Remove(tmp)
|
|
}
|
|
|
|
// Le marqueur est écrit AVANT de localiser le binaire : prebuiltServerBin
|
|
// s'en sert pour élire la version fraîchement extraite s'il en reste d'autres.
|
|
if err := os.WriteFile(filepath.Join(dir, "VERSION"), []byte(tag+" "+cudaVer+" "+prebuiltFormat+"\n"), 0o644); err != nil {
|
|
return "", err
|
|
}
|
|
bin := prebuiltServerBin()
|
|
if bin == "" {
|
|
return "", fmt.Errorf("archives extraites mais llama-server introuvable sous %s", dir)
|
|
}
|
|
if runtime.GOOS != "windows" {
|
|
_ = os.Chmod(bin, 0o755)
|
|
}
|
|
prebuiltPrune(bin, logf)
|
|
logf("binaire installé : " + bin + " (release " + tag + ")")
|
|
return bin, nil
|
|
}
|
|
|
|
// downloadWithProgress télécharge url vers dest en journalisant la progression
|
|
// par tranches de ~25 Mo.
|
|
func downloadWithProgress(url, dest string, total int64, logf func(string)) error {
|
|
client := &http.Client{Timeout: 0}
|
|
resp, err := client.Get(url)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != 200 {
|
|
return fmt.Errorf("HTTP %d", resp.StatusCode)
|
|
}
|
|
if total <= 0 {
|
|
total = resp.ContentLength
|
|
}
|
|
f, err := os.Create(dest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
buf := make([]byte, 1<<20)
|
|
var done, lastLog int64
|
|
for {
|
|
n, rerr := resp.Body.Read(buf)
|
|
if n > 0 {
|
|
if _, werr := f.Write(buf[:n]); werr != nil {
|
|
return werr
|
|
}
|
|
done += int64(n)
|
|
if done-lastLog >= 25<<20 {
|
|
lastLog = done
|
|
if total > 0 {
|
|
logf(fmt.Sprintf("⬇ %d / %d Mo (%d%%)", done/1_000_000, total/1_000_000, done*100/total))
|
|
} else {
|
|
logf(fmt.Sprintf("⬇ %d Mo", done/1_000_000))
|
|
}
|
|
}
|
|
}
|
|
if rerr == io.EOF {
|
|
return nil
|
|
}
|
|
if rerr != nil {
|
|
return rerr
|
|
}
|
|
}
|
|
}
|
|
|
|
// extractArchive extrait un .zip ou un .tar.gz dans dir, en refusant toute
|
|
// entrée qui s'échapperait du dossier (zip-slip).
|
|
func extractArchive(path, dir string) error {
|
|
safe := func(name string) (string, error) {
|
|
p := filepath.Join(dir, filepath.FromSlash(name))
|
|
if rel, err := filepath.Rel(dir, p); err != nil || strings.HasPrefix(rel, "..") {
|
|
return "", fmt.Errorf("entrée d'archive suspecte : %s", name)
|
|
}
|
|
return p, nil
|
|
}
|
|
if strings.HasSuffix(path, ".zip") || strings.HasSuffix(path, ".zip.part") {
|
|
zr, err := zip.OpenReader(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer zr.Close()
|
|
for _, f := range zr.File {
|
|
p, err := safe(f.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if f.FileInfo().IsDir() {
|
|
if err := os.MkdirAll(p, 0o755); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
return err
|
|
}
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w, err := os.Create(p)
|
|
if err != nil {
|
|
rc.Close()
|
|
return err
|
|
}
|
|
_, err = io.Copy(w, rc)
|
|
rc.Close()
|
|
w.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
// tar.gz
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
gz, err := gzip.NewReader(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer gz.Close()
|
|
tr := tar.NewReader(gz)
|
|
// Les liens sont appliqués APRÈS coup : leur cible n'est pas forcément déjà
|
|
// extraite au moment où on croise l'entrée.
|
|
var links []archiveLink
|
|
for {
|
|
h, err := tr.Next()
|
|
if err == io.EOF {
|
|
return applyLinks(links)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p, err := safe(h.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch h.Typeflag {
|
|
case tar.TypeDir:
|
|
if err := os.MkdirAll(p, 0o755); err != nil {
|
|
return err
|
|
}
|
|
case tar.TypeReg:
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
return err
|
|
}
|
|
w, err := os.Create(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.Copy(w, tr); err != nil {
|
|
w.Close()
|
|
return err
|
|
}
|
|
w.Close()
|
|
_ = os.Chmod(p, os.FileMode(h.Mode)&0o777)
|
|
case tar.TypeSymlink, tar.TypeLink:
|
|
// Indispensable sur macOS : les archives llama.cpp livrent les .dylib
|
|
// sous leur nom versionné (libllama-common.0.0.10107.dylib) PLUS un lien
|
|
// portant le nom que cherche le binaire (libllama-common.0.dylib). Ignorer
|
|
// ces entrées donnait un backend installé mais impossible à lancer :
|
|
// « dyld: Library not loaded: @rpath/libllama-common.0.dylib ».
|
|
target := h.Linkname
|
|
if h.Typeflag == tar.TypeLink {
|
|
// Lien dur : la cible est un chemin dans l'archive, pas un chemin relatif.
|
|
t, err := safe(h.Linkname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
target = t
|
|
}
|
|
links = append(links, archiveLink{path: p, target: target})
|
|
}
|
|
}
|
|
}
|
|
|
|
// archiveLink : un lien (symbolique ou dur) relevé dans une archive.
|
|
type archiveLink struct{ path, target string }
|
|
|
|
// applyLinks crée les liens relevés pendant l'extraction. Sur les systèmes où la
|
|
// création de liens symboliques est refusée (Windows sans mode développeur), on
|
|
// copie le fichier cible : moins élégant, mais fonctionnel.
|
|
func applyLinks(links []archiveLink) error {
|
|
for _, l := range links {
|
|
_ = os.Remove(l.path)
|
|
if err := os.MkdirAll(filepath.Dir(l.path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Symlink(l.target, l.path); err == nil {
|
|
continue
|
|
}
|
|
// Repli par copie. La cible peut être relative au dossier du lien.
|
|
src := l.target
|
|
if !filepath.IsAbs(src) {
|
|
src = filepath.Join(filepath.Dir(l.path), l.target)
|
|
}
|
|
data, err := os.ReadFile(src)
|
|
if err != nil {
|
|
continue // cible absente de l'archive : on n'échoue pas l'installation pour ça
|
|
}
|
|
if err := os.WriteFile(l.path, data, 0o755); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|