mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-21 22:18:13 +02:00
Rebaptise AJEAN en Loki (fork, lignée conservée)
- 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.
This commit is contained in:
157 files changed
+928
-928
No files matched your search
@@ -0,0 +1,630 @@
|
||||
// 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
|
||||
}
|
||||
Reference in new issue
Block a user