## ────────────────────────────────────────────── ## WGSecure (WGS) — Makefile ## Compilation Linux & Windows via PyInstaller ## ────────────────────────────────────────────── APP := wgsecure VERSION := 0.13.0 VENV := .venv PYTHON := $(VENV)/bin/python3 PIP := $(VENV)/bin/pip SRC := main.py DIST := dist BUILD := build ICON := /tmp/wgsecure_icon_build.png # Options PyInstaller communes aux deux plateformes # `--clean` vide le répertoire de travail au démarrage. Linux et Windows # portant le même `--name`, ils partageaient `build/wgsecure/` : lancés à la # suite, chacun effaçait les fichiers intermédiaires de l'autre en plein # travail. Résultat observé le 8 septembre 2026 — un .exe Windows gonflé de # 44 Mo et un binaire Linux tronqué dont l'archive ne se décompressait plus # (« decompression resulted in return code -3 »). D'où un workpath par cible. PI_OPTS := \ --noconfirm \ --clean \ --name $(APP) \ --hidden-import qrcode \ --hidden-import qrcode.image.pil \ --hidden-import PIL \ --hidden-import PIL.Image \ --hidden-import cryptography \ --hidden-import cryptography.hazmat.primitives.asymmetric.x25519 \ --collect-submodules PyQt6 .PHONY: all linux windows helper windows-release check-windows-release installer-deps release publish publish-updates version changelog install install-gnome uninstall-gnome run run-admin icon reset-password setup-sudoers check-privileges clean clean-all help venv # ── Cible par défaut ──────────────────────────────────────────────────────── all: linux # ── Environnement virtuel ──────────────────────────────────────────────────── # Le venv est reconstruit si la version de Python du système a changé : un venv # créé pour python3.13 pointe vers .venv/lib/python3.13/site-packages, que # python3.14 n'ouvre pas. L'interpréteur démarrait alors sans aucune de ses # dépendances (« No module named PyQt6 ») sans que make ne le remarque. venv: @if [ -x $(PYTHON) ] && \ [ "$$($(PYTHON) -c 'import sys;print("%d.%d"%sys.version_info[:2])' 2>/dev/null)" \ = "$$(python3 -c 'import sys;print("%d.%d"%sys.version_info[:2])')" ] && \ $(PYTHON) -c 'import PyQt6' >/dev/null 2>&1; then \ echo " ✅ Environnement virtuel valide ($(VENV))"; \ else \ echo " ♻️ (Re)création de l'environnement virtuel $(VENV)…"; \ rm -rf $(VENV); \ python3 -m venv $(VENV); \ $(PIP) install --upgrade pip --quiet; \ $(PIP) install -r requirements.txt --quiet; \ fi # ── Génération de l'icône PNG (utilisée par PyInstaller) ──────────────────── icon: @$(PYTHON) -c "\ import sys; sys.path.insert(0,'.');\ from PyQt6.QtWidgets import QApplication; app=QApplication(sys.argv);\ from app.ui.icons import icon_app;\ pix=icon_app().pixmap(256,256); pix.save('$(ICON)','PNG');\ print(' Icône générée →', '$(ICON)')" 2>/dev/null @test -f $(ICON) || (echo " ❌ Génération icône échouée"; exit 1) # ── Binaire Linux ──────────────────────────────────────────────────────────── linux: install icon @echo "" @echo " 🐧 Compilation Linux (PyInstaller onefile)…" @echo "" $(PYTHON) -m PyInstaller $(PI_OPTS) \ --workpath build/linux \ --onefile \ --icon=$(ICON) \ --add-data "CHANGELOG.md:." \ $(SRC) @echo "" @echo " ✅ Binaire Linux → $(DIST)/$(APP)" @echo "" # ── Binaire Windows (via Wine + Python Windows) ────────────────────────────── # Le hook PyQt6 de pyinstaller-hooks-contrib introspecte QLibraryInfo pour # localiser Qt6/plugins/platforms au moment du build : sous Wine cette # introspection échoue silencieusement, donc qwindows.dll n'est jamais copié # et l'exe plante au lancement avec « no Qt platform plugin could be # initialized ». On force sa copie via --add-data. WINE_QT_PLUGINS := $(HOME)/.wine/drive_c/users/$(USER)/AppData/Local/Programs/Python/Python311/Lib/site-packages/PyQt6/Qt6/plugins windows: icon @echo "" @echo " 🪟 Compilation Windows (Wine + PyInstaller)…" @echo "" @which wine > /dev/null 2>&1 \ || (echo " ❌ Wine non installé → sudo apt install wine"; exit 1) @test -f "$(WINE_QT_PLUGINS)/platforms/qwindows.dll" \ || (echo " ❌ qwindows.dll introuvable dans $(WINE_QT_PLUGINS)/platforms"; \ echo " → Installez PyQt6 dans Wine avec : make setup-wine"; exit 1) @wine $(WINE_PYTHON) -m PyInstaller $(PI_OPTS) \ --workpath build/windows \ --onefile \ --windowed \ --icon=$(ICON) \ --add-data "$(WINE_QT_PLUGINS)/platforms;PyQt6/Qt6/plugins/platforms" \ --add-data "$(WINE_QT_PLUGINS)/styles;PyQt6/Qt6/plugins/styles" \ --add-data "CHANGELOG.md;." \ $(SRC) \ || (echo ""; \ echo " ❌ Échec : Python Windows introuvable dans Wine."; \ echo " → Installez-le avec : make setup-wine"; \ echo ""; exit 1) @echo "" @echo " ✅ Binaire Windows → $(DIST)/$(APP).exe" @echo "" # ── Service Windows privilégié (wgsecure-helper.exe) ───────────────────────── # Exécutable séparé de wgsecure.exe : installe/désinstalle le tunnel et pose/ # retire les règles NRPT (split-DNS) en LocalSystem, pour éviter une invite # d'authentification à chaque connexion/déconnexion (cf. app/service/). Utilise # pywin32, installé uniquement dans ce venv Wine — jamais dans .venv (Linux) ni # dans les dépendances de wgsecure.exe. Cible explicite : ni `windows` ni # `release` ne la déclenchent implicitement, ce composant restant optionnel. # --hidden-import win32timezone : dépendance dynamique de pywin32 que # l'analyse statique de PyInstaller ne détecte pas seule (service qui # démarre mais plante à l'exécution sans ce forçage — cas connu). helper: @echo "" @echo " 🛡️ Compilation du service (Wine + PyInstaller)…" @echo "" @which wine > /dev/null 2>&1 \ || (echo " ❌ Wine non installé → sudo apt install wine"; exit 1) @test -f "$(WINE_PYTHON)" \ || (echo " ❌ Python Windows introuvable dans Wine → make setup-wine"; exit 1) @wine $(WINE_PYTHON) -m pip install -r requirements-helper.txt --quiet @wine $(WINE_PYTHON) -m PyInstaller \ --noconfirm \ --clean \ --workpath build/helper \ --name wgsecure-helper \ --onefile \ --hidden-import win32timezone \ app/service/helper_service.py @echo "" @echo " ✅ Service → $(DIST)/wgsecure-helper.exe" @echo "" # ── Prérequis de l'installeur Windows (wgsecure.iss) ───────────────────────── # WireGuard n'est pas empaqueté avec WGSecure (c'est wg.exe et le pilote # tunnel qui manquent, pas une simple DLL) : wgsecure.iss l'installe donc en # silencieux à partir de ce binaire, téléchargé ici une bonne fois pour # toutes plutôt que d'imposer une étape manuelle avant chaque compilation. WIREGUARD_URL := https://download.wireguard.com/windows-client/wireguard-installer.exe VCREDIST_URL := https://aka.ms/vs/17/release/vc_redist.x64.exe installer/wireguard-installer.exe: @echo " ⬇️ Téléchargement de l'installeur WireGuard…" @curl -L --fail -o $@ $(WIREGUARD_URL) installer/VC_redist.x64.exe: @echo " ⬇️ Téléchargement du Visual C++ Redistributable…" @curl -L --fail -o $@ $(VCREDIST_URL) installer-deps: installer/wireguard-installer.exe installer/VC_redist.x64.exe @echo " ✅ Prérequis de l'installeur prêts dans installer/" # ── Installation de Python dans Wine ───────────────────────────────────────── WINE_PY_VER := 3.11.9 WINE_PY_URL := https://www.python.org/ftp/python/$(WINE_PY_VER)/python-$(WINE_PY_VER)-amd64.exe WINE_PY_EXE := /tmp/python-$(WINE_PY_VER)-amd64.exe WINE_PYTHON := $(HOME)/.wine/drive_c/users/$(USER)/AppData/Local/Programs/Python/Python311/python.exe setup-wine: @echo " 📦 Installation Python $(WINE_PY_VER) Windows dans Wine…" @which wine > /dev/null 2>&1 \ || (echo " ❌ wine non installé → sudo apt install wine"; exit 1) @test -f $(WINE_PY_EXE) \ || (echo " ⬇️ Téléchargement Python Windows…" \ && curl -L -o $(WINE_PY_EXE) $(WINE_PY_URL)) @echo " 🔧 Installation silencieuse…" wine $(WINE_PY_EXE) /quiet InstallAllUsers=0 PrependPath=1 @echo " 📦 Installation des dépendances…" wine $(WINE_PYTHON) -m pip install --upgrade pip wine $(WINE_PYTHON) -m pip install -r requirements.txt wine $(WINE_PYTHON) -m pip install pyinstaller @echo " ✅ Python Windows prêt dans Wine" # ── Release (Linux renommé avec version) ───────────────────────────────────── # Ne fait pas `clean` : celui-ci vide tout $(DIST), y compris les artefacts # Windows produits juste avant (compilés sous Wine, plusieurs minutes) que # `publish` doit justement joindre à la release. Seul l'artefact Linux est # retiré, ce qui suffit à garantir sa fraîcheur. release: linux @rm -f $(DIST)/$(APP)-*-linux-x86_64 @mv $(DIST)/$(APP) $(DIST)/$(APP)-$(VERSION)-linux-x86_64 @echo " 📦 Release → $(DIST)/$(APP)-$(VERSION)-linux-x86_64" # ── Chaîne Windows complète ─────────────────────────────────────────────────── # Compile l'exe, le service, construit l'installeur, puis renomme le tout avec # la version — dans cet ordre : wgsecure.iss source `dist\wgsecure.exe` et # `dist\wgsecure-helper.exe` sous leurs noms bruts, renommer avant de compiler # l'installeur le ferait échouer (ou, pire, embarquer les binaires de la # version précédente). # # La version vit à deux endroits — ce fichier et wgsecure.iss — et rien ne les # synchronise : la garde ci-dessous refuse de produire un installeur dont le # numéro contredirait celui de la release. # Sortie d'ISCC journalisée plutôt qu'affichée : Wine y mêle ses propres # avertissements (wow64), sans intérêt quand tout se passe bien. En cas # d'échec le journal est recraché, sinon la raison serait perdue. ISCC_LOG := /tmp/wgsecure-iscc.log ISCC := $(HOME)/.wine/drive_c/Program Files (x86)/Inno Setup 6/ISCC.exe ISS_VERSION = $(shell sed -n 's/^#define MyAppVersion "\(.*\)"/\1/p' wgsecure.iss) # Vérifications d'abord, compilations ensuite : placées dans la recette de # windows-release, elles n'auraient averti qu'après plusieurs minutes de # PyInstaller, pour un défaut connu dès la première seconde. check-windows-release: @test "$(ISS_VERSION)" = "$(VERSION)" \ || (echo " ❌ Version incohérente : Makefile $(VERSION), wgsecure.iss $(ISS_VERSION)"; \ echo " → Alignez #define MyAppVersion dans wgsecure.iss"; exit 1) @test -f "$(ISCC)" \ || (echo " ❌ Inno Setup introuvable ($(ISCC))"; \ echo " → Installez Inno Setup 6 dans Wine."; exit 1) windows-release: check-windows-release installer-deps windows helper @echo "" @echo " 📦 Construction de l'installeur Windows…" @wine "$(ISCC)" wgsecure.iss > $(ISCC_LOG) 2>&1 \ || (echo " ❌ Échec de la compilation Inno Setup :"; \ tail -20 $(ISCC_LOG); exit 1) @mv $(DIST)/$(APP).exe $(DIST)/$(APP)-$(VERSION)-windows-x86_64.exe @mv $(DIST)/$(APP)-helper.exe $(DIST)/$(APP)-helper-$(VERSION).exe @echo "" @echo " ✅ Windows → $(DIST)/$(APP)-$(VERSION)-windows-x86_64.exe" @echo " ✅ Service → $(DIST)/$(APP)-helper-$(VERSION).exe" @echo " ✅ Setup → $(DIST)/$(APP)-$(VERSION)-setup.exe" @echo "" # ── Publication de la release sur Gitea ─────────────────────────────────────── # Rebuild l'artefact Linux (fraîcheur garantie avec VERSION), joint aussi les # artefacts Windows présents dans dist/ s'ils ont déjà été produits pour cette # version (binaire + installeur), puis tague et publie sur Gitea. Réutilise # gitea_release.sh (BorisSkill) plutôt que de réimplémenter la création de # tag/release/upload d'assets déjà gérée par ce script (jeton, détection du # remote, notes issues du CHANGELOG). GITEA_RELEASE_SH := $(HOME)/.claude/skills/BorisSkill/scripts/gitea_release.sh publish: release @test -f "$(GITEA_RELEASE_SH)" \ || (echo " ❌ gitea_release.sh introuvable ($(GITEA_RELEASE_SH))"; \ echo " → Nécessite BorisSkill (Claude Code)."; exit 1) @ASSETS="--asset $(DIST)/$(APP)-$(VERSION)-linux-x86_64"; \ for f in "$(DIST)/$(APP)-$(VERSION)-windows-x86_64.exe" "$(DIST)/$(APP)-$(VERSION)-setup.exe"; do \ [ -f "$$f" ] && ASSETS="$$ASSETS --asset $$f"; \ done; \ bash "$(GITEA_RELEASE_SH)" --version $(VERSION) $$ASSETS # ── Publication des artefacts vers le serveur de mise à jour ───────────────── # Le client va chercher ses mises à jour sur le serveur WGSecure, pas sur # Gitea : le dépôt est privé, et un jeton embarqué dans une application # distribuée est un jeton donné à tous ses utilisateurs. Cette cible dépose # les binaires et le manifeste que la route /api/version publie. # # Le condensat est calculé ici, une fois, et non par le serveur à chaque # appel : un SHA-256 sur 125 Mo à chaque interrogation bloquerait l'API. UPDATE_HOST := root@192.168.1.206 UPDATE_DIR := /opt/WGSecure-Serveur/artifacts # ANNOUNCE permet d'annoncer les artefacts sous un autre numéro que celui # qu'ils portent : `make publish-updates ANNOUNCE=9.9.9` fait croire au parc # qu'une version existe, sans avoir à en compiler une seconde. Sert à exercer # la chaîne de mise à jour — détection, téléchargement, empreinte, bascule — # avec les binaires déjà construits. ANNOUNCE ?= $(VERSION) publish-updates: @test -f "$(DIST)/$(APP)-$(VERSION)-linux-x86_64" \ || (echo " ❌ Artefact Linux absent — lancez d'abord : make release"; exit 1) @echo "" @echo " 📤 Publication des artefacts $(VERSION) vers $(UPDATE_HOST)…" @FILES="$(DIST)/$(APP)-$(VERSION)-linux-x86_64"; \ [ -f "$(DIST)/$(APP)-$(VERSION)-setup.exe" ] \ && FILES="$$FILES $(DIST)/$(APP)-$(VERSION)-setup.exe" \ || echo " ⚠️ Installeur Windows absent : seuls les postes Linux seront servis"; \ scp -q $$FILES $(UPDATE_HOST):$(UPDATE_DIR)/ @$(PYTHON) -c "import json, hashlib, os, sys; \ v='$(VERSION)'; a='$(ANNOUNCE)'; d='$(DIST)'; app='$(APP)'; \ spec=[('linux-x86_64', f'{app}-{v}-linux-x86_64'), ('windows-setup', f'{app}-{v}-setup.exe')]; \ arts=[{'key': k, 'filename': n, 'size': os.path.getsize(f'{d}/{n}'), \ 'sha256': hashlib.sha256(open(f'{d}/{n}','rb').read()).hexdigest()} \ for k, n in spec if os.path.isfile(f'{d}/{n}')]; \ notes=open('CHANGELOG.md', encoding='utf-8').read().split('## [')[1]; \ json.dump({'version': a, 'notes': '## [' + notes.split('\n---')[0].strip(), \ 'artifacts': arts}, open('/tmp/wgs-manifest.json','w', encoding='utf-8'), \ ensure_ascii=False, indent=2); \ print(f' 🔑 {len(arts)} artefact(s) de {v}, annonces comme {a}')" @scp -q /tmp/wgs-manifest.json $(UPDATE_HOST):$(UPDATE_DIR)/manifest.json @ssh $(UPDATE_HOST) "chown -R wgsecure:wgsecure $(UPDATE_DIR)" @rm -f /tmp/wgs-manifest.json @echo " ✅ Mise à jour $(ANNOUNCE) proposée aux clients" @echo "" # ── Version courante ────────────────────────────────────────────────────────── version: @echo "$(VERSION)" # ── Dernière entrée du changelog ───────────────────────────────────────────── changelog: @awk '/^## \[/{if(n++)exit} n' CHANGELOG.md # ── Installation GNOME (utilisateur local, sans sudo) ──────────────────────── INSTALL_BIN := $(HOME)/.local/bin INSTALL_ICON := $(HOME)/.local/share/icons/hicolor/256x256/apps INSTALL_APPS := $(HOME)/.local/share/applications install-gnome: linux @echo "" @echo " 🐧 Installation de WGSecure pour GNOME…" @mkdir -p $(INSTALL_BIN) $(INSTALL_ICON) $(INSTALL_APPS) @cp $(DIST)/$(APP) $(INSTALL_BIN)/$(APP) @chmod +x $(INSTALL_BIN)/$(APP) @echo " ✅ Binaire → $(INSTALL_BIN)/$(APP)" @cp $(ICON) $(INSTALL_ICON)/$(APP).png @echo " ✅ Icône → $(INSTALL_ICON)/$(APP).png" @printf '[Desktop Entry]\nType=Application\nName=WGSecure\nComment=WireGuard GUI avec MFA TOTP\nExec=%s\nIcon=%s\nCategories=Network;Security;\nStartupWMClass=wgsecure\nTerminal=false\n' \ "$(INSTALL_BIN)/$(APP)" "$(APP)" \ > $(INSTALL_APPS)/$(APP).desktop @echo " ✅ Lanceur → $(INSTALL_APPS)/$(APP).desktop" @gtk-update-icon-cache -f -t $(HOME)/.local/share/icons/hicolor 2>/dev/null || true @update-desktop-database $(INSTALL_APPS) 2>/dev/null || true @echo "" @echo " 🎉 Installation terminée. WGSecure est disponible dans le menu GNOME." @echo "" uninstall-gnome: @echo " 🗑️ Désinstallation de WGSecure…" @rm -f $(INSTALL_BIN)/$(APP) @rm -f $(INSTALL_ICON)/$(APP).png @rm -f $(INSTALL_APPS)/$(APP).desktop @gtk-update-icon-cache -f -t $(HOME)/.local/share/icons/hicolor 2>/dev/null || true @update-desktop-database $(INSTALL_APPS) 2>/dev/null || true @echo " ✅ WGSecure désinstallé" # ── Dépendances Python ──────────────────────────────────────────────────────── install: venv $(PIP) install -r requirements.txt $(PIP) install pyinstaller @echo " ✅ Dépendances installées dans $(VENV)" # ── Diagnostic des droits ──────────────────────────────────────────────────── check-privileges: @echo "" @echo " 🔐 Droits requis par WGSecure :" @$(PYTHON) -c "\ import sys; sys.path.insert(0, '.'); \ from app.utils.platform_utils import privilege_report; \ rows = privilege_report(); \ print(' (exécution en root : aucun droit à configurer)') if not rows else None; \ [print(' ' + ('✅' if ok else '⚠️ ') + ' ' + name.ljust(12) + ('sans mot de passe' if ok else 'dialogue polkit à chaque appel').ljust(32) + '— ' + role) for name, ok, role in rows]; \ print() if not rows else print(chr(10) + ' ⚠️ → lancez make setup-sudoers pour supprimer les dialogues.' if any(not ok for _, ok, _ in rows) else chr(10) + ' ✅ Tout est configuré.')" @echo "" # ── Droits sudo wg-quick sans dialogue de mot de passe ─────────────────────── # `pkexec` (utilisé jusqu'ici) exige un agent d'authentification polkit # graphique : absent d'un terminal simple (SSH, tmux, session sans agent), il # reste bloqué en attente d'une réponse qui n'arrive jamais. `sudo` n'a pas ce # problème : lancé depuis un terminal, il y invite directement le mot de # passe, avec ou sans polkit. La cible est idempotente : elle recalcule les # règles à chaque lancement et n'invite pour un mot de passe que si le # contenu déjà installé diffère (donc jamais lors d'une relance sans # changement). setup-sudoers: @echo "" @echo " 🔧 Configuration sudoers (wg-quick + nettoyage réseau, sans mot de passe)…" @$(PYTHON) -c "\ import os, subprocess, tempfile, shutil; \ user = os.environ.get('SUDO_USER') or os.environ.get('USER') or os.path.basename(os.path.expanduser('~')); \ w = lambda n: shutil.which(n) or ''; \ cmds = [c for c in ( \ w('wg-quick'), \ w('wg'), \ w('ip') + ' link delete dev *' if w('ip') else '', \ w('resolvconf') + ' -d *' if w('resolvconf') else '', \ w('resolvectl') + ' revert *' if w('resolvectl') else '', \ w('resolvectl') + ' dns *' if w('resolvectl') else '', \ w('resolvectl') + ' domain *' if w('resolvectl') else '', \ w('install') + ' -D -m 600 -o root -g root * /etc/wireguard/*' if w('install') else '', \ ) if c]; \ rules = ''.join(user + ' ALL=(ALL) NOPASSWD: ' + c + chr(10) for c in cmds); \ dest = '/etc/sudoers.d/wgsecure'; \ current = subprocess.run(['sudo', '-n', 'cat', dest], capture_output=True, text=True); \ already_ok = current.returncode == 0 and current.stdout == rules; \ print(' ✅ Déjà configuré pour ' + user + ' — rien à faire.') if already_ok else None; \ import sys; sys.exit(0) if already_ok else None; \ fd, tmp = tempfile.mkstemp(prefix='wgsecure_sudoers_'); \ os.write(fd, rules.encode()); os.close(fd); os.chmod(tmp, 0o600); \ chk = subprocess.run(['visudo', '-cqf', tmp], capture_output=True); \ ok = chk.returncode == 0; \ r = subprocess.run(['sudo', 'install', '-m', '440', '-o', 'root', '-g', 'root', tmp, dest]) if ok else None; \ os.unlink(tmp); \ print(' ❌ Règles sudoers invalides, rien installé : ' + chk.stderr.decode().strip()) if not ok else \ print(' ✅ ' + dest + ' configuré pour ' + user + ' — plus de dialogue admin.' if r.returncode == 0 else ' ❌ Échec.')" @echo "" # ── Réinitialisation du mot de passe administrateur ────────────────────────── reset-password: @echo "" @echo " ⚠️ Réinitialisation du mot de passe administrateur WGSecure…" @$(PYTHON) -c "\ import sys; sys.path.insert(0,'.');\ from app.core.config import Config;\ cfg = Config();\ cfg.set_admin_password('');\ print(' ✅ Mot de passe supprimé — accès admin sans restriction au prochain démarrage.')" @echo "" # ── Lancer l'application ───────────────────────────────────────────────────── run: venv DISPLAY=:0 $(PYTHON) $(SRC) run-admin: venv DISPLAY=:0 $(PYTHON) $(SRC) --admin # ── Nettoyage ──────────────────────────────────────────────────────────────── clean: @echo " 🧹 Nettoyage…" @rm -rf $(BUILD) $(DIST) *.spec @find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null; true @find . -name "*.pyc" -delete 2>/dev/null; true @echo " ✅ Artefacts supprimés" clean-all: clean @rm -rf $(VENV) @echo " ✅ Venv supprimé" # ── Aide ───────────────────────────────────────────────────────────────────── help: @echo "" @echo " ╔══════════════════════════════════════════════╗" @echo " ║ 🛡️ WGSecure v$(VERSION) — Makefile ║" @echo " ╠══════════════════════════════════════════════╣" @echo " ║ make install Installe les dépendances ║" @echo " ║ make linux Binaire Linux (onefile) ║" @echo " ║ make windows Binaire Windows (Wine) ║" @echo " ║ make helper Service Windows (Wine) ║" @echo " ║ make installer-deps Prérequis wgsecure.iss ║" @echo " ║ make release Linux + nommage release ║" @echo " ║ make windows-release Windows + exe + setup ║" @echo " ║ make publish Tague + publie sur Gitea ║" @echo " ║ make version Affiche la version ║" @echo " ║ make changelog Dernière entrée changelog║" @echo " ║ make setup-wine Python Windows dans Wine ║" @echo " ║ make install-gnome Installe dans GNOME ║" @echo " ║ make uninstall-gnome Désinstalle de GNOME ║" @echo " ║ make setup-sudoers wg-quick sans sudo ║" @echo " ║ make reset-password Supprime le MDP admin ║" @echo " ║ make run Lance l'application ║" @echo " ║ make run-admin Lance en mode admin ║" @echo " ║ make clean Supprime les artefacts ║" @echo " ╚══════════════════════════════════════════════╝" @echo ""