"""Assistant de génération des supports de la formation (Windows, macOS, Linux). Lancé par Generer-documents.bat / Generer-documents.command / generer-documents.sh. N'utilise que la bibliothèque standard de Python. Étapes : vérifier (ou installer) Quarto, préparer l'environnement Python des schémas, demander quoi générer, lancer Quarto, ouvrir le dossier des documents produits. """ import hashlib import json import os import platform import re import shutil import subprocess import sys import tempfile import time import urllib.request import webbrowser from pathlib import Path RACINE = Path(__file__).resolve().parent.parent MODULES = RACINE / "modules" SORTIE = RACINE / "_output" VENV = RACINE / ".venv" EXIGENCES = RACINE / "schemas" / "requirements.txt" PAGE_QUARTO = "https://quarto.org/docs/get-started/" SYSTEME = platform.system() # "Windows", "Darwin" (macOS) ou "Linux" FORMATS = [ # (libellé, option --to de Quarto, extension produite) ("PDF", "typst", ".pdf"), ("LibreOffice (.odt)", "odt", ".odt"), ("Site web (HTML)", "html", ".html"), ] # --- Dialogue ---------------------------------------------------------------- def titre(texte): print("\n" + "=" * 64 + f"\n {texte}\n" + "=" * 64) def oui_non(question, defaut=True): suffixe = " [O/n] " if defaut else " [o/N] " while True: reponse = input(question + suffixe).strip().lower() if not reponse: return defaut if reponse in ("o", "oui", "y", "yes"): return True if reponse in ("n", "non", "no"): return False print(" Répondez par o (oui) ou n (non).") def choisir(question, options, defaut=1): """Affiche une liste numérotée et renvoie l'index (0-based) choisi.""" print(f"\n{question}") for i, option in enumerate(options, 1): print(f" {i:>2}. {option}" + (" (par défaut)" if i == defaut else "")) while True: reponse = input(f"Votre choix [{defaut}] : ").strip() if not reponse: return defaut - 1 if reponse.isdigit() and 1 <= int(reponse) <= len(options): return int(reponse) - 1 print(f" Tapez un nombre entre 1 et {len(options)}.") def attendre(message="Appuyez sur Entrée pour continuer..."): try: input(message) except EOFError: pass # --- Quarto -------------------------------------------------------------------- def trouver_quarto(): trouve = shutil.which("quarto") if trouve: return trouve candidats = { "Windows": [Path(os.environ.get("ProgramFiles", r"C:\Program Files")) / "Quarto/bin/quarto.exe", Path(os.environ.get("LOCALAPPDATA", "")) / "Programs/Quarto/bin/quarto.exe"], "Darwin": [Path("/Applications/quarto/bin/quarto"), Path("/usr/local/bin/quarto"), Path("/opt/homebrew/bin/quarto")], "Linux": [Path("/opt/quarto/bin/quarto"), Path("/usr/local/bin/quarto"), Path.home() / ".local/bin/quarto"], }.get(SYSTEME, []) for chemin in candidats: if chemin.is_file(): return str(chemin) return None def telecharger_quarto(motif): """Télécharge le fichier d'installation de la dernière version de Quarto dont le nom contient `motif`.""" with urllib.request.urlopen("https://api.github.com/repos/quarto-dev/quarto-cli/releases/latest", timeout=30) as reponse: version = json.load(reponse) fichier = next(a for a in version["assets"] if a["name"].endswith(motif)) destination = Path(tempfile.gettempdir()) / fichier["name"] print(f" Téléchargement de {fichier['name']} ({fichier['size'] // 1_000_000} Mo)...") urllib.request.urlretrieve(fichier["browser_download_url"], destination) return destination def installer_quarto(): try: if SYSTEME == "Windows": if shutil.which("winget"): subprocess.run(["winget", "install", "-e", "--id", "Posit.Quarto", "--accept-source-agreements", "--accept-package-agreements"]) else: subprocess.run(["msiexec", "/i", str(telecharger_quarto("win.msi"))]) elif SYSTEME == "Darwin": if shutil.which("brew"): subprocess.run(["brew", "install", "--cask", "quarto"]) else: print(" L'installateur va s'ouvrir : suivez ses étapes puis revenez ici.") subprocess.run(["open", "-W", str(telecharger_quarto("macos.pkg"))]) elif SYSTEME == "Linux" and shutil.which("apt-get"): arch = "arm64" if platform.machine().lower() in ("aarch64", "arm64") else "amd64" paquet = telecharger_quarto(f"linux-{arch}.deb") print(" Votre mot de passe peut être demandé (installation système).") subprocess.run(["sudo", "apt-get", "install", "-y", str(paquet)]) elif SYSTEME == "Linux" and shutil.which("pacman"): return installer_quarto_arch() else: return False except Exception as erreur: # réseau indisponible, fichier introuvable… print(f" L'installation automatique a échoué : {erreur}") return False return True def installer_quarto_arch(): """Arch Linux et dérivées : dépôts officiels si le paquet y existe, sinon AUR via yay (ou paru).""" for nom in ("quarto-cli", "quarto"): if subprocess.run(["pacman", "-Si", nom], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0: print(" Votre mot de passe peut être demandé (installation système).") subprocess.run(["sudo", "pacman", "-S", "--needed", nom]) return True paquet_aur = "quarto-cli-bin" # binaire officiel empaqueté, à jour (quarto-cli compile depuis les sources) for assistant_aur in ("yay", "paru"): if shutil.which(assistant_aur): print(f" Quarto n'est disponible que dans l'AUR : installation de « {paquet_aur} » avec {assistant_aur}.") print(" Validez les questions posées ; votre mot de passe sera demandé à la fin.") # Jamais avec sudo : yay et paru s'exécutent en utilisateur normal et appellent sudo eux-mêmes. subprocess.run([assistant_aur, "-S", "--needed", paquet_aur]) return True print(" Sous Arch, Quarto n'est disponible que dans l'AUR (paquet « quarto-cli-bin »),") print(" et aucun assistant AUR (yay ou paru) n'a été trouvé sur cet ordinateur.") print(" Installez yay (https://github.com/Jguer/yay) puis relancez, ou installez le paquet à la main.") return False def verifier_quarto(): titre("1/3 Quarto (outil de mise en page)") quarto = trouver_quarto() if quarto: print(f" Quarto est installé : {quarto}") return quarto print(" Quarto n'est pas installé. Il est indispensable pour produire les documents.") if oui_non(" L'installer automatiquement maintenant ?"): installer_quarto() quarto = trouver_quarto() while not quarto: print(f"\n Installez Quarto depuis la page qui va s'ouvrir : {PAGE_QUARTO}") webbrowser.open(PAGE_QUARTO) attendre(" Une fois l'installation terminée, appuyez sur Entrée...") quarto = trouver_quarto() if not quarto and not oui_non(" Quarto reste introuvable. Réessayer ?"): raise SystemExit(1) print(f" Quarto est prêt : {quarto}") return quarto # --- Environnement Python des schémas ---------------------------------------------- def python_du_venv(): return VENV / ("Scripts/python.exe" if SYSTEME == "Windows" else "bin/python") def preparer_venv(): titre("2/3 Environnement Python (schémas)") empreinte = hashlib.sha256(EXIGENCES.read_bytes()).hexdigest() temoin = VENV / ".empreinte-requirements" if python_du_venv().exists() and temoin.exists() and temoin.read_text().strip() == empreinte: print(" Déjà prêt.") return True try: if not python_du_venv().exists(): print(" Création de l'environnement virtuel .venv ...") subprocess.run([sys.executable, "-m", "venv", str(VENV)], check=True) print(" Installation des bibliothèques (la première fois, cela peut prendre quelques minutes)...") subprocess.run([str(python_du_venv()), "-m", "pip", "install", "--disable-pip-version-check", "-q", "-r", str(EXIGENCES)], check=True) except subprocess.CalledProcessError: print(" Impossible de préparer l'environnement Python (connexion Internet ?).") print(" Ce n'est pas bloquant : les documents peuvent être produits avec les schémas existants.") return False temoin.write_text(empreinte) print(" Environnement prêt.") return True # --- Génération -------------------------------------------------------------------- def lister_supports(): supports = [] for qmd in sorted(MODULES.glob("*.qmd")): m = re.search(r'^title:\s*"?(.*?)"?\s*$', qmd.read_text(encoding="utf-8"), re.M) supports.append((qmd, m.group(1) if m else qmd.stem)) return supports def ouvrir_dossier(dossier): try: if SYSTEME == "Windows": os.startfile(dossier) elif SYSTEME == "Darwin": subprocess.run(["open", str(dossier)]) else: subprocess.run(["xdg-open", str(dossier)]) except Exception: pass def generer(quarto, venv_ok): titre("3/3 Génération des documents") i_format = choisir("Quel format ?", [f[0] for f in FORMATS]) libelle, cible, extension = FORMATS[i_format] supports = lister_supports() i_portee = choisir("Quels supports ?", ["Tous les supports"] + [t for _, t in supports]) if venv_ok and oui_non("\nRégénérer aussi les schémas (utile seulement si vous les avez modifiés) ?", defaut=False): subprocess.run([str(python_du_venv()), str(RACINE / "schemas" / "generer.py")], cwd=RACINE) debut = time.time() if i_portee == 0: print(f"\nProduction de tous les supports en {libelle} : patientez...\n") commandes = [[quarto, "render", "--to", cible]] else: qmd = supports[i_portee - 1][0] print(f"\nProduction de « {supports[i_portee - 1][1]} » en {libelle}...\n") commandes = [[quarto, "render", str(qmd.relative_to(RACINE)), "--to", cible]] for commande in commandes: if subprocess.run(commande, cwd=RACINE).returncode != 0: print("\n*** La génération a échoué. Le message d'erreur se trouve juste au-dessus. ***") return False produits = sorted(p for p in SORTIE.rglob(f"*{extension}") if p.stat().st_mtime >= debut - 1) titre("Terminé") for p in produits: print(f" {p.relative_to(RACINE)}") print(f"\n {len(produits)} document(s) dans le dossier : {SORTIE}") ouvrir_dossier(SORTIE / "modules" if (SORTIE / "modules").is_dir() else SORTIE) return True def main(): print("Assistant de génération — Formation ESP32-S3 avec Arduino Framework") if sys.version_info < (3, 9): print("Python 3.9 ou plus récent est nécessaire.") raise SystemExit(1) quarto = verifier_quarto() venv_ok = preparer_venv() while True: generer(quarto, venv_ok) if not oui_non("\nGénérer d'autres documents ?", defaut=False): break if __name__ == "__main__": try: main() except KeyboardInterrupt: print("\nInterrompu.") except SystemExit: raise except Exception as erreur: print(f"\nErreur inattendue : {erreur}") raise SystemExit(1)