mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-12 01:37:06 +02:00
Le sélecteur de l'éditeur de modèle proposait encore « Précompilé » et « Compilé » — deux installations que Loki ne fait pas en conteneur : cliquer répondait « installez d'abord llama.cpp… ». Pire, le moteur de l'image ne correspondant à aucun des deux, il était classé « Personnalisé ». En conteneur, ces deux options laissent place à « Image » (sélectionnée par défaut, y compris pour un preset sans BIN) ; « Personnalisé » reste pour un binaire déposé dans /data/backends. Le moteur de l'image est désormais déclaré par l'image elle-même via LOKI_ENGINE_BIN (Dockerfile + entrypoint) au lieu d'être deviné d'après le chemin : /api/llamacpp expose provided et provided_bin. Attribution : sous le nom Loki, « fork de AJEAN » avec lien vers le dépôt d'origine. Vérifié dans l'image : provided=true, provided_bin=/app/llama-server, BIN semé identique ; build, vet et tests verts.
664 lines
21 KiB
Go
664 lines
21 KiB
Go
// web_llamacpp.go — pilotage du backend llama.cpp depuis l'UI web :
|
|
// statut (commit, retard, binaire, plan de build), vérification des mises à
|
|
// jour (git fetch), et jobs asynchrones install / update / rebuild dont la
|
|
// progression et les logs sont pollés par le client.
|
|
//
|
|
// GET /api/llamacpp → statut complet (dépôt, commit, binaire, plan)
|
|
// POST /api/llamacpp/check → git fetch + retard sur origin (réseau)
|
|
// POST /api/llamacpp/install → job d'installation {force?}
|
|
// POST /api/llamacpp/update → job de mise à jour {clean?}
|
|
// GET /api/llamacpp/job?from= → progression du job (phase, lignes, erreur)
|
|
package loki
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// lcJob est LE job llama.cpp en cours (un seul à la fois). Les lignes de log
|
|
// sont indexées de façon absolue (base = index de Lines[0]) pour que le client
|
|
// puisse poller « à partir de N » même quand on tronque le début.
|
|
type lcJob struct {
|
|
Action string // "install" | "update"
|
|
Phase string
|
|
Compiled int // fichiers compilés (progression du build)
|
|
Running bool
|
|
Err string
|
|
StartedAt int64
|
|
EndedAt int64
|
|
OldCommit string
|
|
NewCommit string
|
|
lines []string
|
|
base int
|
|
}
|
|
|
|
var (
|
|
lcMu sync.Mutex
|
|
lcCur *lcJob
|
|
)
|
|
|
|
const lcMaxLines = 4000
|
|
|
|
// lcAppend ajoute une ligne au log du job courant et met à jour la phase / le
|
|
// compteur de fichiers compilés à partir du contenu (mêmes heuristiques que le
|
|
// spinner du CLI). Appelée par le sink de build (emitBuildLine).
|
|
func lcAppend(line string) {
|
|
lcMu.Lock()
|
|
defer lcMu.Unlock()
|
|
if lcCur == nil {
|
|
return
|
|
}
|
|
lcCur.lines = append(lcCur.lines, line)
|
|
if len(lcCur.lines) > lcMaxLines {
|
|
drop := len(lcCur.lines) - lcMaxLines
|
|
lcCur.lines = append([]string(nil), lcCur.lines[drop:]...)
|
|
lcCur.base += drop
|
|
}
|
|
if f := compiledFile(line); f != "" {
|
|
lcCur.Compiled++
|
|
lcCur.Phase = fmt.Sprintf("compilation… %d fichiers", lcCur.Compiled)
|
|
} else if p := phaseLabel(line); p != "" {
|
|
lcCur.Phase = p
|
|
}
|
|
lcSaveLine(line)
|
|
lcSave(false)
|
|
}
|
|
|
|
// lcPhase pose une phase explicite (étapes hors build : clone, fetch, service…)
|
|
// et la trace aussi dans le log.
|
|
func lcPhase(phase string) {
|
|
lcMu.Lock()
|
|
if lcCur != nil {
|
|
lcCur.Phase = phase
|
|
lcCur.lines = append(lcCur.lines, "▶ "+phase)
|
|
lcSaveLine("▶ " + phase)
|
|
lcSave(true)
|
|
}
|
|
lcMu.Unlock()
|
|
}
|
|
|
|
// startLcJob démarre un job (install ou update) si aucun n'est en cours.
|
|
func startLcJob(action string, run func()) error {
|
|
lcMu.Lock()
|
|
defer lcMu.Unlock()
|
|
if lcCur != nil && lcCur.Running {
|
|
return fmt.Errorf("un job %s est déjà en cours", lcCur.Action)
|
|
}
|
|
lcCur = &lcJob{Action: action, Running: true, Phase: "démarrage…", StartedAt: time.Now().Unix()}
|
|
lcResetLog()
|
|
lcSave(true)
|
|
setBuildSink(lcAppend)
|
|
go func() {
|
|
defer func() {
|
|
setBuildSink(nil)
|
|
lcMu.Lock()
|
|
lcCur.Running = false
|
|
lcCur.EndedAt = time.Now().Unix()
|
|
lcSave(true)
|
|
lcMu.Unlock()
|
|
}()
|
|
run()
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func lcFail(err error) {
|
|
lcMu.Lock()
|
|
if lcCur != nil {
|
|
lcCur.Err = err.Error()
|
|
lcCur.Phase = "échec"
|
|
lcCur.lines = append(lcCur.lines, "✗ "+err.Error())
|
|
lcSaveLine("✗ " + err.Error())
|
|
lcSave(true)
|
|
}
|
|
lcMu.Unlock()
|
|
}
|
|
|
|
func lcDone(msg string) {
|
|
lcMu.Lock()
|
|
if lcCur != nil {
|
|
lcCur.Phase = msg
|
|
lcCur.lines = append(lcCur.lines, "✓ "+msg)
|
|
lcSaveLine("✓ " + msg)
|
|
lcSave(true)
|
|
}
|
|
lcMu.Unlock()
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Handlers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// handleLlamacpp renvoie le statut complet du backend llama.cpp : dépôt,
|
|
// commit courant, retard connu (sans fetch réseau), binaire compilé et son
|
|
// usage dans config.env, plan de build détecté, et le job éventuel.
|
|
func handleLlamacpp(w http.ResponseWriter, r *http.Request) {
|
|
repo := llamacppRepoDir()
|
|
out := map[string]any{
|
|
"repo": repo,
|
|
"installed": isDir(filepath.Join(repo, ".git")),
|
|
}
|
|
if out["installed"].(bool) {
|
|
out["branch"] = gitOutput(repo, "rev-parse", "--abbrev-ref", "HEAD")
|
|
out["commit"] = gitOutput(repo, "rev-parse", "--short", "HEAD")
|
|
out["commit_date"] = gitOutput(repo, "log", "-1", "--format=%ci")
|
|
out["commit_msg"] = gitOutput(repo, "log", "-1", "--format=%s")
|
|
if br, _ := out["branch"].(string); br != "" && br != "HEAD" {
|
|
if behind := gitOutput(repo, "rev-list", "--count", "HEAD..origin/"+br); behind != "" {
|
|
n, _ := strconv.Atoi(behind)
|
|
out["behind"] = n
|
|
}
|
|
}
|
|
}
|
|
bin := llamaServerBin(repo)
|
|
out["bin"] = bin
|
|
cfgBin := ReadConfig()["BIN"]
|
|
out["config_bin"] = cfgBin
|
|
out["in_use"] = bin != "" && samePath(bin, cfgBin)
|
|
// Moteur livré par l'image Docker : il n'y a rien à installer, et l'image
|
|
// ne contient ni cmake ni compilateur. L'UI masque alors les trois modes,
|
|
// et le sélecteur par modèle propose « Image » au lieu de « Précompilé /
|
|
// Compilé », qui ne désignent rien ici.
|
|
out["provided"] = engineProvided()
|
|
out["provided_bin"] = providedEngineBin()
|
|
|
|
plan := detectBuildPlan()
|
|
out["plan"] = map[string]any{
|
|
"backend": plan.backend,
|
|
"arch": plan.cudaArch,
|
|
"jobs": plan.jobs,
|
|
}
|
|
// Mode « binaires précompilés » : installé ? utilisé par la config ?
|
|
pbTag, _ := prebuiltVersion()
|
|
pbBin := prebuiltServerBin()
|
|
out["prebuilt"] = map[string]any{
|
|
"tag": pbTag,
|
|
"bin": pbBin,
|
|
"dir": prebuiltDir(),
|
|
// Tout BIN vivant sous le dossier prebuilt EST le moteur précompilé, même
|
|
// s'il porte le numéro d'une release remplacée depuis (chemin versionné).
|
|
"in_use": pbBin != "" && prebuiltOwns(cfgBin),
|
|
}
|
|
out["backends_dir"] = filepath.Join(LokiHome(), "backends")
|
|
out["reco"] = recommendedMode(plan.backend)
|
|
out["job"] = lcJobSnapshot(0, false)
|
|
sendJSON(w, 200, out)
|
|
}
|
|
|
|
// providedEngineBin renvoie le moteur livré par l'image Docker, ou "" hors
|
|
// conteneur. L'image le déclare elle-même via LOKI_ENGINE_BIN (voir Dockerfile
|
|
// et docker-entrypoint.sh) : pas de chemin en dur ici, et n'importe quelle image
|
|
// de base convient tant qu'elle pose cette variable.
|
|
//
|
|
// Sans cette notion, l'UI proposait d'installer un moteur déjà présent et
|
|
// l'utilisateur tombait sur « outils manquants : cmake » — l'image ne contient
|
|
// volontairement ni cmake ni compilateur, puisqu'elle part de l'image officielle
|
|
// llama.cpp où llama-server est déjà compilé.
|
|
func providedEngineBin() string {
|
|
p := strings.TrimSpace(os.Getenv("LOKI_ENGINE_BIN"))
|
|
if p == "" {
|
|
return ""
|
|
}
|
|
if st, err := os.Stat(prebuiltResolveBin(p)); err != nil || st.IsDir() {
|
|
return ""
|
|
}
|
|
return p
|
|
}
|
|
|
|
// engineProvided : le moteur vient-il de l'image ? (raccourci de lecture)
|
|
func engineProvided() bool { return providedEngineBin() != "" }
|
|
|
|
// refuseIfProvided coupe court aux jobs d'installation/compilation quand le
|
|
// moteur vient de l'image : rien à installer, et aucun compilateur disponible.
|
|
// Renvoie true si la requête a été refusée (l'appelant doit sortir).
|
|
func refuseIfProvided(w http.ResponseWriter) bool {
|
|
bin := providedEngineBin()
|
|
if bin == "" {
|
|
return false
|
|
}
|
|
sendJSON(w, 409, map[string]any{"ok": false, "error": "Le moteur est fourni par l'image Docker (" + bin +
|
|
") : il n'y a rien à installer ici, et l'image ne contient ni cmake ni compilateur. " +
|
|
"Pour changer de moteur, reconstruis l'image avec un autre build-arg LLAMACPP_IMAGE."})
|
|
return true
|
|
}
|
|
|
|
// samePath compare deux chemins en neutralisant séparateurs, symlinks et casse
|
|
// (Windows).
|
|
func samePath(a, b string) bool {
|
|
if a == "" || b == "" {
|
|
return false
|
|
}
|
|
norm := func(p string) string {
|
|
if real, err := filepath.EvalSymlinks(p); err == nil {
|
|
p = real
|
|
}
|
|
p = filepath.Clean(p)
|
|
return strings.ToLower(filepath.ToSlash(p))
|
|
}
|
|
return norm(a) == norm(b)
|
|
}
|
|
|
|
// handleLlamacppCheck fait un vrai git fetch puis renvoie le retard sur origin
|
|
// et le dernier commit distant. Synchrone (quelques secondes réseau).
|
|
func handleLlamacppCheck(w http.ResponseWriter, r *http.Request) {
|
|
repo := llamacppRepoDir()
|
|
if !isDir(filepath.Join(repo, ".git")) {
|
|
sendJSON(w, 200, map[string]any{"ok": false, "error": "llama.cpp n'est pas installé"})
|
|
return
|
|
}
|
|
branch := gitOutput(repo, "rev-parse", "--abbrev-ref", "HEAD")
|
|
if branch == "" || branch == "HEAD" {
|
|
branch = "master"
|
|
}
|
|
if err := runStep("git fetch", repo, "git", "fetch", "origin", "--quiet"); err != nil {
|
|
sendJSON(w, 200, map[string]any{"ok": false, "error": "git fetch a échoué : " + err.Error()})
|
|
return
|
|
}
|
|
behind := 0
|
|
if b := gitOutput(repo, "rev-list", "--count", "HEAD..origin/"+branch); b != "" {
|
|
behind, _ = strconv.Atoi(b)
|
|
}
|
|
sendJSON(w, 200, map[string]any{
|
|
"ok": true,
|
|
"behind": behind,
|
|
"local": gitOutput(repo, "rev-parse", "--short", "HEAD"),
|
|
"remote": gitOutput(repo, "rev-parse", "--short", "origin/"+branch),
|
|
"remote_date": gitOutput(repo, "log", "-1", "--format=%ci", "origin/"+branch),
|
|
"remote_msg": gitOutput(repo, "log", "-1", "--format=%s", "origin/"+branch),
|
|
"branch": branch,
|
|
"has_binary": llamaServerBin(repo) != "",
|
|
"needs_rebuild": behind > 0 || llamaServerBin(repo) == "",
|
|
})
|
|
}
|
|
|
|
// handleLlamacppInstall lance le job d'installation (clone + build + BIN).
|
|
func handleLlamacppInstall(w http.ResponseWriter, r *http.Request) {
|
|
if refuseIfProvided(w) {
|
|
return
|
|
}
|
|
var req struct {
|
|
Force bool `json:"force"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
if err := startLcJob("install", func() { lcRunInstall(req.Force) }); err != nil {
|
|
sendJSON(w, 409, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": true})
|
|
}
|
|
|
|
// handleLlamacppInstallCustom lance un job d'installation d'un backend CUSTOM
|
|
// (fork llama.cpp) depuis une URL de dépôt Git : cloné dans backends/<name> et
|
|
// compilé pour la machine, SANS toucher au BIN global. Il apparaît ensuite dans
|
|
// /api/backends et se choisit par modèle (éditeur de preset → Moteur).
|
|
func handleLlamacppInstallCustom(w http.ResponseWriter, r *http.Request) {
|
|
if refuseIfProvided(w) {
|
|
return
|
|
}
|
|
var req struct {
|
|
Repo string `json:"repo"`
|
|
Name string `json:"name"`
|
|
Ref string `json:"ref"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
if strings.TrimSpace(req.Repo) == "" {
|
|
sendJSON(w, 400, map[string]any{"ok": false, "error": "URL du dépôt requise"})
|
|
return
|
|
}
|
|
if err := startLcJob("custom", func() { lcRunCustomInstall(req.Repo, req.Name, req.Ref) }); err != nil {
|
|
sendJSON(w, 409, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": true})
|
|
}
|
|
|
|
// lcRunCustomInstall : corps du job d'install d'un backend custom (sans bascule
|
|
// de BIN — c'est volontaire, il se rattache par preset).
|
|
func lcRunCustomInstall(url, name, ref string) {
|
|
bin, err := installCustomBackend(url, name, ref, lcPhase)
|
|
if err != nil {
|
|
lcFail(err)
|
|
return
|
|
}
|
|
lcAppend("binaire compilé : " + bin)
|
|
lcAppend("→ pour l'utiliser : édite un modèle → section Moteur → « backend détecté » et choisis-le.")
|
|
lcDone("backend custom installé")
|
|
}
|
|
|
|
// handleLlamacppUpdate lance le job de mise à jour (pull + rebuild + restart).
|
|
// {clean:true} force une recompilation from scratch même sans nouveau commit.
|
|
func handleLlamacppUpdate(w http.ResponseWriter, r *http.Request) {
|
|
if refuseIfProvided(w) {
|
|
return
|
|
}
|
|
var req struct {
|
|
Clean bool `json:"clean"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
if err := startLcJob("update", func() { lcRunUpdate(req.Clean) }); err != nil {
|
|
sendJSON(w, 409, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": true})
|
|
}
|
|
|
|
// handleLlamacppPrebuiltCheck interroge la dernière release officielle de
|
|
// llama.cpp et la compare à la version précompilée installée. Synchrone.
|
|
func handleLlamacppPrebuiltCheck(w http.ResponseWriter, r *http.Request) {
|
|
tag, assets, err := fetchLlamaLatest()
|
|
if err != nil {
|
|
sendJSON(w, 200, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
main, _, label, _, err := pickPrebuilt(assets)
|
|
if err != nil {
|
|
sendJSON(w, 200, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
cur, _ := prebuiltVersion()
|
|
sendJSON(w, 200, map[string]any{
|
|
"ok": true,
|
|
"latest": tag,
|
|
"current": cur,
|
|
"variant": label,
|
|
"size_mb": main.Size / 1_000_000,
|
|
"update": cur != tag || prebuiltServerBin() == "",
|
|
})
|
|
}
|
|
|
|
// handleLlamacppPrebuilt lance le job de téléchargement / mise à jour des
|
|
// binaires précompilés officiels (pas de compilation).
|
|
func handleLlamacppPrebuilt(w http.ResponseWriter, r *http.Request) {
|
|
if err := startLcJob("prebuilt", lcRunPrebuilt); err != nil {
|
|
sendJSON(w, 409, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": true})
|
|
}
|
|
|
|
// lcRunPrebuilt : télécharge les binaires officiels, pointe BIN dessus, en
|
|
// stoppant le service pendant le remplacement (binaire verrouillé en cours
|
|
// d'exécution) puis en le relançant.
|
|
func lcRunPrebuilt() {
|
|
svcWasUp := serviceIsActive()
|
|
if svcWasUp {
|
|
lcPhase("arrêt du service le temps de l'installation…")
|
|
if err := serviceAction("stop"); err != nil {
|
|
lcAppend("[warn] impossible d'arrêter le service : " + err.Error())
|
|
}
|
|
}
|
|
bin, err := prebuiltInstall(lcAppend, lcPhase)
|
|
if err == nil {
|
|
if serr := SetConfigKey("BIN", bin); serr != nil {
|
|
err = fmt.Errorf("binaires installés mais échec d'écriture de BIN : %w", serr)
|
|
} else {
|
|
lcAppend("BIN mis à jour")
|
|
}
|
|
}
|
|
if svcWasUp {
|
|
lcPhase("redémarrage du service…")
|
|
if serr := serviceAction("start"); serr != nil {
|
|
lcAppend("[warn] redémarrage du service échoué : " + serr.Error())
|
|
}
|
|
}
|
|
if err != nil {
|
|
lcFail(err)
|
|
return
|
|
}
|
|
tag, _ := prebuiltVersion()
|
|
lcDone("binaires précompilés installés (" + tag + ")")
|
|
}
|
|
|
|
// handleLlamacppUse bascule BIN entre deux versions DÉJÀ installées, sans
|
|
// rien recompiler : "fast" = binaires précompilés, "opt" = build local. Le
|
|
// service redémarre pour prendre le nouveau binaire. L'UI n'appelle ceci que
|
|
// quand la version cible existe déjà (sinon elle lance un job d'installation).
|
|
func handleLlamacppUse(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
Mode string `json:"mode"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
var bin string
|
|
switch req.Mode {
|
|
case "fast":
|
|
bin = prebuiltServerBin()
|
|
case "opt":
|
|
bin = llamaServerBin(llamacppRepoDir())
|
|
default:
|
|
sendJSON(w, 400, map[string]any{"ok": false, "error": "mode inconnu"})
|
|
return
|
|
}
|
|
if bin == "" {
|
|
sendJSON(w, 400, map[string]any{"ok": false, "error": "cette version n'est pas installée"})
|
|
return
|
|
}
|
|
if err := SetConfigKey("BIN", bin); err != nil {
|
|
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
if serviceIsActive() {
|
|
_ = serviceAction("restart")
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": true, "bin": bin})
|
|
}
|
|
|
|
// handleLlamacppJob renvoie l'état du job courant + les lignes de log depuis
|
|
// l'offset absolu ?from=N (le client mémorise `next` et enchaîne).
|
|
func handleLlamacppJob(w http.ResponseWriter, r *http.Request) {
|
|
from, _ := strconv.Atoi(r.URL.Query().Get("from"))
|
|
sendJSON(w, 200, lcJobSnapshot(from, true))
|
|
}
|
|
|
|
// lcJobSnapshot construit la vue JSON du job courant. withLines=false renvoie
|
|
// juste l'entête (imbriquée dans /api/llamacpp).
|
|
func lcJobSnapshot(from int, withLines bool) map[string]any {
|
|
lcMu.Lock()
|
|
defer lcMu.Unlock()
|
|
if lcCur == nil {
|
|
return map[string]any{"exists": false}
|
|
}
|
|
j := lcCur
|
|
out := map[string]any{
|
|
"exists": true,
|
|
"action": j.Action,
|
|
"running": j.Running,
|
|
"phase": j.Phase,
|
|
"compiled": j.Compiled,
|
|
"error": j.Err,
|
|
"started_at": j.StartedAt,
|
|
"ended_at": j.EndedAt,
|
|
"old": j.OldCommit,
|
|
"new": j.NewCommit,
|
|
}
|
|
if withLines {
|
|
start := from - j.base
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
if start > len(j.lines) {
|
|
start = len(j.lines)
|
|
}
|
|
out["lines"] = append([]string(nil), j.lines[start:]...)
|
|
out["next"] = j.base + len(j.lines)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Corps des jobs (miroir web de llamacppInstall / llamacppUpdate, sans stdout
|
|
// interactif : tout passe par lcPhase / le sink de build)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func lcRunInstall(force bool) {
|
|
repo := defaultRepoDir()
|
|
|
|
lcPhase("vérification des outils (git, cmake, compilateur)…")
|
|
if err := requireTools("git", "cmake"); err != nil {
|
|
lcFail(err)
|
|
return
|
|
}
|
|
if err := ensureCompiler(); err != nil {
|
|
lcFail(err)
|
|
return
|
|
}
|
|
ensureAccelerator()
|
|
|
|
if isDir(filepath.Join(repo, ".git")) {
|
|
if !force {
|
|
// Dépôt déjà là : on bascule sur une mise à jour (même intention).
|
|
lcPhase("dépôt déjà présent — bascule en mise à jour")
|
|
lcRunUpdate(false)
|
|
return
|
|
}
|
|
lcPhase("suppression du dépôt existant (--force)…")
|
|
if err := os.RemoveAll(repo); err != nil {
|
|
lcFail(err)
|
|
return
|
|
}
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(repo), 0o755); err != nil {
|
|
lcFail(err)
|
|
return
|
|
}
|
|
|
|
lcPhase("clone de llama.cpp…")
|
|
if err := runStep("git clone", "", "git", "clone", "--depth=1", llamacppRepoURL, repo); err != nil {
|
|
lcFail(fmt.Errorf("git clone a échoué : %w", err))
|
|
return
|
|
}
|
|
|
|
if !lcBuildAndSwitch(repo, true) {
|
|
return
|
|
}
|
|
lcDone("installation terminée")
|
|
}
|
|
|
|
func lcRunUpdate(clean bool) {
|
|
lcPhase("vérification des outils (git, cmake, compilateur)…")
|
|
if err := requireTools("git", "cmake"); err != nil {
|
|
lcFail(err)
|
|
return
|
|
}
|
|
if err := ensureCompiler(); err != nil {
|
|
lcFail(err)
|
|
return
|
|
}
|
|
ensureAccelerator()
|
|
|
|
repo := llamacppRepoDir()
|
|
if !isDir(filepath.Join(repo, ".git")) {
|
|
lcFail(fmt.Errorf("aucun dépôt llama.cpp (%s) — lance d'abord l'installation", repo))
|
|
return
|
|
}
|
|
|
|
oldCommit := gitOutput(repo, "rev-parse", "--short", "HEAD")
|
|
lcMu.Lock()
|
|
lcCur.OldCommit = oldCommit
|
|
lcMu.Unlock()
|
|
|
|
branch := gitOutput(repo, "rev-parse", "--abbrev-ref", "HEAD")
|
|
if branch == "" || branch == "HEAD" {
|
|
branch = "master"
|
|
}
|
|
|
|
lcPhase("git fetch origin…")
|
|
if err := runStep("git fetch", repo, "git", "fetch", "origin", "--quiet"); err != nil {
|
|
lcFail(fmt.Errorf("git fetch a échoué : %w", err))
|
|
return
|
|
}
|
|
localRev := gitOutput(repo, "rev-parse", "HEAD")
|
|
remoteRev := gitOutput(repo, "rev-parse", "origin/"+branch)
|
|
if localRev != "" && localRev == remoteRev && !clean && llamaServerBin(repo) != "" {
|
|
lcDone("déjà à jour (" + oldCommit + ") — rien à faire")
|
|
return
|
|
}
|
|
if localRev != remoteRev {
|
|
lcPhase("git pull origin/" + branch + "…")
|
|
if err := runStep("git pull --ff-only", repo, "git", "pull", "--ff-only", "origin", branch); err != nil {
|
|
lcFail(fmt.Errorf("git pull a échoué (modifs locales ?) : %w", err))
|
|
return
|
|
}
|
|
}
|
|
newCommit := gitOutput(repo, "rev-parse", "--short", "HEAD")
|
|
lcMu.Lock()
|
|
lcCur.NewCommit = newCommit
|
|
lcMu.Unlock()
|
|
|
|
// Le binaire en cours d'exécution ne peut pas être réécrit → stop du service
|
|
// pendant le build, redémarrage après (même en échec).
|
|
svcWasUp := serviceIsActive()
|
|
if svcWasUp {
|
|
lcPhase("arrêt du service le temps du build…")
|
|
if err := serviceAction("stop"); err != nil {
|
|
lcAppend("[warn] impossible d'arrêter le service : " + err.Error())
|
|
}
|
|
}
|
|
|
|
ok := lcBuildAndSwitch(repo, clean)
|
|
if svcWasUp {
|
|
lcPhase("redémarrage du service…")
|
|
if err := serviceAction("start"); err != nil {
|
|
lcAppend("[warn] redémarrage du service échoué : " + err.Error())
|
|
}
|
|
}
|
|
if !ok {
|
|
return
|
|
}
|
|
if oldCommit == newCommit {
|
|
lcDone("recompilé (" + newCommit + ")")
|
|
} else {
|
|
lcDone("mis à jour : " + oldCommit + " → " + newCommit)
|
|
}
|
|
}
|
|
|
|
// lcBuildAndSwitch détecte le plan, compile llama-server et pointe BIN dessus.
|
|
// Renvoie false (job en échec) si une étape casse.
|
|
func lcBuildAndSwitch(repo string, clean bool) bool {
|
|
plan := detectBuildPlan()
|
|
lcAppend(fmt.Sprintf("plan de build : backend=%s arch=%s jobs=%d", plan.backend, plan.cudaArch, plan.jobs))
|
|
lcPhase("configuration CMake…")
|
|
if err := buildLlamacpp(repo, plan, clean); err != nil {
|
|
lcAppendLogTail(filepath.Join(repo, "configure.log"), filepath.Join(repo, "build.log"))
|
|
lcFail(err)
|
|
return false
|
|
}
|
|
bin := llamaServerBin(repo)
|
|
if bin == "" {
|
|
lcFail(fmt.Errorf("build terminé mais binaire introuvable sous %s", filepath.Join(repo, "build")))
|
|
return false
|
|
}
|
|
lcAppend("binaire compilé : " + bin)
|
|
if err := SetConfigKey("BIN", bin); err != nil {
|
|
lcFail(fmt.Errorf("build ok mais échec d'écriture de BIN : %w", err))
|
|
return false
|
|
}
|
|
lcAppend("BIN mis à jour")
|
|
return true
|
|
}
|
|
|
|
// lcAppendLogTail remonte la fin des logs de build dans le job pour que
|
|
// l'erreur réelle soit visible dans l'UI sans aller chercher les fichiers.
|
|
func lcAppendLogTail(paths ...string) {
|
|
for _, p := range paths {
|
|
b, err := os.ReadFile(p)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
lines := strings.Split(strings.TrimRight(string(b), "\n"), "\n")
|
|
if len(lines) > 30 {
|
|
lines = lines[len(lines)-30:]
|
|
}
|
|
lcAppend("--- fin de " + filepath.Base(p) + " ---")
|
|
for _, l := range lines {
|
|
lcAppend(l)
|
|
}
|
|
}
|
|
}
|