Files
Loki/internal/ajean/sys_install_unix.go
T
nathaninline 1aabc0f9c7 Installateurs Unix fusionnes, staticcheck sans avertissement
- sys_install_unix.go porte le parcours commun (arborescence, lien du binaire,
  /etc/default, chown) ; Linux et macOS ne gardent que leur declaration de
  services derriere installServices/uninstallServices (395 -> 339 lignes, et
  surtout une seule copie a corriger)
- l'installateur macOS pose desormais les DEUX services (il n'en posait qu'un)
- API bbolt depreciee remplacee, message d'erreur duplique factorise
- staticcheck.conf : ST1005 ecartee, avec la raison (messages francais montres
  a l'utilisateur). Le projet est a zero avertissement.
2026-08-07 17:57:51 +02:00

154 lines
4.9 KiB
Go

//go:build !windows
package ajean
import (
"fmt"
"os"
"os/user"
"path/filepath"
"strings"
)
// sys_install_unix.go — installation et désinstallation, partie commune à Linux
// et macOS. Les deux plateformes font exactement la même chose, à un détail
// près : la façon de déclarer les services (systemd d'un côté, launchd de
// l'autre). Ce détail vit dans sys_install_linux.go / sys_install_darwin.go,
// derrière deux fonctions : installServices et uninstallServices.
//
// Avant, chaque plateforme avait sa copie intégrale du parcours : deux fichiers
// identiques à 60 %, donc toute correction à faire deux fois. Comme le support
// macOS n'est pas testé sur une vraie machine, c'est précisément là qu'une
// divergence passait inaperçue.
func cmdInstall(args []string) error {
if os.Geteuid() != 0 {
return fmt.Errorf("ajean install doit être exécuté en root (sudo ajean install)")
}
targetUser := os.Getenv("SUDO_USER")
if targetUser == "" {
targetUser = "root"
}
for _, a := range args {
if strings.HasPrefix(a, "--user=") {
targetUser = strings.TrimPrefix(a, "--user=")
}
}
u, err := user.Lookup(targetUser)
if err != nil {
return fmt.Errorf("utilisateur '%s' introuvable: %w", targetUser, err)
}
ajeanHome := defaultAjeanHome()
if v := os.Getenv("AJEAN_HOME"); v != "" {
ajeanHome = v
}
fmt.Printf("Installation pour utilisateur %s\n", cyan(targetUser))
fmt.Printf(" AJEAN_HOME = %s\n", ajeanHome)
fmt.Printf(" services = %s + %s\n", serviceName(), uiUnitName)
// 1. Arborescence + configuration de départ.
if err := provisionDataDir(); err != nil {
return err
}
fmt.Printf(" %s arborescence prête\n", green("✓"))
// 2. Lien vers /usr/local/bin/ajean.
if err := linkInstalledExe(); err != nil {
return err
}
// 3. /etc/default/ajean, pour que les invocations root résolvent AJEAN_HOME.
if err := os.MkdirAll("/etc/default", 0o755); err != nil {
return err
}
defaults := fmt.Sprintf("# Généré par ajean install — racine des données\nAJEAN_HOME=%s\n", ajeanHome)
if err := os.WriteFile("/etc/default/ajean", []byte(defaults), 0o644); err != nil {
return err
}
fmt.Printf(" %s /etc/default/ajean\n", green("✓"))
// 4. Déclaration des services (systemd ou launchd selon la plateforme).
if err := installServices(targetUser, ajeanHome); err != nil {
return err
}
// 5. AJEAN_HOME appartient à l'utilisateur cible : les deux services et les
// commandes qu'il tape doivent pouvoir écrire dans la base.
chown(ajeanHome, u)
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("sudo -u "+targetUser+" ajean edit"))
fmt.Printf(" 2. démarre le moteur : %s\n", bold("sudo ajean start"))
fmt.Printf(" 3. démarre l'interface: %s\n", bold("sudo ajean ui start"))
return nil
}
// linkInstalledExe pose /usr/local/bin/ajean sur le binaire courant.
//
// Garde-fou (issue #5) : si on tourne DÉJÀ depuis la cible — l'utilisateur a
// posé le binaire là puis lancé `sudo ajean install` — il ne faut surtout pas
// Remove+Symlink sur soi-même : ça effacerait le vrai binaire et créerait un
// lien vers lui-même (« Too many levels of symbolic links »). On compare les
// chemins réels, résolus.
func linkInstalledExe() error {
self, err := os.Executable()
if err != nil {
return err
}
if rp, e := filepath.EvalSymlinks(self); e == nil {
self = rp
}
target := installedExePath()
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return err
}
if tp, e := filepath.EvalSymlinks(target); e == nil && tp == self {
fmt.Printf(" %s %s est déjà le binaire installé (aucun lien à créer)\n", green("✓"), target)
return nil
}
_ = os.Remove(target)
if err := os.Symlink(self, target); err != nil {
// Repli sur une copie si le lien échoue (volumes différents).
data, rerr := os.ReadFile(self)
if rerr != nil {
return rerr
}
if werr := os.WriteFile(target, data, 0o755); werr != nil {
return werr
}
}
fmt.Printf(" %s %s -> %s\n", green("✓"), target, self)
return nil
}
func cmdUninstall(args []string) error {
if os.Geteuid() != 0 {
return fmt.Errorf("ajean uninstall doit être exécuté en root")
}
uninstallServices()
for _, p := range []string{"/etc/default/ajean", installedExePath()} {
if err := os.Remove(p); err == nil {
fmt.Printf(" %s %s\n", green("✓"), p)
}
}
fmt.Println(dim("(données utilisateur conservées — supprime $AJEAN_HOME manuellement si tu veux purger)"))
fmt.Println(green("[ok]") + " désinstallé")
return nil
}
// chown donne récursivement path à l'utilisateur/groupe indiqué.
func chown(path string, u *user.User) {
var uid, gid int
fmt.Sscanf(u.Uid, "%d", &uid)
fmt.Sscanf(u.Gid, "%d", &gid)
_ = filepath.Walk(path, func(p string, info os.FileInfo, err error) error {
if err == nil {
_ = os.Chown(p, uid, gid)
}
return nil
})
}