Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04a7b5dc64 | ||
|
|
3d4d6de0f8 | ||
|
|
bc080fd76a | ||
|
|
59b6c75942 | ||
|
|
599192fd67 | ||
|
|
f73c32af2a |
@@ -5,3 +5,5 @@ dist/
|
||||
*.spec
|
||||
.build-venv-linux/
|
||||
.build-venv-windows/
|
||||
.build-wine/
|
||||
.build-wine-cache/
|
||||
|
||||
@@ -1,5 +1,41 @@
|
||||
# Changelog
|
||||
|
||||
## [0.1.4] - 2026-09-08
|
||||
|
||||
### Fixed
|
||||
- La jointure de domaine sous Windows échouait sans aucun message, avec un code
|
||||
retour 0. Le script PowerShell était transmis sur l'entrée standard de
|
||||
`powershell -Command -`, qui analyse stdin ligne par ligne : le bloc
|
||||
`try`/`catch` final, dépourvu de saut de ligne terminal, restait en attente
|
||||
dans le parseur et n'était jamais exécuté. PowerShell rendait donc la main
|
||||
sans avoir tenté la jointure. Le script est désormais transmis d'un bloc via
|
||||
`-EncodedCommand`.
|
||||
- En l'absence de marqueur de succès comme d'échec, le message d'erreur se
|
||||
réduisait à « code retour 0 », impossible à diagnostiquer. La sortie brute de
|
||||
PowerShell est maintenant reportée à l'utilisateur (le mot de passe ne
|
||||
transitant que par l'environnement, il n'y apparaît jamais).
|
||||
|
||||
## [0.1.3] - 2026-09-08
|
||||
|
||||
### Fixed
|
||||
- Le binaire Windows produit sous Wine ne démarrait pas (« could not initialize
|
||||
Qt platform plugin "windows" ») : le hook PySide6 de PyInstaller importe
|
||||
QtCore pour localiser les plugins Qt, or Qt 6.10+ lie `Qt6Core.dll` à l'ICU
|
||||
de Windows (`icuuc.dll`), absent de Wine. L'import échouait et aucun plugin
|
||||
n'était embarqué. La compilation Windows épingle désormais `PySide6<6.10` et
|
||||
vérifie explicitement l'import de PySide6 puis la présence de `qwindows.dll`.
|
||||
|
||||
### Added
|
||||
- `Makefile` avec aide auto-documentée par catégories (`make help`).
|
||||
- `packaging/build_windows_wine.sh` : production du `.exe` Windows depuis Linux.
|
||||
- `README.md`.
|
||||
- `make publish` : pousse branche et tags vers tous les dépôts Gitea configurés.
|
||||
|
||||
## [0.1.2] - 2026-09-04
|
||||
|
||||
### Changed
|
||||
- Recompilation du binaire Linux (aucun changement fonctionnel depuis la 0.1.1).
|
||||
|
||||
## [0.1.1] - 2026-09-04
|
||||
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# JoinDomainGUI — diagnostic reseau AD + jointure de domaine
|
||||
# `make` seul (ou `make help`) affiche les cibles disponibles.
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
VENV := .build-venv-linux
|
||||
PY := $(VENV)/bin/python
|
||||
PIP := $(VENV)/bin/pip
|
||||
VERSION := $(shell sed -n 's/^__version__ = "\(.*\)"/\1/p' domainjoin/__init__.py)
|
||||
DOMAIN ?= h3adm.lan
|
||||
BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
|
||||
REMOTES := $(shell git remote)
|
||||
|
||||
##@ 📦 Installation
|
||||
|
||||
$(VENV)/bin/python:
|
||||
python3 -m venv --system-site-packages $(VENV)
|
||||
$(PIP) install --upgrade pip -q
|
||||
$(PIP) install -q -r requirements.txt
|
||||
|
||||
.PHONY: install
|
||||
install: $(VENV)/bin/python ## Cree l'environnement virtuel et installe les dependances
|
||||
@echo "✅ Environnement pret : $(VENV)"
|
||||
|
||||
##@ 🚀 Utilisation
|
||||
|
||||
.PHONY: run
|
||||
run: install ## Lance l'interface graphique
|
||||
$(PY) main.py
|
||||
|
||||
.PHONY: diagnose
|
||||
diagnose: install ## Lance le diagnostic reseau en CLI (make diagnose DOMAIN=exemple.lan)
|
||||
$(PY) main.py diagnose --domain $(DOMAIN)
|
||||
|
||||
##@ 🏗️ Compilation
|
||||
|
||||
.PHONY: build
|
||||
build: build-linux build-windows ## Compile les binaires Linux et Windows
|
||||
|
||||
.PHONY: build-linux
|
||||
build-linux: ## Compile le binaire Linux autonome (dist/JoinDomainGUI)
|
||||
./packaging/build_linux.sh
|
||||
|
||||
.PHONY: build-windows
|
||||
build-windows: ## Compile le .exe Windows depuis Linux via Wine (dist/windows/JoinDomainGUI.exe)
|
||||
./packaging/build_windows_wine.sh
|
||||
|
||||
##@ 🚢 Publication
|
||||
|
||||
.PHONY: remotes
|
||||
remotes: ## Liste les depots distants configures
|
||||
@git remote -v | awk '$$3 == "(push)" { printf " %-10s %s\n", $$1, $$2 }'
|
||||
|
||||
.PHONY: publish
|
||||
publish: ## Pousse la branche courante et les tags vers tous les depots (Gitea)
|
||||
@git diff --quiet HEAD || { echo "❌ Modifications non commitees — commit avant de publier."; exit 1; }
|
||||
@for remote in $(REMOTES); do \
|
||||
echo "🚀 $$remote ← $(BRANCH)"; \
|
||||
git push $$remote $(BRANCH) || exit 1; \
|
||||
git push $$remote --tags || exit 1; \
|
||||
done
|
||||
@echo "✅ Publie sur : $(REMOTES)"
|
||||
|
||||
##@ 🧹 Nettoyage
|
||||
|
||||
.PHONY: clean
|
||||
clean: ## Supprime les artefacts de compilation et les caches Python
|
||||
rm -rf build dist
|
||||
find . -path ./$(VENV) -prune -o -name '__pycache__' -type d -print0 | xargs -0 rm -rf
|
||||
|
||||
.PHONY: distclean
|
||||
distclean: clean ## Nettoyage complet : + environnements virtuels, prefixe Wine et telechargements
|
||||
rm -rf $(VENV) .build-venv-windows .build-wine .build-wine-cache
|
||||
|
||||
##@ ℹ️ Aide
|
||||
|
||||
.PHONY: version
|
||||
version: ## Affiche la version courante
|
||||
@echo "JoinDomainGUI $(VERSION)"
|
||||
|
||||
.PHONY: help
|
||||
help: ## Affiche cette aide
|
||||
@echo "JoinDomainGUI $(VERSION)"
|
||||
@awk 'BEGIN {FS = ":.*?## "} \
|
||||
/^[a-zA-Z0-9_.-]+:.*?##/ { printf " \033[36m%-16s\033[0m %s\n", $$1, $$2 } \
|
||||
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
|
||||
@echo ""
|
||||
@@ -0,0 +1,121 @@
|
||||
# JoinDomainGUI
|
||||
|
||||
Outil de **diagnostic réseau Active Directory** et de **jointure de domaine**,
|
||||
avec interface graphique (PySide6) et interface en ligne de commande.
|
||||
Fonctionne sous **Linux** (`realmd`) et sous **Windows** (PowerShell `Add-Computer`),
|
||||
le backend étant choisi automatiquement selon la plateforme.
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
- **Diagnostic réseau AD** : résolution DNS du domaine, enregistrements SRV
|
||||
(LDAP / Kerberos), ping des contrôleurs de domaine, test des ports AD
|
||||
(53, 88, 389, 445, 464, 636, 3268), synchronisation NTP et présence des
|
||||
outils système requis.
|
||||
- **Jointure de domaine** avec unité d'organisation et nom de machine
|
||||
optionnels, et détection du besoin de redémarrage.
|
||||
- **Installation des prérequis Linux** manquants en un clic ou une commande
|
||||
(`realmd`, `sssd`, `adcli`, `samba-common-bin`, `packagekit` via `apt-get`).
|
||||
- **Interface graphique** à trois onglets : Diagnostic réseau, Rejoindre le
|
||||
domaine, À propos.
|
||||
- **Interface en ligne de commande** utilisable sans environnement graphique
|
||||
(SSH, déploiement scripté) : la CLI n'importe jamais PySide6.
|
||||
- **Le mot de passe ne transite jamais par la ligne de commande** : il est
|
||||
transmis au sous-processus via stdin ou son environnement, et n'apparaît
|
||||
donc pas dans `ps` ni dans l'historique du shell.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
make install # crée l'environnement virtuel et installe les dépendances
|
||||
```
|
||||
|
||||
Dépendances : Python 3.11+, `PySide6`, `dnspython`, `ntplib`.
|
||||
|
||||
## Utilisation
|
||||
|
||||
### Interface graphique
|
||||
|
||||
```bash
|
||||
make run # depuis les sources
|
||||
sudo ./dist/JoinDomainGUI # binaire autonome Linux
|
||||
```
|
||||
|
||||
La jointure de domaine nécessite les droits root / administrateur ; le
|
||||
diagnostic, non.
|
||||
|
||||
### Ligne de commande
|
||||
|
||||
```bash
|
||||
# Diagnostic réseau
|
||||
python main.py diagnose --domain exemple.lan
|
||||
make diagnose DOMAIN=exemple.lan
|
||||
|
||||
# Jointure de domaine (root / administrateur requis)
|
||||
echo 'motdepasse' | sudo python main.py join \
|
||||
--domain exemple.lan --username administrateur --password-stdin
|
||||
|
||||
# Installation des prérequis système (Linux / apt uniquement)
|
||||
sudo python main.py install-prereqs
|
||||
```
|
||||
|
||||
Sans mot de passe fourni, il est demandé de façon interactive et masquée.
|
||||
`--password` existe mais est déconseillé (visible dans `ps` et l'historique).
|
||||
|
||||
Code de sortie : `0` si tout va bien, `1` si un test échoue ou si la jointure
|
||||
échoue, `2` en cas de droits insuffisants ou de backend indisponible.
|
||||
|
||||
## Compilation des binaires
|
||||
|
||||
```bash
|
||||
make build-linux # -> dist/JoinDomainGUI (ELF autonome)
|
||||
make build-windows # -> dist/windows/JoinDomainGUI.exe (PE64 autonome)
|
||||
make build # les deux
|
||||
```
|
||||
|
||||
PyInstaller ne fait pas de cross-compilation. Le binaire Windows est donc
|
||||
produit depuis Linux en exécutant un vrai PyInstaller Windows dans un
|
||||
préfixe **Wine** dédié (`packaging/build_windows_wine.sh`) : le `.exe` obtenu
|
||||
est un binaire Windows natif. Le premier appel télécharge un Python Windows
|
||||
embarqué et les dépendances (plusieurs minutes) ; les suivants sont
|
||||
incrémentaux.
|
||||
|
||||
Sur une machine Windows, `packaging/build_windows.bat` fait le même travail
|
||||
nativement.
|
||||
|
||||
La build Wine épingle **Qt 6.9** (`PySide6<6.10`) : à partir de Qt 6.10,
|
||||
`Qt6Core.dll` dépend de l'ICU fourni par Windows (`icuuc.dll` dans `System32`),
|
||||
que Wine n'implémente pas. PySide6 y devient inimportable — et le hook PySide6
|
||||
de PyInstaller, qui importe QtCore pour localiser les plugins Qt, échoue alors
|
||||
*silencieusement* et produit un `.exe` sans plugin de plateforme, qui refuse de
|
||||
démarrer sous Windows (« could not initialize Qt platform plugin windows »). Le
|
||||
script vérifie donc, avant compilation, que PySide6 s'importe, et après
|
||||
compilation, que `qwindows.dll` est bien embarqué.
|
||||
|
||||
## Publication
|
||||
|
||||
```bash
|
||||
make publish # pousse la branche courante et les tags vers tous les dépôts
|
||||
```
|
||||
|
||||
Le projet est publié sur deux instances Gitea :
|
||||
[git.tuxtech.fr](https://git.tuxtech.fr/tuxgyver/JoinDomainGUI) (`origin`) et
|
||||
[git.h3campus.fr](https://git.h3campus.fr/Johnny/JoinDomainGUI) (`h3campus`).
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
main.py Point d'entrée : GUI sans argument, CLI sinon
|
||||
domainjoin/
|
||||
├── cli.py Interface en ligne de commande
|
||||
├── gui.py Interface PySide6
|
||||
├── diagnostics.py Moteur de diagnostic réseau
|
||||
├── worker.py Exécution en arrière-plan pour la GUI
|
||||
├── privileges.py Détection des droits root/administrateur
|
||||
└── backend/
|
||||
├── base.py Interface commune de jointure
|
||||
├── linux_realmd.py Backend Linux (realmd / adcli)
|
||||
└── windows_addcomputer.py Backend Windows (Add-Computer)
|
||||
packaging/ Scripts de compilation PyInstaller
|
||||
```
|
||||
|
||||
Voir [CHANGELOG.md](CHANGELOG.md) pour l'historique des versions.
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.1.1"
|
||||
__version__ = "0.1.4"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -19,6 +20,20 @@ def _ps_quote(value: str) -> str:
|
||||
return value.replace("'", "''")
|
||||
|
||||
|
||||
def _encode_command(script: str) -> str:
|
||||
"""Encode un script pour `powershell -EncodedCommand` (base64 d'UTF-16LE).
|
||||
|
||||
Le script est ainsi transmis comme un bloc unique et atomique. L'alternative
|
||||
`-Command -` fait analyser l'entrée standard ligne par ligne : la dernière
|
||||
instruction d'un bloc `try`/`catch` restait en attente dans le parseur et
|
||||
n'était jamais exécutée à la fermeture de stdin — PowerShell rendait alors
|
||||
la main avec le code 0 sans avoir rien fait, et la jointure echouait sans
|
||||
le moindre message. Le mot de passe reste hors du script (variable
|
||||
d'environnement) : la ligne de commande ne l'expose pas.
|
||||
"""
|
||||
return base64.b64encode(script.encode("utf-16-le")).decode("ascii")
|
||||
|
||||
|
||||
class WindowsAddComputerBackend(DomainJoinBackend):
|
||||
def is_available(self) -> tuple[bool, str]:
|
||||
if shutil.which("powershell") is None and shutil.which("powershell.exe") is None:
|
||||
@@ -76,8 +91,16 @@ class WindowsAddComputerBackend(DomainJoinBackend):
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
["powershell", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "-"],
|
||||
stdin=subprocess.PIPE,
|
||||
[
|
||||
"powershell",
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-EncodedCommand",
|
||||
_encode_command(script),
|
||||
],
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
@@ -90,12 +113,6 @@ class WindowsAddComputerBackend(DomainJoinBackend):
|
||||
finally:
|
||||
env[_PASSWORD_ENV_VAR] = ""
|
||||
|
||||
try:
|
||||
process.stdin.write(script)
|
||||
process.stdin.close()
|
||||
except BrokenPipeError:
|
||||
pass
|
||||
|
||||
output_lines: list[str] = []
|
||||
if process.stdout:
|
||||
for line in process.stdout:
|
||||
@@ -114,5 +131,15 @@ class WindowsAddComputerBackend(DomainJoinBackend):
|
||||
)
|
||||
|
||||
failure_detail = next((l for l in output_lines if l.strip().startswith(_FAILURE_PREFIX)), None)
|
||||
message = failure_detail or f"Échec de la jointure au domaine (code retour {returncode})."
|
||||
if failure_detail:
|
||||
message = failure_detail
|
||||
else:
|
||||
# Sans ce report de la sortie brute, un echec inattendu se resumait a
|
||||
# « code retour 0 », indiagnosticable. Le mot de passe n'apparait
|
||||
# jamais dans cette sortie : il ne transite que par l'environnement.
|
||||
detail = full_output.strip() or "aucune sortie produite par PowerShell"
|
||||
message = (
|
||||
f"Échec de la jointure au domaine (code retour {returncode}).\n"
|
||||
f"Sortie de PowerShell : {detail}"
|
||||
)
|
||||
return JoinResult(success=False, message=message)
|
||||
|
||||
Executable
+109
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fabrique le binaire Windows autonome (.exe) de JoinDomainGUI DEPUIS LINUX,
|
||||
# en pilotant un Python Windows embarque dans un prefixe Wine dedie.
|
||||
#
|
||||
# PyInstaller ne cross-compile pas : on execute donc un vrai PyInstaller Windows
|
||||
# sous Wine. Le .exe produit est un binaire Windows natif.
|
||||
#
|
||||
# Prerequis : wine (>= 8), curl, unzip.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
PYTHON_VERSION="${PYTHON_VERSION:-3.12.10}"
|
||||
# Depuis Qt 6.10, Qt6Core.dll est lie a l'ICU fourni par Windows (icuuc.dll dans
|
||||
# System32, present depuis Win10 1703). Wine ne fournit pas cette DLL : PySide6
|
||||
# y devient inimportable, et le hook PySide6 de PyInstaller -- qui importe QtCore
|
||||
# pour localiser les plugins Qt -- echoue alors SILENCIEUSEMENT et produit un
|
||||
# .exe depourvu de plugin de plateforme ("could not initialize Qt platform
|
||||
# plugin windows" au lancement). On contraint donc cette build a Qt 6.9.
|
||||
PYSIDE6_CONSTRAINT="${PYSIDE6_CONSTRAINT:-PySide6<6.10}"
|
||||
WINEPREFIX="${WINEPREFIX:-$PWD/.build-wine}"
|
||||
export WINEPREFIX
|
||||
export WINEARCH=win64
|
||||
export WINEDEBUG="${WINEDEBUG:--all}"
|
||||
|
||||
PY_DIR="$WINEPREFIX/drive_c/python"
|
||||
WPY="c:\\python\\python.exe"
|
||||
CACHE=".build-wine-cache"
|
||||
|
||||
command -v wine >/dev/null || { echo "wine est requis (apt install wine)" >&2; exit 1; }
|
||||
|
||||
mkdir -p "$CACHE"
|
||||
|
||||
if [ ! -d "$WINEPREFIX" ]; then
|
||||
echo ">> Initialisation du prefixe Wine ($WINEPREFIX)"
|
||||
wineboot --init >/dev/null 2>&1
|
||||
wineserver -w
|
||||
fi
|
||||
|
||||
if [ ! -f "$PY_DIR/python.exe" ]; then
|
||||
ZIP="$CACHE/python-$PYTHON_VERSION-embed-amd64.zip"
|
||||
[ -f "$ZIP" ] || curl -fL --retry 3 -o "$ZIP" \
|
||||
"https://www.python.org/ftp/python/$PYTHON_VERSION/python-$PYTHON_VERSION-embed-amd64.zip"
|
||||
echo ">> Installation de Python $PYTHON_VERSION (Windows) dans le prefixe"
|
||||
rm -rf "$PY_DIR"
|
||||
mkdir -p "$PY_DIR"
|
||||
unzip -q "$ZIP" -d "$PY_DIR"
|
||||
# La distribution "embeddable" desactive site-packages : on le reactive
|
||||
# pour que pip et les paquets installes soient importables.
|
||||
sed -i 's/^#import site/import site/' "$PY_DIR"/python*._pth
|
||||
fi
|
||||
|
||||
if [ ! -f "$PY_DIR/Scripts/pip.exe" ]; then
|
||||
GETPIP="$CACHE/get-pip.py"
|
||||
[ -f "$GETPIP" ] || curl -fL --retry 3 -o "$GETPIP" https://bootstrap.pypa.io/get-pip.py
|
||||
echo ">> Installation de pip"
|
||||
cp "$GETPIP" "$PY_DIR/get-pip.py"
|
||||
wine "$WPY" c:\\python\\get-pip.py --no-warn-script-location
|
||||
fi
|
||||
|
||||
# pip sous Wine est lent (plusieurs minutes pour le wheel PySide6) : on ne
|
||||
# reinstalle que si requirements.txt ou la contrainte ont change (FORCE_DEPS=1
|
||||
# pour forcer).
|
||||
DEPS_STAMP="$PY_DIR/.deps-stamp"
|
||||
WANT_HASH="$(cat requirements.txt <(echo "$PYSIDE6_CONSTRAINT") | sha256sum | cut -d' ' -f1)"
|
||||
if [ "${FORCE_DEPS:-0}" = "1" ] || [ "$(cat "$DEPS_STAMP" 2>/dev/null)" != "$WANT_HASH" ]; then
|
||||
echo ">> Installation des dependances Windows (peut prendre plusieurs minutes)"
|
||||
printf '%s\n' "$PYSIDE6_CONSTRAINT" > "$PY_DIR/constraints.txt"
|
||||
# Desinstallation prealable : pip ne retrograde pas toujours un paquet deja
|
||||
# installe qui viole une contrainte nouvellement ajoutee.
|
||||
wine "$WPY" -m pip uninstall -q -y PySide6 PySide6-Essentials PySide6-Addons shiboken6 >/dev/null 2>&1 || true
|
||||
wine "$WPY" -m pip install -q --no-warn-script-location \
|
||||
-c c:\\python\\constraints.txt -r requirements.txt pyinstaller
|
||||
echo "$WANT_HASH" > "$DEPS_STAMP"
|
||||
else
|
||||
echo ">> Dependances Windows a jour (FORCE_DEPS=1 pour les reinstaller)"
|
||||
fi
|
||||
|
||||
# Garde-fou : si PySide6 ne s'importe pas ici, le hook PyInstaller ne collectera
|
||||
# aucun plugin Qt et produira un .exe casse sans le signaler. Echouer tot.
|
||||
echo ">> Verification de PySide6 sous Wine"
|
||||
if ! wine "$WPY" -c "import PySide6.QtWidgets" 2>&1; then
|
||||
echo "ERREUR : PySide6 n'est pas importable sous Wine — le .exe produit serait" >&2
|
||||
echo " depourvu de plugins Qt. Verifiez PYSIDE6_CONSTRAINT." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ">> Compilation du .exe"
|
||||
wine "$WPY" -m PyInstaller \
|
||||
--noconfirm \
|
||||
--onefile \
|
||||
--windowed \
|
||||
--name JoinDomainGUI \
|
||||
--distpath dist/windows \
|
||||
--workpath build/windows \
|
||||
--specpath build/windows \
|
||||
main.py
|
||||
|
||||
wineserver -w
|
||||
|
||||
# Garde-fou : sans qwindows.dll, l'application refuse de demarrer sous Windows.
|
||||
if ! grep -qi 'qwindows.dll' build/windows/JoinDomainGUI/PKG-00.toc; then
|
||||
echo "ERREUR : le plugin de plateforme qwindows.dll est absent du binaire." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Binaire produit : dist/windows/JoinDomainGUI.exe"
|
||||
echo "Lancement : clic droit -> Executer en tant qu'administrateur"
|
||||
Reference in New Issue
Block a user