mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
llama.cpp publie plusieurs versions par jour ; l'image de Loki ne se reconstruit qu'à une mise à jour de Loki. Le moteur y était donc figé à la date du dernier build, et le rattraper imposait un rebuild complet de 2,6 Go pour un composant qui en pèse 170 Mo. Le panneau « Moteur » ne le disait même pas : il annonçait « rien à mettre à jour ici ». Réglages → Moteur affiche maintenant la version qui tourne (bXXXXX et son commit, lus dans la bannière du binaire — jusqu'ici invisibles ailleurs que dans le journal du moteur) et la met à jour en un clic. Où le moteur est pris. Pas dans les releases GitHub de llama.cpp : elles ne contiennent AUCUN binaire CUDA pour Linux, et le mode « précompilé » retomberait sur Vulkan, donc sur une régression pour une carte NVIDIA. La seule distribution CUDA/Linux officielle et précompilée est l'image de conteneur — celle-là même dont l'image de Loki hérite. On lit son manifeste OCI et on ne télécharge que les couches qui portent /app, en descendant du sommet : le runtime CUDA (2 Go) et la base système sont déjà là. S'arrêter au binaire ne suffit pas — llama.cpp le range dans une couche et ses .so dans la précédente — d'où une règle d'arrêt sur « binaire + libggml-base + libllama », et un garde-fou de taille qui interdit de descendre jusqu'au runtime. Le moteur atterrit dans /data/engine/<version>/, donc sur le volume de données : il survit à un docker compose pull. La variante (CUDA, Vulkan, SYCL, MUSA, CPU) est déduite des backends ggml posés à côté du moteur courant — le conteneur ne sait pas de quelle image il vient, et faire retenir « server-cuda » à l'utilisateur serait un piège. Le risque, et ce qui le couvre. La mise à jour apporte llama.cpp, pas le runtime CUDA, qui reste celui de l'image : un llama.cpp compilé pour un CUDA plus récent ne chargerait pas son backend GPU. Le symptôme serait silencieux — tout marche, mais sur le processeur. Le nouveau moteur est donc lancé à blanc avant toute bascule ; il est refusé s'il ne démarre pas, ET s'il ne voit plus aucune carte alors que le moteur courant en voyait. Dans les deux cas le moteur courant n'est pas touché, et celui de l'image reste intact : « revenir au moteur de l'image » y ramène en un clic, sans réseau. Vérifié de bout en bout contre le vrai ghcr.io (166 Mo, 7 s, toutes les bibliothèques et leurs liens de version présents) et, pour les chemins d'échec, contre un faux registre. LOKI_OCI_REGISTRY permet de viser un miroir quand ghcr.io n'est pas joignable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
641 lines
19 KiB
Go
641 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 {
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return downloadRequest(req, dest, total, logf)
|
|
}
|
|
|
|
// downloadRequest est la même chose pour une requête déjà construite — un
|
|
// registre OCI exige un en-tête d'autorisation, même en lecture anonyme.
|
|
func downloadRequest(req *http.Request, dest string, total int64, logf func(string)) error {
|
|
client := &http.Client{Timeout: 0}
|
|
resp, err := client.Do(req)
|
|
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
|
|
}
|