mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-13 18:28:03 +02:00
v0.2.5 : build llama.cpp 100% automatique et propre sur Windows et Linux
Windows : `jean llamacpp install` part de zéro et provisionne toute la chaîne sans intervention : - install Windows ajoute jean au PATH (copie dans %JEAN_HOME%\bin) - auto-install des outils manquants via winget (cmake, git, ninja) + refresh du PATH du process depuis le registre - compilateur MSVC Build Tools auto-installé ; générateur Visual Studio (localise MSVC sans Developer Prompt) ; binaire multi-config (Release\) - détection GPU + auto-install du CUDA Toolkit quand une carte NVIDIA est vue sans nvcc ; CUDA_PATH/CUDA_PATH_Vx_y injectés pour MSBuild ; DLL runtime CUDA (bin + bin\x64) ajoutées au PATH de llama-server - console UTF-8 + VT pour un affichage correct Multi-OS : - auto-install des outils aussi sous Unix (apt/dnf/pacman/brew) - désactive l'UI web embarquée de llama-server (LLAMA_BUILD_UI=OFF + LLAMA_USE_PREBUILT_UI=OFF) : supprime la dépendance npm/HuggingFace qui cassait le build ; jean fournit sa propre UI - build CUDA allégé (sans FA_ALL_QUANTS) : bien plus rapide - sortie de build propre : configure/build passent par un runner qui écrit le détail dans un log fichier et n'affiche qu'un spinner animé + le compteur de fichiers (formats MSBuild et Make/Ninja) + les erreurs réelles ; tests unitaires du filtre Validé en exécution réelle : Windows (GTX 1650 Ti, sm_75) et serveur Linux multi-GPU (sm_75;120) jusqu'au serveur GPU opérationnel.
This commit is contained in:
1 parent
747032cbf6
commit
949e261538
7 files changed
+809
-27
No files matched your search
@@ -0,0 +1,51 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCompiledFile(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
` Compiling CUDA source file ..\..\..\..\ggml\src\ggml-cuda\acc.cu...`: "acc.cu",
|
||||
` ggml-threading.cpp`: "ggml-threading.cpp",
|
||||
` ggml-quants.c`: "ggml-quants.c",
|
||||
// Make (Linux) et Ninja.
|
||||
`[ 45%] Building CXX object src/CMakeFiles/llama.dir/llama.cpp.o`: "llama.cpp",
|
||||
`[123/456] Building CUDA object ggml/src/ggml-cuda/CMakeFiles/ggml-cuda.dir/acc.cu.o`: "acc.cu",
|
||||
// La ligne de commande nvcc géante ne doit PAS être prise pour un fichier.
|
||||
` C:\...\nvcc.exe -x cu ... -o ggml-cuda.dir\Release\acc.obj "C:\...\acc.cu"`: "",
|
||||
`Building Custom Rule C:/ProgramData/jean/...`: "",
|
||||
`-- UI: running npm install`: "",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := compiledFile(in); got != want {
|
||||
t.Errorf("compiledFile(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildError(t *testing.T) {
|
||||
keep := []string{
|
||||
`acc.cu(12): error C2065: 'foo': undeclared identifier`,
|
||||
`LINK : fatal error LNK1104: cannot open file`,
|
||||
// gcc/clang (Linux).
|
||||
`/x/ggml.cpp:42:9: error: 'foo' was not declared in this scope`,
|
||||
}
|
||||
drop := []string{
|
||||
// Les warnings ne doivent PAS remonter (bruit tiers).
|
||||
`ggml.cpp(10): warning C4244: conversion`,
|
||||
`LINK : warning LNK4098: conflit entre la bibliothèque ...`,
|
||||
// Contient -D_CRT_SECURE_NO_WARNINGS mais n'est pas un diagnostic.
|
||||
`nvcc.exe ... -D_CRT_SECURE_NO_WARNINGS -DGGML_SHARED ... -o acc.obj`,
|
||||
`Compiling CUDA source file acc.cu...`,
|
||||
`C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3\bin\nvcc.exe`,
|
||||
}
|
||||
for _, l := range keep {
|
||||
if !reBuildError.MatchString(l) {
|
||||
t.Errorf("erreur attendue mais ratée: %q", l)
|
||||
}
|
||||
}
|
||||
for _, l := range drop {
|
||||
if reBuildError.MatchString(l) {
|
||||
t.Errorf("fausse erreur: %q", l)
|
||||
}
|
||||
}
|
||||
}
|
||||
+122
-1
@@ -4,8 +4,11 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// On Windows there's no systemd unit, sudoers, or /usr/local/bin to populate.
|
||||
@@ -59,16 +62,128 @@ func cmdInstall(args []string) error {
|
||||
fmt.Printf(" %s config.env déjà présent — inchangé\n", dim("•"))
|
||||
}
|
||||
|
||||
// 3. Install the binary into JEAN_HOME\bin and put that dir on the user PATH,
|
||||
// so `jean` is callable from any shell (this is the Windows analogue of the
|
||||
// /usr/local/bin symlink the Unix installer creates).
|
||||
binDir := filepath.Join(jeanHome, "bin")
|
||||
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
onPath := false
|
||||
if dst, err := installSelf(binDir); err != nil {
|
||||
fmt.Printf(" %s copie du binaire impossible (%v) — ajoute-le au PATH à la main\n", dim("•"), err)
|
||||
} else {
|
||||
fmt.Printf(" %s binaire installé %s\n", green("✓"), dst)
|
||||
added, err := addToUserPath(binDir)
|
||||
switch {
|
||||
case err != nil:
|
||||
fmt.Printf(" %s mise à jour du PATH impossible (%v)\n", dim("•"), err)
|
||||
case added:
|
||||
fmt.Printf(" %s %s ajouté au PATH utilisateur\n", green("✓"), binDir)
|
||||
onPath = true
|
||||
default:
|
||||
fmt.Printf(" %s %s déjà dans le PATH\n", dim("•"), binDir)
|
||||
onPath = true
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("%s installation terminée.\n", green("[ok]"))
|
||||
fmt.Printf("\nProchaines étapes :\n")
|
||||
fmt.Printf(" 1. édite la config : %s (renseigne BIN, MODEL)\n", bold("jean edit"))
|
||||
fmt.Printf(" 2. démarre le service: %s\n", bold("jean start"))
|
||||
fmt.Printf(" 3. UI web : %s\n", bold("jean web"))
|
||||
fmt.Printf("\n%s pour exécuter 'jean' depuis n'importe où, ajoute son dossier au PATH.\n", dim("[info]"))
|
||||
if onPath {
|
||||
fmt.Printf("\n%s ouvre un NOUVEAU terminal pour que 'jean' soit reconnu (le PATH n'est lu qu'au démarrage du shell).\n", dim("[info]"))
|
||||
} else {
|
||||
fmt.Printf("\n%s pour exécuter 'jean' depuis n'importe où, ajoute son dossier au PATH.\n", dim("[info]"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// installSelf copies the currently running executable into binDir as jean.exe
|
||||
// and returns the destination path. If the running exe already lives there
|
||||
// (re-install), it's a no-op.
|
||||
func installSelf(binDir string) (string, error) {
|
||||
src, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
src, _ = filepath.EvalSymlinks(src)
|
||||
dst := filepath.Join(binDir, "jean.exe")
|
||||
if strings.EqualFold(src, dst) {
|
||||
return dst, nil
|
||||
}
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer in.Close()
|
||||
// Can't overwrite a running exe, but jean.exe in binDir isn't the one we're
|
||||
// running (src != dst here), so a plain create is fine.
|
||||
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
out.Close()
|
||||
return "", err
|
||||
}
|
||||
if err := out.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
// addToUserPath appends dir to the per-user PATH (HKCU\Environment) persistently,
|
||||
// without admin rights and without setx's 1024-char truncation. Returns false if
|
||||
// dir was already present. The change applies to newly launched shells.
|
||||
func addToUserPath(dir string) (bool, error) {
|
||||
// Read the *user* PATH (not the process PATH, which is User+Machine merged),
|
||||
// edit it, and write it back, all via PowerShell's environment API which
|
||||
// handles the registry REG_EXPAND_SZ type and the WM_SETTINGCHANGE broadcast.
|
||||
ps := fmt.Sprintf(`$d=%s
|
||||
$p=[Environment]::GetEnvironmentVariable('Path','User')
|
||||
if (-not $p) { $p='' }
|
||||
$parts=$p.Split(';') | Where-Object { $_ -ne '' }
|
||||
if ($parts -contains $d) { Write-Output 'present'; exit 0 }
|
||||
$new=(@($parts) + $d) -join ';'
|
||||
[Environment]::SetEnvironmentVariable('Path',$new,'User')
|
||||
Write-Output 'added'`, psQuote(dir))
|
||||
cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", ps)
|
||||
outBytes, err := cmd.CombinedOutput()
|
||||
out := strings.TrimSpace(string(outBytes))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("%v: %s", err, out)
|
||||
}
|
||||
return strings.Contains(out, "added"), nil
|
||||
}
|
||||
|
||||
// removeFromUserPath drops dir from the per-user PATH if present. Returns false
|
||||
// if it wasn't there.
|
||||
func removeFromUserPath(dir string) (bool, error) {
|
||||
ps := fmt.Sprintf(`$d=%s
|
||||
$p=[Environment]::GetEnvironmentVariable('Path','User')
|
||||
if (-not $p) { Write-Output 'absent'; exit 0 }
|
||||
$parts=$p.Split(';') | Where-Object { $_ -ne '' -and $_ -ne $d }
|
||||
if (($p.Split(';') | Where-Object { $_ -eq $d }).Count -eq 0) { Write-Output 'absent'; exit 0 }
|
||||
[Environment]::SetEnvironmentVariable('Path',($parts -join ';'),'User')
|
||||
Write-Output 'removed'`, psQuote(dir))
|
||||
cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", ps)
|
||||
outBytes, err := cmd.CombinedOutput()
|
||||
out := strings.TrimSpace(string(outBytes))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("%v: %s", err, out)
|
||||
}
|
||||
return strings.Contains(out, "removed"), nil
|
||||
}
|
||||
|
||||
// psQuote wraps s in a PowerShell single-quoted string literal (doubling any
|
||||
// embedded single quotes), safe against spaces and metacharacters in the path.
|
||||
func psQuote(s string) string {
|
||||
return "'" + strings.ReplaceAll(s, "'", "''") + "'"
|
||||
}
|
||||
|
||||
func cmdUninstall(args []string) error {
|
||||
keepData := true
|
||||
for _, a := range args {
|
||||
@@ -82,6 +197,12 @@ func cmdUninstall(args []string) error {
|
||||
// Stop the background server if it's running.
|
||||
_ = svcStop(false)
|
||||
|
||||
// Pull JEAN_HOME\bin off the user PATH (best-effort).
|
||||
binDir := filepath.Join(JeanHome(), "bin")
|
||||
if removed, err := removeFromUserPath(binDir); err == nil && removed {
|
||||
fmt.Printf(" %s %s retiré du PATH utilisateur\n", green("✓"), binDir)
|
||||
}
|
||||
|
||||
if !keepData {
|
||||
jeanHome := JeanHome()
|
||||
if err := os.RemoveAll(jeanHome); err != nil {
|
||||
|
||||
+341
-18
@@ -1,13 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// llamacpp.go — gestion du backend llama.cpp (clone, build, mise à jour).
|
||||
@@ -29,6 +34,8 @@ type buildPlan struct {
|
||||
cudaCXX string // chemin de nvcc quand backend == cuda
|
||||
flags []string // flags -D… passés à `cmake -B build`
|
||||
jobs int // parallélisme du build
|
||||
gen string // générateur CMake (-G), vide => défaut de la plateforme
|
||||
genArch string // architecture du générateur (-A), ex. "x64" (VS uniquement)
|
||||
}
|
||||
|
||||
func cmdLlamacpp(args []string) error {
|
||||
@@ -116,6 +123,10 @@ func llamacppInstall(args []string) error {
|
||||
if err := requireTools("git", "cmake"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureCompiler(); err != nil {
|
||||
return err
|
||||
}
|
||||
ensureAccelerator() // best-effort : installe le toolkit GPU si une carte est détectée
|
||||
|
||||
// Dépôt déjà présent ? On bascule sur update plutôt que de re-cloner.
|
||||
if isDir(filepath.Join(repo, ".git")) {
|
||||
@@ -153,9 +164,9 @@ func llamacppInstall(args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
bin := filepath.Join(repo, "build", "bin", "llama-server")
|
||||
if !isFile(bin) {
|
||||
return fmt.Errorf("build terminé mais binaire introuvable: %s", bin)
|
||||
bin := llamaServerBin(repo)
|
||||
if bin == "" {
|
||||
return fmt.Errorf("build terminé mais binaire introuvable sous %s", filepath.Join(repo, "build"))
|
||||
}
|
||||
fmt.Printf("\n%s binaire compilé : %s\n", green("✓"), bin)
|
||||
|
||||
@@ -199,6 +210,10 @@ func llamacppUpdate(args []string) error {
|
||||
if err := requireTools("git", "cmake"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureCompiler(); err != nil {
|
||||
return err
|
||||
}
|
||||
ensureAccelerator() // best-effort : installe le toolkit GPU si une carte est détectée
|
||||
|
||||
repo := llamacppRepoDir()
|
||||
if !isDir(filepath.Join(repo, ".git")) {
|
||||
@@ -224,8 +239,7 @@ func llamacppUpdate(args []string) error {
|
||||
// Déjà à jour ? On s'arrête (sauf --clean / --force qui forcent un rebuild).
|
||||
localRev := gitOutput(repo, "rev-parse", "HEAD")
|
||||
remoteRev := gitOutput(repo, "rev-parse", "origin/"+branch)
|
||||
bin := filepath.Join(repo, "build", "bin", "llama-server")
|
||||
if localRev != "" && localRev == remoteRev && !clean && !force && isFile(bin) {
|
||||
if localRev != "" && localRev == remoteRev && !clean && !force && llamaServerBin(repo) != "" {
|
||||
fmt.Printf("%s déjà à jour (%s) — rien à faire\n", green("[ok]"), oldCommit)
|
||||
fmt.Printf(" (utilise %s pour forcer une recompilation)\n", dim("--force"))
|
||||
return nil
|
||||
@@ -263,8 +277,12 @@ func llamacppUpdate(args []string) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !isFile(bin) {
|
||||
return fmt.Errorf("build terminé mais binaire introuvable: %s", bin)
|
||||
bin := llamaServerBin(repo)
|
||||
if bin == "" {
|
||||
return fmt.Errorf("build terminé mais binaire introuvable sous %s", filepath.Join(repo, "build"))
|
||||
}
|
||||
if err := SetConfigKey("BIN", bin); err != nil {
|
||||
return fmt.Errorf("build ok mais échec écriture BIN dans config.env: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s mis à jour : %s → %s\n", green("✓"), oldCommit, newCommit)
|
||||
@@ -298,8 +316,7 @@ func llamacppStatus(args []string) error {
|
||||
fmt.Printf(" branche : %s\n", branch)
|
||||
fmt.Printf(" commit : %s\n", commit)
|
||||
|
||||
bin := filepath.Join(repo, "build", "bin", "llama-server")
|
||||
if isFile(bin) {
|
||||
if bin := llamaServerBin(repo); bin != "" {
|
||||
fmt.Printf(" binaire : %s\n", green(bin))
|
||||
} else {
|
||||
fmt.Printf(" binaire : %s (pas encore compilé)\n", yellow("absent"))
|
||||
@@ -331,6 +348,29 @@ func detectBuildPlan() buildPlan {
|
||||
p.flags = []string{
|
||||
"-DCMAKE_BUILD_TYPE=Release",
|
||||
"-DGGML_NATIVE=ON",
|
||||
// L'UI web embarquée de llama-server exige npm (ou un téléchargement
|
||||
// d'assets pré-compilés depuis HuggingFace) pour générer un service-worker
|
||||
// PWA — une dépendance lourde qui casse le build sur une machine sans node.
|
||||
// jean fournit sa propre UI, donc on la désactive : build plus rapide et
|
||||
// sans dépendance réseau/npm. BUILD_UI=OFF coupe npm ; USE_PREBUILT_UI=OFF
|
||||
// coupe le téléchargement d'assets pré-compilés depuis HuggingFace (qui
|
||||
// échoue sur un réseau restreint et fait planter l'embed). Sur un checkout
|
||||
// neuf le dist est vide → llama-server embarque une UI vide sans erreur.
|
||||
// Voir scripts/ui-assets.cmake côté llama.cpp.
|
||||
"-DLLAMA_BUILD_UI=OFF",
|
||||
"-DLLAMA_USE_PREBUILT_UI=OFF",
|
||||
}
|
||||
|
||||
// Sur Windows, le générateur CMake par défaut est « NMake Makefiles », qui
|
||||
// suppose un Developer Command Prompt MSVC. On force le générateur Visual
|
||||
// Studio : il localise le toolchain MSVC tout seul via le registre, sans
|
||||
// vcvars, depuis un shell ordinaire.
|
||||
if runtime.GOOS == "windows" {
|
||||
p.gen = msvcGenerator()
|
||||
p.genArch = "x64"
|
||||
if runtime.GOARCH == "arm64" {
|
||||
p.genArch = "ARM64"
|
||||
}
|
||||
}
|
||||
|
||||
if runtime.GOOS == "darwin" {
|
||||
@@ -344,7 +384,10 @@ func detectBuildPlan() buildPlan {
|
||||
if nvcc := findNvcc(); nvcc != "" && hasNvidiaGPU() {
|
||||
p.backend = "cuda"
|
||||
p.cudaCXX = nvcc
|
||||
p.flags = append(p.flags, "-DGGML_CUDA=ON", "-DGGML_CUDA_F16=ON", "-DGGML_CUDA_FA_ALL_QUANTS=ON")
|
||||
// NB : on n'active PAS GGML_CUDA_FA_ALL_QUANTS — il compile les kernels
|
||||
// Flash-Attention pour toutes les combinaisons de quant (des centaines de
|
||||
// .cu), ce qui explose le temps de build pour un gain d'inférence marginal.
|
||||
p.flags = append(p.flags, "-DGGML_CUDA=ON", "-DGGML_CUDA_F16=ON")
|
||||
if arch := detectCudaArch(); arch != "" {
|
||||
p.cudaArch = arch
|
||||
p.flags = append(p.flags, "-DCMAKE_CUDA_ARCHITECTURES="+arch)
|
||||
@@ -391,17 +434,40 @@ func buildLlamacpp(repo string, p buildPlan, clean bool) error {
|
||||
env := ""
|
||||
if p.backend == "cuda" && p.cudaCXX != "" {
|
||||
cudaBin := filepath.Dir(p.cudaCXX)
|
||||
env = "CUDACXX=" + p.cudaCXX + "\x00PATH=" + cudaBin + string(os.PathListSeparator) + os.Getenv("PATH")
|
||||
parts := []string{
|
||||
"CUDACXX=" + p.cudaCXX,
|
||||
"PATH=" + cudaBin + string(os.PathListSeparator) + os.Getenv("PATH"),
|
||||
}
|
||||
// L'intégration MSBuild CUDA (générateur Visual Studio) résout
|
||||
// CudaToolkitDir depuis CUDA_PATH / CUDA_PATH_Vx_y. L'installeur les pose
|
||||
// dans l'environnement persistant, mais pas dans ce process déjà lancé —
|
||||
// on les réinjecte sinon le configure échoue sur « CUDA Toolkit directory '' ».
|
||||
parts = append(parts, cudaPathEnv(filepath.Dir(cudaBin))...)
|
||||
env = strings.Join(parts, "\x00")
|
||||
}
|
||||
|
||||
cfgArgs := append([]string{"-B", "build", "-S", "."}, p.flags...)
|
||||
if err := runStepEnv("cmake configure", repo, env, "cmake", cfgArgs...); err != nil {
|
||||
cfgArgs := []string{"-B", "build", "-S", "."}
|
||||
if p.gen != "" {
|
||||
cfgArgs = append(cfgArgs, "-G", p.gen)
|
||||
if p.genArch != "" {
|
||||
cfgArgs = append(cfgArgs, "-A", p.genArch)
|
||||
}
|
||||
}
|
||||
cfgArgs = append(cfgArgs, p.flags...)
|
||||
cfgLog := filepath.Join(repo, "configure.log")
|
||||
if err := runBuildStep("cmake configure", repo, env, "cmake", cfgLog, cfgArgs...); err != nil {
|
||||
return fmt.Errorf("configuration CMake échouée: %w", err)
|
||||
}
|
||||
|
||||
buildArgs := []string{"--build", "build", "--config", "Release",
|
||||
"-j", fmt.Sprintf("%d", p.jobs), "--target", "llama-server"}
|
||||
if err := runStepEnv("cmake build", repo, env, "cmake", buildArgs...); err != nil {
|
||||
// Générateur Visual Studio : MSBuild réaffiche par défaut la ligne de commande
|
||||
// nvcc complète de chaque kernel (des pavés illisibles). On le passe en
|
||||
// verbosité minimale via les args natifs après « -- ».
|
||||
if strings.HasPrefix(p.gen, "Visual Studio") {
|
||||
buildArgs = append(buildArgs, "--", "/nologo", "/verbosity:minimal")
|
||||
}
|
||||
if err := runBuildStep("cmake build", repo, env, "cmake", filepath.Join(repo, "build.log"), buildArgs...); err != nil {
|
||||
return fmt.Errorf("compilation échouée: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -438,6 +504,26 @@ func findNvcc() string {
|
||||
if p, err := exec.LookPath("nvcc"); err == nil {
|
||||
return p
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
// CUDA_PATH est posé par l'installeur officiel.
|
||||
if cp := os.Getenv("CUDA_PATH"); cp != "" {
|
||||
if p := filepath.Join(cp, "bin", "nvcc.exe"); isFile(p) {
|
||||
return p
|
||||
}
|
||||
}
|
||||
// Layout standard : …\NVIDIA GPU Computing Toolkit\CUDA\v12.x\bin\nvcc.exe
|
||||
for _, base := range []string{os.Getenv("ProgramFiles"), `C:\Program Files`} {
|
||||
if base == "" {
|
||||
continue
|
||||
}
|
||||
matches, _ := filepath.Glob(filepath.Join(base, "NVIDIA GPU Computing Toolkit", "CUDA", "v*", "bin", "nvcc.exe"))
|
||||
if len(matches) > 0 {
|
||||
sort.Strings(matches) // v12.2 < v12.8 → on prend le plus récent
|
||||
return matches[len(matches)-1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
if p := "/usr/local/cuda/bin/nvcc"; isFile(p) {
|
||||
return p
|
||||
}
|
||||
@@ -504,22 +590,66 @@ func isDir(p string) bool {
|
||||
return err == nil && fi.IsDir()
|
||||
}
|
||||
|
||||
// llamaServerBin returns the path to the built llama-server binary under repo,
|
||||
// probing the layouts the different CMake generators emit: the Visual Studio
|
||||
// multi-config generator nests it under build/bin/Release/ and Windows adds a
|
||||
// .exe suffix, whereas the Unix Makefiles generator drops it in build/bin/.
|
||||
// Returns "" when no binary is found.
|
||||
func llamaServerBin(repo string) string {
|
||||
ext := ""
|
||||
if runtime.GOOS == "windows" {
|
||||
ext = ".exe"
|
||||
}
|
||||
for _, rel := range []string{
|
||||
filepath.Join("build", "bin", "Release", "llama-server"+ext),
|
||||
filepath.Join("build", "bin", "llama-server"+ext),
|
||||
filepath.Join("build", "Release", "llama-server"+ext),
|
||||
filepath.Join("build", "llama-server"+ext),
|
||||
} {
|
||||
if p := filepath.Join(repo, rel); isFile(p) {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func hasTool(name string) bool {
|
||||
_, err := exec.LookPath(name)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func requireTools(tools ...string) error {
|
||||
missing := missingTools(tools)
|
||||
if len(missing) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tentative d'installation automatique (winget sur Windows, apt/brew/dnf sur
|
||||
// Unix). On rafraîchit ensuite le PATH du process car un installeur système
|
||||
// écrit le PATH machine sans toucher l'environnement déjà chargé.
|
||||
fmt.Printf("%s outils manquants: %s — installation automatique…\n", yellow("[info]"), strings.Join(missing, ", "))
|
||||
for _, t := range missing {
|
||||
if err := autoInstallTool(t); err != nil {
|
||||
fmt.Printf(" %s %s: %v\n", dim("•"), t, err)
|
||||
}
|
||||
}
|
||||
refreshToolPath()
|
||||
|
||||
if still := missingTools(tools); len(still) > 0 {
|
||||
return fmt.Errorf("outils toujours manquants après tentative d'install: %s — installe-les à la main puis réessaie", strings.Join(still, ", "))
|
||||
}
|
||||
fmt.Printf("%s outils installés.\n", green("✓"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func missingTools(tools []string) []string {
|
||||
var missing []string
|
||||
for _, t := range tools {
|
||||
if !hasTool(t) {
|
||||
missing = append(missing, t)
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
return fmt.Errorf("outils manquants: %s — installe-les puis réessaie", strings.Join(missing, ", "))
|
||||
}
|
||||
return nil
|
||||
return missing
|
||||
}
|
||||
|
||||
// gitOutput runs a git command in `dir` and returns trimmed stdout (or "").
|
||||
@@ -560,6 +690,199 @@ func runStepEnv(name, dir, extraEnv, bin string, args ...string) error {
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// runBuildStep runs a compile step while keeping the terminal clean: the full
|
||||
// output goes to logPath, and the screen shows only a single self-rewriting
|
||||
// progress line (spinner + compiled-file count) plus any real compiler
|
||||
// diagnostics. The hundreds of per-file nvcc/cl command echoes are hidden. On
|
||||
// failure the tail of the log is printed so the actual error is never lost.
|
||||
func runBuildStep(name, dir, extraEnv, bin, logPath string, args ...string) error {
|
||||
fmt.Printf("\n%s %s\n", cyan("▶"), name)
|
||||
cmd := exec.Command(bin, args...)
|
||||
cmd.Dir = dir
|
||||
if extraEnv != "" {
|
||||
env := os.Environ()
|
||||
for _, kv := range strings.Split(extraEnv, "\x00") {
|
||||
if kv != "" {
|
||||
env = upsertEnv(env, kv)
|
||||
}
|
||||
}
|
||||
cmd.Env = env
|
||||
}
|
||||
|
||||
var logf *os.File
|
||||
if logPath != "" {
|
||||
if f, err := os.Create(logPath); err == nil {
|
||||
logf = f
|
||||
defer logf.Close()
|
||||
}
|
||||
}
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
cmd.Stdout = pw
|
||||
cmd.Stderr = pw
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
frames := []rune{'⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'}
|
||||
var (
|
||||
mu sync.Mutex
|
||||
count int
|
||||
label = "préparation…"
|
||||
fi int
|
||||
)
|
||||
clearLine := func() {
|
||||
if colorOn {
|
||||
fmt.Print("\r\033[K")
|
||||
}
|
||||
}
|
||||
// draw redessine la ligne d'état ; appelé par une horloge pour rester animé
|
||||
// même quand un seul gros fichier compile pendant plusieurs minutes.
|
||||
draw := func() {
|
||||
if !colorOn {
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
fi = (fi + 1) % len(frames)
|
||||
fmt.Printf("\r\033[K %c %s", frames[fi], label)
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
sc := bufio.NewScanner(pr)
|
||||
sc.Buffer(make([]byte, 1<<20), 1<<20) // les échos de commande sont énormes
|
||||
for sc.Scan() {
|
||||
line := sc.Text()
|
||||
if logf != nil {
|
||||
fmt.Fprintln(logf, line)
|
||||
}
|
||||
mu.Lock()
|
||||
if f := compiledFile(line); f != "" {
|
||||
count++
|
||||
label = fmt.Sprintf("compilation… %d fichiers %s", count, dim("("+f+")"))
|
||||
mu.Unlock()
|
||||
continue
|
||||
}
|
||||
if p := phaseLabel(line); p != "" {
|
||||
label = p
|
||||
}
|
||||
mu.Unlock()
|
||||
// On ne fait remonter que les vraies ERREURS (les warnings MSVC/linker
|
||||
// d'un projet tiers sont du bruit ; ils restent dans le log). Les CMake
|
||||
// Error de la phase configure sont aussi affichés.
|
||||
if reBuildError.MatchString(line) || strings.HasPrefix(strings.TrimSpace(line), "CMake Error") {
|
||||
mu.Lock()
|
||||
clearLine()
|
||||
fmt.Println(" " + strings.TrimSpace(line))
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Horloge d'animation, indépendante du flux de sortie.
|
||||
stop := make(chan struct{})
|
||||
tickerDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(tickerDone)
|
||||
t := time.NewTicker(120 * time.Millisecond)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-t.C:
|
||||
draw()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err := cmd.Wait()
|
||||
_ = pw.Close()
|
||||
<-done
|
||||
close(stop)
|
||||
<-tickerDone
|
||||
clearLine()
|
||||
if err == nil && count > 0 {
|
||||
fmt.Printf(" %s %d fichiers compilés\n", green("✓"), count)
|
||||
}
|
||||
if err != nil && logPath != "" {
|
||||
fmt.Printf("%s étape échouée — log complet : %s\n", yellow("[err]"), logPath)
|
||||
printLogTail(logPath, 30)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// phaseLabel maps a non-compile output line to a short status label, or "" to
|
||||
// leave the current label unchanged. Keeps the spinner informative during the
|
||||
// CMake configure phase and the final link.
|
||||
func phaseLabel(line string) string {
|
||||
t := strings.TrimSpace(line)
|
||||
switch {
|
||||
case strings.HasPrefix(t, "-- "):
|
||||
return "configuration… " + truncLabel(strings.TrimPrefix(t, "-- "), 50)
|
||||
case strings.Contains(t, "Linking") || strings.Contains(t, "Build files have been written"):
|
||||
return "édition de liens…"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func truncLabel(s string, n int) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s) > n {
|
||||
return s[:n-1] + "…"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var (
|
||||
// MSBuild (Windows) : « Compiling CUDA source file …\foo.cu… » ou nom de
|
||||
// source seul « foo.cpp » imprimé par cl.
|
||||
reCompilingCUDA = regexp.MustCompile(`Compiling .*?([\w.\-]+\.cu)\b`)
|
||||
reBareSource = regexp.MustCompile(`^[\w.\-]+\.(c|cc|cpp|cxx|cu|cuh)$`)
|
||||
// Make / Ninja (Linux, macOS) : « [ 45%] Building CXX object …/foo.cpp.o » ou
|
||||
// « [12/345] Building CUDA object …/foo.cu.o ».
|
||||
reBuildingObj = regexp.MustCompile(`Building (?:C|CXX|CUDA|ASM)\w* object .*?/([^/]+?)\.o(?:bj)?\b`)
|
||||
// Vraies erreurs : « foo.cpp(12): error C2065 » (MSVC), « foo.cpp:12:5: error: »
|
||||
// (gcc/clang), « LINK : fatal error LNK1104 ». On exige le « : » devant le
|
||||
// mot-clé pour ne PAS matcher les flags type -D_CRT_SECURE_NO_WARNINGS dans les
|
||||
// lignes de commande. Les warnings (bruit d'un projet tiers) sont exclus.
|
||||
reBuildError = regexp.MustCompile(`(?i):\s*(fatal error|error)\b`)
|
||||
)
|
||||
|
||||
// compiledFile returns the source filename a build line announces compiling, or
|
||||
// "" if the line isn't a compile-progress marker. Handles both the MSBuild
|
||||
// (Windows) and Make/Ninja (Unix) output formats.
|
||||
func compiledFile(line string) string {
|
||||
t := strings.TrimSpace(line)
|
||||
if m := reCompilingCUDA.FindStringSubmatch(t); m != nil {
|
||||
return m[1]
|
||||
}
|
||||
if m := reBuildingObj.FindStringSubmatch(t); m != nil {
|
||||
return m[1]
|
||||
}
|
||||
if reBareSource.MatchString(t) {
|
||||
return t
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// printLogTail prints the last n lines of the log file (best-effort).
|
||||
func printLogTail(path string, n int) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
lines := strings.Split(strings.TrimRight(string(b), "\n"), "\n")
|
||||
if len(lines) > n {
|
||||
lines = lines[len(lines)-n:]
|
||||
}
|
||||
for _, l := range lines {
|
||||
fmt.Println(" " + dim(l))
|
||||
}
|
||||
}
|
||||
|
||||
// upsertEnv replaces KEY=… in env if present, else appends kv (kv is "KEY=VAL").
|
||||
func upsertEnv(env []string, kv string) []string {
|
||||
key := kv
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const Version = "0.2.4"
|
||||
const Version = "0.2.5"
|
||||
|
||||
func main() {
|
||||
args := os.Args[1:]
|
||||
|
||||
@@ -4,6 +4,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -57,6 +58,82 @@ func cudaLibDirs() []string {
|
||||
return dirs
|
||||
}
|
||||
|
||||
// autoInstallTool installs a missing build tool with the system package manager
|
||||
// (apt/dnf/pacman on Linux, brew on macOS). Best-effort: returns an error when no
|
||||
// known manager is available or the install fails. Uses sudo on Linux when not
|
||||
// already root (brew refuses to run as root).
|
||||
func autoInstallTool(name string) error {
|
||||
managers := []struct {
|
||||
bin string
|
||||
install []string // args before the package name
|
||||
}{
|
||||
{"apt-get", []string{"install", "-y"}},
|
||||
{"dnf", []string{"install", "-y"}},
|
||||
{"pacman", []string{"-S", "--noconfirm"}},
|
||||
{"brew", []string{"install"}},
|
||||
}
|
||||
for _, m := range managers {
|
||||
if _, err := exec.LookPath(m.bin); err != nil {
|
||||
continue
|
||||
}
|
||||
argv := append(append([]string{m.bin}, m.install...), name)
|
||||
if m.bin != "brew" && os.Geteuid() != 0 {
|
||||
if _, err := exec.LookPath("sudo"); err != nil {
|
||||
return fmt.Errorf("%s requiert root (ni root ni sudo disponibles)", m.bin)
|
||||
}
|
||||
argv = append([]string{"sudo"}, argv...)
|
||||
}
|
||||
cmd := exec.Command(argv[0], argv[1:]...)
|
||||
cmd.Stdout, cmd.Stderr, cmd.Stdin = os.Stdout, os.Stderr, os.Stdin
|
||||
return cmd.Run()
|
||||
}
|
||||
return fmt.Errorf("aucun gestionnaire de paquets connu — installe %s manuellement", name)
|
||||
}
|
||||
|
||||
// msvcGenerator is Windows-only; on Unix the default CMake generator (Unix
|
||||
// Makefiles) is correct, so detectBuildPlan never calls this for real. Present
|
||||
// only so the shared code compiles.
|
||||
func msvcGenerator() string { return "" }
|
||||
|
||||
// ensureCompiler makes sure a C/C++ toolchain (cc + c++ + make) is present,
|
||||
// installing it via the system package manager when missing. build-essential on
|
||||
// Debian/Ubuntu pulls the lot; elsewhere we fall back to individual packages.
|
||||
func ensureCompiler() error {
|
||||
haveCC := hasTool("cc") || hasTool("gcc") || hasTool("clang")
|
||||
haveCXX := hasTool("c++") || hasTool("g++") || hasTool("clang++")
|
||||
if haveCC && haveCXX && hasTool("make") {
|
||||
return nil
|
||||
}
|
||||
candidates := []string{"build-essential", "gcc", "g++", "make"}
|
||||
if _, err := exec.LookPath("apt-get"); err != nil {
|
||||
// Non-Debian: build-essential n'existe pas, on vise les paquets directs.
|
||||
candidates = []string{"gcc", "gcc-c++", "make"}
|
||||
}
|
||||
fmt.Println(yellow("[info]") + " compilateur C/C++ absent — installation via le gestionnaire de paquets…")
|
||||
for _, pkg := range candidates {
|
||||
_ = autoInstallTool(pkg) // best-effort, paquets variables selon la distro
|
||||
}
|
||||
if (hasTool("cc") || hasTool("gcc")) && (hasTool("c++") || hasTool("g++")) && hasTool("make") {
|
||||
fmt.Println(green("✓") + " compilateur C/C++ prêt.")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("compilateur C/C++ introuvable — installe gcc/g++/make (ou build-essential) manuellement")
|
||||
}
|
||||
|
||||
// cudaPathEnv is Windows-specific (the MSBuild CUDA integration needs CUDA_PATH);
|
||||
// on Unix the Makefiles/Ninja generators find nvcc via PATH/CUDACXX, so there's
|
||||
// nothing extra to inject.
|
||||
func cudaPathEnv(toolkitDir string) []string { return nil }
|
||||
|
||||
// ensureAccelerator is a no-op on Unix: CUDA/ROCm toolkits are installed through
|
||||
// the distro (their layout is already probed by findNvcc / detectBuildPlan), and
|
||||
// auto-installing multi-GB GPU toolkits across distros is too varied to do safely.
|
||||
func ensureAccelerator() {}
|
||||
|
||||
// refreshToolPath is a no-op on Unix: package managers install into directories
|
||||
// already on PATH (/usr/bin, /usr/local/bin), unlike Windows.
|
||||
func refreshToolPath() {}
|
||||
|
||||
// execServer replaces the current process with llama-server (so systemd
|
||||
// supervises llama-server directly, as the old start.sh did with `exec`).
|
||||
// args[0] must be the binary path.
|
||||
|
||||
+214
-7
@@ -4,11 +4,41 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// init makes the Windows console behave like a modern terminal: UTF-8 so the
|
||||
// Unicode glyphs jean prints (✓ ▶ …) and child-process output don't turn into
|
||||
// mojibake (the default OEM codepage, e.g. cp850, renders "✓" as "├ö"), and VT
|
||||
// processing so the ANSI colour/cursor escapes (including the build progress
|
||||
// line) are interpreted instead of printed literally. Best-effort: a redirected
|
||||
// or legacy console just keeps its defaults.
|
||||
func init() {
|
||||
const (
|
||||
cpUTF8 = 65001
|
||||
enableVirtualTerminalProcessing = 0x0004
|
||||
stdOutputHandle = ^uintptr(10) // -11 as DWORD
|
||||
)
|
||||
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||
_, _, _ = kernel32.NewProc("SetConsoleOutputCP").Call(uintptr(cpUTF8))
|
||||
_, _, _ = kernel32.NewProc("SetConsoleCP").Call(uintptr(cpUTF8))
|
||||
|
||||
getStdHandle := kernel32.NewProc("GetStdHandle")
|
||||
getConsoleMode := kernel32.NewProc("GetConsoleMode")
|
||||
setConsoleMode := kernel32.NewProc("SetConsoleMode")
|
||||
h, _, _ := getStdHandle.Call(stdOutputHandle)
|
||||
var mode uint32
|
||||
if r, _, _ := getConsoleMode.Call(h, uintptr(unsafe.Pointer(&mode))); r != 0 {
|
||||
_, _, _ = setConsoleMode.Call(h, uintptr(mode|enableVirtualTerminalProcessing))
|
||||
}
|
||||
}
|
||||
|
||||
// defaultJeanHome is the data root when $JEAN_HOME is unset. We use
|
||||
// %ProgramData%\jean (machine-wide, the closest analogue to /etc/jean), falling
|
||||
// back to %LOCALAPPDATA%\jean for unprivileged setups.
|
||||
@@ -25,15 +55,28 @@ func defaultJeanHome() string {
|
||||
// defaultEditor is used by `jean edit` when $EDITOR is unset.
|
||||
func defaultEditor() string { return "notepad" }
|
||||
|
||||
// setLibraryPath ensures llama-server can load DLLs bundled next to the binary.
|
||||
// Windows resolves dependent DLLs via PATH (and the binary's own directory), so
|
||||
// we prepend dir to PATH.
|
||||
// setLibraryPath ensures llama-server can load its dependent DLLs. Windows
|
||||
// resolves them via PATH (and the binary's own directory), so we prepend dir.
|
||||
// For a CUDA build we must also add the CUDA Toolkit's bin: ggml-cuda.dll links
|
||||
// against cublas64_*/cublasLt64_*.dll which live there, not next to the binary —
|
||||
// without it the server dies with 0xC0000135 (DLL not found) unless the launching
|
||||
// shell happened to have CUDA on PATH.
|
||||
func setLibraryPath(dir string) {
|
||||
p := dir
|
||||
if existing := os.Getenv("PATH"); existing != "" {
|
||||
p = dir + string(os.PathListSeparator) + existing
|
||||
parts := []string{dir}
|
||||
if nvcc := findNvcc(); nvcc != "" {
|
||||
binDir := filepath.Dir(nvcc) // …\CUDA\vX.Y\bin
|
||||
parts = append(parts, binDir)
|
||||
// CUDA 13+ a déplacé les DLL runtime (cublas64_*, cublasLt64_*, cudart64_*)
|
||||
// dans bin\x64\ ; sur CUDA 12 elles sont directement dans bin\. On ajoute
|
||||
// les deux pour couvrir les deux layouts.
|
||||
if x64 := filepath.Join(binDir, "x64"); isDir(x64) {
|
||||
parts = append(parts, x64)
|
||||
}
|
||||
}
|
||||
_ = os.Setenv("PATH", p)
|
||||
if existing := os.Getenv("PATH"); existing != "" {
|
||||
parts = append(parts, existing)
|
||||
}
|
||||
_ = os.Setenv("PATH", strings.Join(parts, string(os.PathListSeparator)))
|
||||
}
|
||||
|
||||
// execServer runs llama-server as a child process and waits for it. Windows has
|
||||
@@ -49,6 +92,170 @@ func execServer(bin string, args []string) error {
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// wingetIDs maps a tool's command name to its winget package ID.
|
||||
var wingetIDs = map[string]string{
|
||||
"git": "Git.Git",
|
||||
"cmake": "Kitware.CMake",
|
||||
"ninja": "Ninja-build.Ninja",
|
||||
}
|
||||
|
||||
// autoInstallTool installs a missing build tool via winget (bundled with Windows
|
||||
// 10/11 and Server 2025). Returns an error if winget is absent or the install
|
||||
// fails; the caller re-checks availability afterwards.
|
||||
func autoInstallTool(name string) error {
|
||||
if _, err := exec.LookPath("winget"); err != nil {
|
||||
return fmt.Errorf("winget introuvable — installe %s manuellement", name)
|
||||
}
|
||||
id, ok := wingetIDs[name]
|
||||
if !ok {
|
||||
id = name
|
||||
}
|
||||
cmd := exec.Command("winget", "install", "--id", id, "-e",
|
||||
"--accept-source-agreements", "--accept-package-agreements",
|
||||
"--disable-interactivity", "--silent")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// refreshToolPath reloads the process PATH from the Windows registry (Machine +
|
||||
// User), so tools just installed by winget become resolvable without restarting
|
||||
// the shell.
|
||||
func refreshToolPath() {
|
||||
ps := `$m=[Environment]::GetEnvironmentVariable('Path','Machine')
|
||||
$u=[Environment]::GetEnvironmentVariable('Path','User')
|
||||
Write-Output ((@($m,$u) | Where-Object { $_ }) -join ';')`
|
||||
out, err := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", ps).Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if merged := strings.TrimSpace(string(out)); merged != "" {
|
||||
_ = os.Setenv("PATH", merged)
|
||||
}
|
||||
}
|
||||
|
||||
// vswherePath returns the location of vswhere.exe, the official tool for
|
||||
// locating Visual Studio / Build Tools installs. It ships in a fixed spot.
|
||||
func vswherePath() string {
|
||||
base := os.Getenv("ProgramFiles(x86)")
|
||||
if base == "" {
|
||||
base = os.Getenv("ProgramFiles")
|
||||
}
|
||||
return filepath.Join(base, "Microsoft Visual Studio", "Installer", "vswhere.exe")
|
||||
}
|
||||
|
||||
// msvcInstallVersion returns the major version of the newest MSVC install that
|
||||
// has the C++ toolchain (e.g. "17"), or "" if none is found.
|
||||
func msvcInstallVersion() string {
|
||||
vs := vswherePath()
|
||||
if _, err := os.Stat(vs); err != nil {
|
||||
return ""
|
||||
}
|
||||
out, err := exec.Command(vs, "-latest", "-products", "*",
|
||||
"-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
|
||||
"-property", "installationVersion").Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
ver := strings.TrimSpace(string(out))
|
||||
if ver == "" {
|
||||
return ""
|
||||
}
|
||||
if i := strings.IndexByte(ver, '.'); i > 0 {
|
||||
return ver[:i]
|
||||
}
|
||||
return ver
|
||||
}
|
||||
|
||||
// msvcGenerator returns the CMake generator name for the installed MSVC, falling
|
||||
// back to VS 2022 (the version `ensureCompiler` installs).
|
||||
func msvcGenerator() string {
|
||||
switch msvcInstallVersion() {
|
||||
case "16":
|
||||
return "Visual Studio 16 2019"
|
||||
case "15":
|
||||
return "Visual Studio 15 2017"
|
||||
default:
|
||||
return "Visual Studio 17 2022"
|
||||
}
|
||||
}
|
||||
|
||||
// ensureCompiler makes sure an MSVC C++ toolchain is available, installing the
|
||||
// Visual Studio 2022 Build Tools (VCTools workload) via winget if not. This is a
|
||||
// large download but keeps `jean llamacpp install` fully unattended on a bare
|
||||
// Windows box.
|
||||
func ensureCompiler() error {
|
||||
if msvcInstallVersion() != "" {
|
||||
return nil
|
||||
}
|
||||
if _, err := exec.LookPath("winget"); err != nil {
|
||||
return fmt.Errorf("compilateur C++ absent et winget introuvable — installe « Visual Studio Build Tools » (charge de travail C++) manuellement")
|
||||
}
|
||||
fmt.Printf("%s compilateur C++ absent — installation des Build Tools MSVC (gros téléchargement, une seule fois)…\n", yellow("[info]"))
|
||||
cmd := exec.Command("winget", "install", "--id", "Microsoft.VisualStudio.2022.BuildTools", "-e",
|
||||
"--accept-source-agreements", "--accept-package-agreements",
|
||||
"--disable-interactivity",
|
||||
"--override", "--quiet --wait --norestart --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("installation des Build Tools MSVC échouée: %w", err)
|
||||
}
|
||||
if msvcInstallVersion() == "" {
|
||||
return fmt.Errorf("Build Tools installés mais toolchain C++ introuvable — relance la commande ou vérifie l'installation Visual Studio")
|
||||
}
|
||||
fmt.Printf("%s compilateur C++ prêt.\n", green("✓"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureAccelerator installs the CUDA Toolkit when an NVIDIA GPU is present but
|
||||
// nvcc isn't, so the build can target the GPU instead of falling back to CPU.
|
||||
// Best-effort: any failure just leaves the machine on the CPU path (the caller
|
||||
// ignores the return value). The CUDA download is large; we only trigger it when
|
||||
// a GPU is actually detected.
|
||||
func ensureAccelerator() {
|
||||
if !hasNvidiaGPU() {
|
||||
return // pas de GPU NVIDIA visible → rien à installer, build CPU
|
||||
}
|
||||
if findNvcc() != "" {
|
||||
return // toolkit déjà présent
|
||||
}
|
||||
if _, err := exec.LookPath("winget"); err != nil {
|
||||
fmt.Printf("%s GPU NVIDIA détecté mais CUDA Toolkit absent et winget introuvable — build CPU (installe le CUDA Toolkit pour l'accélération GPU)\n", yellow("[info]"))
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s GPU NVIDIA détecté — installation du CUDA Toolkit pour l'accélération GPU (gros téléchargement, une seule fois)…\n", yellow("[info]"))
|
||||
cmd := exec.Command("winget", "install", "--id", "Nvidia.CUDA", "-e",
|
||||
"--accept-source-agreements", "--accept-package-agreements",
|
||||
"--disable-interactivity")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("%s installation du CUDA Toolkit échouée (%v) — on continue en CPU\n", yellow("[warn]"), err)
|
||||
return
|
||||
}
|
||||
refreshToolPath()
|
||||
if findNvcc() != "" {
|
||||
fmt.Printf("%s CUDA Toolkit prêt — build GPU activé.\n", green("✓"))
|
||||
} else {
|
||||
fmt.Printf("%s CUDA Toolkit installé mais nvcc introuvable dans cette session — relance la commande pour activer le GPU\n", yellow("[info]"))
|
||||
}
|
||||
}
|
||||
|
||||
// cudaPathEnv returns the CUDA toolkit env vars the MSBuild CUDA integration
|
||||
// needs (CUDA_PATH and the version-specific CUDA_PATH_Vx_y), derived from the
|
||||
// toolkit root, e.g. "...\CUDA\v13.3" → CUDA_PATH_V13_3. Returns NUL-free
|
||||
// "KEY=VAL" strings.
|
||||
func cudaPathEnv(toolkitDir string) []string {
|
||||
out := []string{"CUDA_PATH=" + toolkitDir}
|
||||
// Le dossier se nomme "v13.3" → variable CUDA_PATH_V13_3.
|
||||
ver := strings.TrimPrefix(filepath.Base(toolkitDir), "v")
|
||||
if ver != "" {
|
||||
out = append(out, "CUDA_PATH_V"+strings.ReplaceAll(ver, ".", "_")+"="+toolkitDir)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// newShellCmd builds the command used by the run_shell tool.
|
||||
func newShellCmd(ctx context.Context, command string) *exec.Cmd {
|
||||
return exec.CommandContext(ctx, "cmd", "/C", command)
|
||||
|
||||
@@ -268,6 +268,9 @@ func handleBackends(w http.ResponseWriter, r *http.Request) {
|
||||
subpaths := []string{
|
||||
"build/bin/llama-server", "build-sm120/bin/llama-server",
|
||||
"build/llama-server", "bin/llama-server", "llama-server",
|
||||
// Layout du générateur Visual Studio (multi-config) + suffixe .exe Windows.
|
||||
"build/bin/Release/llama-server.exe", "build/bin/llama-server.exe",
|
||||
"build/bin/Release/llama-server", "llama-server.exe",
|
||||
}
|
||||
out := []map[string]any{}
|
||||
for _, e := range entries {
|
||||
|
||||
Reference in new issue
Block a user