[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to be installed by hand afterwards. A check list now carries them, filtered per machine — Android Studio is x86_64 only, Google publishes no Linux aarch64 build — and their disk cost reaches the plan before any qcow2 is created. They are installed BEFORE the clone: PyCharm writes the .idea/ of the repository, and the install that follows is what runs pycharm_configuration.py, through update_env_version.pycharm_update(). Its launcher is named studio, which is enough to conclude the install failed; it now answers to android-studio too. GNOME extensions come from the site by UUID, per running Shell: the same endpoint serves gTile v59 for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and the install's exit code still wins, 45 tests. --- FR --- Une VM graphique n'était qu'un bureau : chaque outil de développement restait à poser à la main. Une liste à cocher les porte, filtrés machine par machine — Android Studio n'existe qu'en x86_64, Google ne publiant aucune archive Linux aarch64 — et leur place disque atteint le plan avant qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance pycharm_configuration.py, via update_env_version.pycharm_update(). Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il répond désormais aussi à android-studio. Les extensions GNOME viennent du site par UUID, selon le Shell qui tourne : le même point d'entrée sert gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque outil peut échouer sans que le code de sortie de l'installation ne change, 45 tests. Assisted-by: Claude Opus 5
This commit is contained in:
parent
57ab0b506f
commit
c2852089a0
7 changed files with 1322 additions and 8 deletions
|
|
@ -262,6 +262,33 @@ Ubuntu VM (libvirt)**. From there you can deploy a VM, preview a dry-run,
|
|||
download an image, list VMs and show a VM IP address — the menu asks for the
|
||||
parameters and builds the command for you.
|
||||
|
||||
When a VM is graphical, the menu also offers a **check list of development
|
||||
tools**: PyCharm (installed from the official JetBrains archive
|
||||
into `/opt/pycharm`, its launcher opening the ERPLibre checkout), Android
|
||||
Studio (`/opt/android-studio`, command `studio` or `android-studio`; x86_64
|
||||
only — Google publishes no Linux aarch64 build) and a set of suggested GNOME extensions.
|
||||
|
||||
The extension packages of the distribution are installed but left disabled —
|
||||
their UUID is not reliably known, and the Extension Manager is there to pick
|
||||
from. Three extensions named by UUID are installed **and enabled**, straight
|
||||
from extensions.gnome.org: **gTile**, **Freon** and **Tracker**. The archive
|
||||
is fetched for the GNOME Shell version actually running in the VM — the same
|
||||
endpoint serves gTile v59 for GNOME 46 and v62 for GNOME 48, so a frozen URL
|
||||
would install a build made for another release. A mismatched build is never
|
||||
loaded by GNOME anyway: it compares `metadata.json` with its own version and
|
||||
shows the extension as outdated rather than breaking the session.
|
||||
|
||||
The tools are installed **before** the clone and the ERPLibre install, and
|
||||
the order matters: PyCharm writes the repository's `.idea/` the first time it
|
||||
opens the project, and the install that follows runs
|
||||
`pycharm_configuration.py` on it (`update_env_version.pycharm_update()`,
|
||||
which skips silently when there is no `.idea` yet). So on a fresh VM: open
|
||||
PyCharm once on `~/git/erplibre`, close it, then re-run
|
||||
`make install_odoo_18`.
|
||||
|
||||
Each tool is filtered per VM — by architecture and by desktop flavour — and
|
||||
its disk cost is added to the plan before anything is created.
|
||||
|
||||
## Main options
|
||||
|
||||
- `--distro` — `ubuntu` (default), `debian` or `fedora`.
|
||||
|
|
@ -279,9 +306,38 @@ parameters and builds the command for you.
|
|||
- `--no-install-deps` — never auto-install dependencies.
|
||||
- `--dry-run` — show the commands without executing anything.
|
||||
- `--force` — overwrite the existing working qcow2 disk.
|
||||
- `--lang` — language of the SSH login guide, `fr` (default) or `en`. The
|
||||
TODO menu passes its own language.
|
||||
- `--erplibre-dir` — where ERPLibre will live in the VM
|
||||
(`~/git/erplibre`, or `/opt/erplibre` in production). Adds the ERPLibre
|
||||
section to the login guide; omitted, that section is left out.
|
||||
- `--erplibre-make` — the make target that installed the VM
|
||||
(e.g. `install_odoo_18`), shown in the guide as the way to update it.
|
||||
- `--no-git-identity` — do not copy the host's `user.name`, `user.email`
|
||||
and `core.editor` into the VM's `~/.gitconfig`.
|
||||
|
||||
Run `./script/qemu/deploy_qemu.py --help` for the full list.
|
||||
|
||||
## Login guide (`/etc/motd`)
|
||||
|
||||
Every VM greets you, at each interactive SSH login, with the commands of
|
||||
**its own** distribution — `apt`, `dnf`, `zypper` or `pacman` — plus the
|
||||
ERPLibre ones (edit the server, restart it, update modules, update Odoo,
|
||||
inspect the instance, open the TODO menu). It is written by cloud-init, so
|
||||
it is there from the first boot: before ERPLibre is installed, and still
|
||||
there if that installation fails, which is exactly when you log in by hand.
|
||||
|
||||
`--dry-run` prints the generated guide along with the rest of the user-data.
|
||||
The guide is not shown to `ssh host 'command'`, so it never pollutes an
|
||||
installation log.
|
||||
|
||||
The host's git identity travels with it, into the VM's `~/.gitconfig`: a
|
||||
commit made in the VM then carries your name instead of
|
||||
`erplibre@<vm-name>`. The editor follows the same route — `core.editor`, the
|
||||
`config.conf` line of the guide, and the package installed in the VM all
|
||||
come from one table, so the guide never names a command the VM does not
|
||||
have.
|
||||
|
||||
<!-- [fr] -->
|
||||
L'utilisateur par défaut est `erplibre` (modifiable avec `--user`).
|
||||
|
||||
|
|
@ -294,6 +350,35 @@ prévisualiser un dry-run, télécharger une image, lister les VM et afficher
|
|||
l'IP d'une VM — le menu demande les paramètres et construit la commande pour
|
||||
vous.
|
||||
|
||||
Quand une VM est graphique, le menu propose en plus une **liste à cocher
|
||||
d'outils de développement** : PyCharm (posé depuis l'archive officielle
|
||||
JetBrains dans `/opt/pycharm`, son lanceur ouvrant le dépôt ERPLibre),
|
||||
Android Studio (`/opt/android-studio`, commande `studio` ou
|
||||
`android-studio` ; x86_64 seulement — Google ne publie aucune archive Linux
|
||||
aarch64) et un jeu
|
||||
d'extensions GNOME suggérées.
|
||||
|
||||
Les extensions empaquetées par la distribution sont installées sans être
|
||||
activées — leur UUID n'est pas connu de façon fiable, et le gestionnaire
|
||||
d'extensions est là pour choisir. Trois extensions nommées par leur UUID sont,
|
||||
elles, installées **et activées**, directement depuis extensions.gnome.org :
|
||||
**gTile**, **Freon** et **Tracker**. L'archive est prise pour la version de
|
||||
GNOME Shell qui tourne vraiment dans la VM — le même point d'entrée sert gTile
|
||||
v59 en GNOME 46 et v62 en GNOME 48, si bien qu'une URL figée poserait une
|
||||
version faite pour une autre release. Une archive mal appariée n'est de toute
|
||||
façon jamais chargée par GNOME : il compare `metadata.json` à sa propre
|
||||
version et affiche l'extension comme obsolète plutôt que de casser la session.
|
||||
|
||||
Les outils sont posés **avant** le clone et l'installation d'ERPLibre, et
|
||||
l'ordre compte : PyCharm écrit le `.idea/` du dépôt à la première ouverture du
|
||||
projet, et l'installation qui suit y lance `pycharm_configuration.py`
|
||||
(`update_env_version.pycharm_update()`, qui se tait tant qu'il n'y a pas de
|
||||
`.idea`). Sur une VM neuve : ouvrir PyCharm une fois sur `~/git/erplibre`, le
|
||||
fermer, puis relancer `make install_odoo_18`.
|
||||
|
||||
Chaque outil est filtré VM par VM — architecture et saveur de bureau — et sa
|
||||
place disque s'ajoute au plan avant que rien ne soit créé.
|
||||
|
||||
## Principales options
|
||||
|
||||
- `--distro` — `ubuntu` (défaut), `debian` ou `fedora`.
|
||||
|
|
@ -314,9 +399,37 @@ vous.
|
|||
- `--no-install-deps` — n'installe jamais les dépendances automatiquement.
|
||||
- `--dry-run` — affiche les commandes sans rien exécuter.
|
||||
- `--force` — écrase le disque de travail qcow2 existant.
|
||||
- `--lang` — langue du guide affiché à la connexion SSH, `fr` (défaut) ou
|
||||
`en`. Le menu TODO passe la sienne.
|
||||
- `--erplibre-dir` — où ERPLibre sera installé dans la VM
|
||||
(`~/git/erplibre`, ou `/opt/erplibre` en production). Ajoute la section
|
||||
ERPLibre au guide de connexion ; omis, cette section est laissée de côté.
|
||||
- `--erplibre-make` — la cible make qui a installé la VM
|
||||
(ex. `install_odoo_18`), reprise dans le guide pour la mettre à jour.
|
||||
- `--no-git-identity` — ne recopie pas les `user.name`, `user.email` et
|
||||
`core.editor` de l'hôte dans le `~/.gitconfig` de la VM.
|
||||
|
||||
Lancez `./script/qemu/deploy_qemu.py --help` pour la liste complète.
|
||||
|
||||
## Guide de connexion (`/etc/motd`)
|
||||
|
||||
Chaque VM accueille celui qui s'y connecte en SSH avec les commandes de **sa**
|
||||
distribution — `apt`, `dnf`, `zypper` ou `pacman` — et celles d'ERPLibre :
|
||||
éditer le serveur, le redémarrer, mettre à jour des modules, mettre à jour
|
||||
Odoo, inspecter l'instance, ouvrir le menu TODO. Il est écrit par cloud-init,
|
||||
donc présent dès le premier démarrage : avant l'installation d'ERPLibre, et
|
||||
encore là si elle échoue — le moment où l'on se connecte justement à la main.
|
||||
|
||||
`--dry-run` affiche le guide généré avec le reste du user-data. Il ne
|
||||
s'affiche PAS pour un `ssh hôte 'commande'` : les journaux d'installation
|
||||
restent nets.
|
||||
|
||||
L'identité git de l'hôte voyage avec lui, dans le `~/.gitconfig` de la VM :
|
||||
un commit fait dans la VM porte alors votre nom plutôt que
|
||||
`erplibre@<nom-de-vm>`. L'éditeur suit le même chemin — `core.editor`, la
|
||||
ligne `config.conf` du guide et le paquet installé dans la VM viennent d'une
|
||||
seule table, de sorte que le guide ne nomme jamais une commande absente.
|
||||
|
||||
<!-- [en] -->
|
||||
## Managing VMs
|
||||
|
||||
|
|
|
|||
|
|
@ -146,6 +146,35 @@ prévisualiser un dry-run, télécharger une image, lister les VM et afficher
|
|||
l'IP d'une VM — le menu demande les paramètres et construit la commande pour
|
||||
vous.
|
||||
|
||||
Quand une VM est graphique, le menu propose en plus une **liste à cocher
|
||||
d'outils de développement** : PyCharm (posé depuis l'archive officielle
|
||||
JetBrains dans `/opt/pycharm`, son lanceur ouvrant le dépôt ERPLibre),
|
||||
Android Studio (`/opt/android-studio`, commande `studio` ou
|
||||
`android-studio` ; x86_64 seulement — Google ne publie aucune archive Linux
|
||||
aarch64) et un jeu
|
||||
d'extensions GNOME suggérées.
|
||||
|
||||
Les extensions empaquetées par la distribution sont installées sans être
|
||||
activées — leur UUID n'est pas connu de façon fiable, et le gestionnaire
|
||||
d'extensions est là pour choisir. Trois extensions nommées par leur UUID sont,
|
||||
elles, installées **et activées**, directement depuis extensions.gnome.org :
|
||||
**gTile**, **Freon** et **Tracker**. L'archive est prise pour la version de
|
||||
GNOME Shell qui tourne vraiment dans la VM — le même point d'entrée sert gTile
|
||||
v59 en GNOME 46 et v62 en GNOME 48, si bien qu'une URL figée poserait une
|
||||
version faite pour une autre release. Une archive mal appariée n'est de toute
|
||||
façon jamais chargée par GNOME : il compare `metadata.json` à sa propre
|
||||
version et affiche l'extension comme obsolète plutôt que de casser la session.
|
||||
|
||||
Les outils sont posés **avant** le clone et l'installation d'ERPLibre, et
|
||||
l'ordre compte : PyCharm écrit le `.idea/` du dépôt à la première ouverture du
|
||||
projet, et l'installation qui suit y lance `pycharm_configuration.py`
|
||||
(`update_env_version.pycharm_update()`, qui se tait tant qu'il n'y a pas de
|
||||
`.idea`). Sur une VM neuve : ouvrir PyCharm une fois sur `~/git/erplibre`, le
|
||||
fermer, puis relancer `make install_odoo_18`.
|
||||
|
||||
Chaque outil est filtré VM par VM — architecture et saveur de bureau — et sa
|
||||
place disque s'ajoute au plan avant que rien ne soit créé.
|
||||
|
||||
## Principales options
|
||||
|
||||
- `--distro` — `ubuntu` (défaut), `debian` ou `fedora`.
|
||||
|
|
@ -166,9 +195,37 @@ vous.
|
|||
- `--no-install-deps` — n'installe jamais les dépendances automatiquement.
|
||||
- `--dry-run` — affiche les commandes sans rien exécuter.
|
||||
- `--force` — écrase le disque de travail qcow2 existant.
|
||||
- `--lang` — langue du guide affiché à la connexion SSH, `fr` (défaut) ou
|
||||
`en`. Le menu TODO passe la sienne.
|
||||
- `--erplibre-dir` — où ERPLibre sera installé dans la VM
|
||||
(`~/git/erplibre`, ou `/opt/erplibre` en production). Ajoute la section
|
||||
ERPLibre au guide de connexion ; omis, cette section est laissée de côté.
|
||||
- `--erplibre-make` — la cible make qui a installé la VM
|
||||
(ex. `install_odoo_18`), reprise dans le guide pour la mettre à jour.
|
||||
- `--no-git-identity` — ne recopie pas les `user.name`, `user.email` et
|
||||
`core.editor` de l'hôte dans le `~/.gitconfig` de la VM.
|
||||
|
||||
Lancez `./script/qemu/deploy_qemu.py --help` pour la liste complète.
|
||||
|
||||
## Guide de connexion (`/etc/motd`)
|
||||
|
||||
Chaque VM accueille celui qui s'y connecte en SSH avec les commandes de **sa**
|
||||
distribution — `apt`, `dnf`, `zypper` ou `pacman` — et celles d'ERPLibre :
|
||||
éditer le serveur, le redémarrer, mettre à jour des modules, mettre à jour
|
||||
Odoo, inspecter l'instance, ouvrir le menu TODO. Il est écrit par cloud-init,
|
||||
donc présent dès le premier démarrage : avant l'installation d'ERPLibre, et
|
||||
encore là si elle échoue — le moment où l'on se connecte justement à la main.
|
||||
|
||||
`--dry-run` affiche le guide généré avec le reste du user-data. Il ne
|
||||
s'affiche PAS pour un `ssh hôte 'commande'` : les journaux d'installation
|
||||
restent nets.
|
||||
|
||||
L'identité git de l'hôte voyage avec lui, dans le `~/.gitconfig` de la VM :
|
||||
un commit fait dans la VM porte alors votre nom plutôt que
|
||||
`erplibre@<nom-de-vm>`. L'éditeur suit le même chemin — `core.editor`, la
|
||||
ligne `config.conf` du guide et le paquet installé dans la VM viennent d'une
|
||||
seule table, de sorte que le guide ne nomme jamais une commande absente.
|
||||
|
||||
## Gestion des VM
|
||||
|
||||
Lister, arrêter et supprimer les VM (le disque qcow2 sous
|
||||
|
|
|
|||
|
|
@ -136,6 +136,33 @@ Ubuntu VM (libvirt)**. From there you can deploy a VM, preview a dry-run,
|
|||
download an image, list VMs and show a VM IP address — the menu asks for the
|
||||
parameters and builds the command for you.
|
||||
|
||||
When a VM is graphical, the menu also offers a **check list of development
|
||||
tools**: PyCharm (installed from the official JetBrains archive
|
||||
into `/opt/pycharm`, its launcher opening the ERPLibre checkout), Android
|
||||
Studio (`/opt/android-studio`, command `studio` or `android-studio`; x86_64
|
||||
only — Google publishes no Linux aarch64 build) and a set of suggested GNOME extensions.
|
||||
|
||||
The extension packages of the distribution are installed but left disabled —
|
||||
their UUID is not reliably known, and the Extension Manager is there to pick
|
||||
from. Three extensions named by UUID are installed **and enabled**, straight
|
||||
from extensions.gnome.org: **gTile**, **Freon** and **Tracker**. The archive
|
||||
is fetched for the GNOME Shell version actually running in the VM — the same
|
||||
endpoint serves gTile v59 for GNOME 46 and v62 for GNOME 48, so a frozen URL
|
||||
would install a build made for another release. A mismatched build is never
|
||||
loaded by GNOME anyway: it compares `metadata.json` with its own version and
|
||||
shows the extension as outdated rather than breaking the session.
|
||||
|
||||
The tools are installed **before** the clone and the ERPLibre install, and
|
||||
the order matters: PyCharm writes the repository's `.idea/` the first time it
|
||||
opens the project, and the install that follows runs
|
||||
`pycharm_configuration.py` on it (`update_env_version.pycharm_update()`,
|
||||
which skips silently when there is no `.idea` yet). So on a fresh VM: open
|
||||
PyCharm once on `~/git/erplibre`, close it, then re-run
|
||||
`make install_odoo_18`.
|
||||
|
||||
Each tool is filtered per VM — by architecture and by desktop flavour — and
|
||||
its disk cost is added to the plan before anything is created.
|
||||
|
||||
## Main options
|
||||
|
||||
- `--distro` — `ubuntu` (default), `debian` or `fedora`.
|
||||
|
|
@ -153,9 +180,38 @@ parameters and builds the command for you.
|
|||
- `--no-install-deps` — never auto-install dependencies.
|
||||
- `--dry-run` — show the commands without executing anything.
|
||||
- `--force` — overwrite the existing working qcow2 disk.
|
||||
- `--lang` — language of the SSH login guide, `fr` (default) or `en`. The
|
||||
TODO menu passes its own language.
|
||||
- `--erplibre-dir` — where ERPLibre will live in the VM
|
||||
(`~/git/erplibre`, or `/opt/erplibre` in production). Adds the ERPLibre
|
||||
section to the login guide; omitted, that section is left out.
|
||||
- `--erplibre-make` — the make target that installed the VM
|
||||
(e.g. `install_odoo_18`), shown in the guide as the way to update it.
|
||||
- `--no-git-identity` — do not copy the host's `user.name`, `user.email`
|
||||
and `core.editor` into the VM's `~/.gitconfig`.
|
||||
|
||||
Run `./script/qemu/deploy_qemu.py --help` for the full list.
|
||||
|
||||
## Login guide (`/etc/motd`)
|
||||
|
||||
Every VM greets you, at each interactive SSH login, with the commands of
|
||||
**its own** distribution — `apt`, `dnf`, `zypper` or `pacman` — plus the
|
||||
ERPLibre ones (edit the server, restart it, update modules, update Odoo,
|
||||
inspect the instance, open the TODO menu). It is written by cloud-init, so
|
||||
it is there from the first boot: before ERPLibre is installed, and still
|
||||
there if that installation fails, which is exactly when you log in by hand.
|
||||
|
||||
`--dry-run` prints the generated guide along with the rest of the user-data.
|
||||
The guide is not shown to `ssh host 'command'`, so it never pollutes an
|
||||
installation log.
|
||||
|
||||
The host's git identity travels with it, into the VM's `~/.gitconfig`: a
|
||||
commit made in the VM then carries your name instead of
|
||||
`erplibre@<vm-name>`. The editor follows the same route — `core.editor`, the
|
||||
`config.conf` line of the guide, and the package installed in the VM all
|
||||
come from one table, so the guide never names a command the VM does not
|
||||
have.
|
||||
|
||||
## Managing VMs
|
||||
|
||||
List, stop and remove VMs (the qcow2 disk under `/var/lib/libvirt/images`
|
||||
|
|
|
|||
|
|
@ -294,6 +294,7 @@ def build_spec(vms, domains, form):
|
|||
"ssh_key": form["ssh_key"],
|
||||
"timezone": form.get("timezone", ""),
|
||||
"desktop": form.get("desktop", ""),
|
||||
"desktop_tools": tuple(form.get("desktop_tools") or ()),
|
||||
"python_provider": form.get("python_provider", ""),
|
||||
"app_store": form.get("app_store", "deb"),
|
||||
"install": form["install"],
|
||||
|
|
@ -364,6 +365,13 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
# {clé de saveur: suffixe de nom}, fourni par todo.py qui décrit les
|
||||
# saveurs — on ne le redéfinit pas ici.
|
||||
desktop_suffixes = dict(ctx.get("desktop_suffixes") or {})
|
||||
# Outils de développement d'une VM graphique : [(clé, libellé, indice)] et
|
||||
# leurs contraintes, toutes décrites dans todo.py — le formulaire ne fait
|
||||
# que les afficher et rendre les cases cochées.
|
||||
desktop_tools = list(ctx.get("desktop_tools") or [])
|
||||
tool_disk = dict(ctx.get("desktop_tool_disk") or {})
|
||||
tool_arches = dict(ctx.get("desktop_tool_arches") or {})
|
||||
tool_desktops = dict(ctx.get("desktop_tool_desktops") or {})
|
||||
# Architectures pour lesquelles mise publie un binaire.
|
||||
mise_arches = set(ctx.get("mise_arches") or ())
|
||||
# [(clé, libellé)] des magasins d'applications, et les distributions qui
|
||||
|
|
@ -655,6 +663,21 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
for i, (_k, label) in enumerate(app_stores):
|
||||
yield RadioButton(label, value=i == 0)
|
||||
yield Static("", id="storewarn")
|
||||
if desktop_tools:
|
||||
# Une case par outil, et non une liste déroulante : ils
|
||||
# sont indépendants, et chacun se prend ou se laisse.
|
||||
yield Static(
|
||||
t("Development tools (graphical VMs):"),
|
||||
classes="grouptitle",
|
||||
)
|
||||
for key, label, hint in desktop_tools:
|
||||
gb = tool_disk.get(key, 0)
|
||||
yield Checkbox(
|
||||
f"{label} +{gb} Go — {hint}",
|
||||
value=key in (defaults.get("tools") or ()),
|
||||
id=f"f_tool_{key}",
|
||||
)
|
||||
yield Static("", id="toolwarn")
|
||||
yield Static("ERPLibre", classes="grouptitle")
|
||||
yield Checkbox(
|
||||
t("Install ERPLibre"),
|
||||
|
|
@ -816,9 +839,19 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
# Le bureau pèse sur le disque de la VM QUI LE PORTE, et d'elle
|
||||
# seule : un supplément commun mentait dès que les types
|
||||
# différaient d'une machine à l'autre.
|
||||
tools = self._desktop_tools()
|
||||
for row in self.rows:
|
||||
if row["vm"].get("desktop"):
|
||||
row["disk_gb"] += desktop_disk
|
||||
# Même règle pour les outils, et pour la même raison : ils ne
|
||||
# pèsent que sur les VM qui les reçoivent réellement. Android
|
||||
# Studio n'existe qu'en x86_64, les extensions GNOME n'ont de
|
||||
# sens que sous GNOME — une VM qui ne les aura pas ne doit pas
|
||||
# se voir gonfler son disque.
|
||||
row["disk_gb"] += sum(
|
||||
tool_disk.get(k, 0)
|
||||
for k in self._tools_for_vm(row["vm"], tools)
|
||||
)
|
||||
# Le plan doit MONTRER qu'une VM a été personnalisée : sans marque,
|
||||
# deux lignes aux ressources différentes n'ont aucune explication à
|
||||
# l'écran, et la surcharge est oubliée à la relecture. Le drapeau
|
||||
|
|
@ -831,6 +864,70 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
self._render_plan()
|
||||
self._render_mise()
|
||||
self._render_store()
|
||||
self._render_tools()
|
||||
|
||||
def _desktop_tools(self):
|
||||
"""Clés des outils cochés, dans l'ordre de la liste."""
|
||||
picked = []
|
||||
for key, _label, _hint in desktop_tools:
|
||||
try:
|
||||
if self.query_one(f"#f_tool_{key}", Checkbox).value:
|
||||
picked.append(key)
|
||||
except Exception:
|
||||
continue
|
||||
return tuple(picked)
|
||||
|
||||
def _tools_for_vm(self, vm, tools):
|
||||
"""Outils qu'une VM donnée recevra vraiment.
|
||||
|
||||
Même filtre que todo.py côté déploiement : une VM ARM ne verra
|
||||
jamais Android Studio, une VM Cinnamon jamais les extensions
|
||||
GNOME, et un serveur aucun des trois."""
|
||||
if not vm.get("desktop"):
|
||||
return []
|
||||
out = []
|
||||
for key in tools:
|
||||
arches = tool_arches.get(key) or ()
|
||||
desks = tool_desktops.get(key) or ()
|
||||
if arches and vm["arch"] not in arches:
|
||||
continue
|
||||
if desks and vm["desktop"] not in desks:
|
||||
continue
|
||||
out.append(key)
|
||||
return out
|
||||
|
||||
def _render_tools(self):
|
||||
"""Grise les cases sans VM graphique, et NOMME ce qui sera écarté.
|
||||
|
||||
Cocher Android Studio sur un parc ARM ne produit rien : le dire ici
|
||||
évite de le découvrir dans le journal d'installation."""
|
||||
if not desktop_tools:
|
||||
return
|
||||
graphical = [vm for vm in self.vms if vm.get("desktop")]
|
||||
for key, _label, _hint in desktop_tools:
|
||||
self.query_one(f"#f_tool_{key}", Checkbox).disabled = (
|
||||
not graphical
|
||||
)
|
||||
if not graphical:
|
||||
self.query_one("#toolwarn", Static).update(
|
||||
f" {t('No graphical VM: these tools are not installed.')}"
|
||||
)
|
||||
return
|
||||
picked = self._desktop_tools()
|
||||
skipped = sorted(
|
||||
{
|
||||
vm["name"]
|
||||
for vm in graphical
|
||||
for k in picked
|
||||
if k not in self._tools_for_vm(vm, picked)
|
||||
}
|
||||
)
|
||||
self.query_one("#toolwarn", Static).update(
|
||||
f" ⚠ {t('Partly skipped (arch or desktop):')} "
|
||||
f"{', '.join(skipped)}"
|
||||
if skipped
|
||||
else ""
|
||||
)
|
||||
|
||||
def _render_mise(self):
|
||||
"""Grise le choix quand aucune VM retenue n'est servie par mise,
|
||||
|
|
@ -1676,6 +1773,10 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
self._recompute() # le disque annoncé inclut le +5 G ERPLibre
|
||||
elif event.checkbox.id == "f_par_all":
|
||||
self.query_one("#f_par", Select).disabled = event.value
|
||||
elif str(event.checkbox.id or "").startswith("f_tool_"):
|
||||
# Un IDE de plus, c'est un disque plus grand : le plan doit le
|
||||
# montrer AVANT de déployer, pas après une heure d'installation.
|
||||
self._recompute()
|
||||
|
||||
# -- actions ---------------------------------------------------- #
|
||||
def action_select_all(self) -> None:
|
||||
|
|
@ -1734,6 +1835,7 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
or ctx.get("timezone")
|
||||
or "",
|
||||
"desktop": self._default_desktop(),
|
||||
"desktop_tools": self._desktop_tools(),
|
||||
"python_provider": self._python_provider(),
|
||||
"app_store": self._app_store(),
|
||||
"install": install,
|
||||
|
|
|
|||
|
|
@ -4508,6 +4508,30 @@ class TODO:
|
|||
~/git/erplibre (dev)."""
|
||||
return "/opt/erplibre" if prod else "$HOME/git/erplibre"
|
||||
|
||||
@staticmethod
|
||||
def _qemu_guide_dir(prod):
|
||||
"""Répertoire d'ERPLibre tel que le GUIDE de connexion l'annonce.
|
||||
|
||||
« ~/git/erplibre » plutôt que « $HOME/git/erplibre » : ce chemin n'est
|
||||
pas exécuté par un script, il est lu par quelqu'un qui recopie la ligne
|
||||
dans son shell — où les deux marchent — et le tilde est la forme qu'il
|
||||
reconnaît. En production le chemin est absolu et la question ne se pose
|
||||
pas."""
|
||||
return "/opt/erplibre" if prod else "~/git/erplibre"
|
||||
|
||||
@staticmethod
|
||||
def _qemu_make_target(install_cmd):
|
||||
"""Cible make qui installe Odoo dans `install_cmd`, pour le guide.
|
||||
|
||||
Les profils s'écrivent « make install_os && make install_odoo_18 » : la
|
||||
cible utile est la SECONDE, celle qui installe Odoo, et c'est aussi
|
||||
celle qu'on relance après un « git pull ». Les profils qui n'en ont pas
|
||||
(« ERPLibre seul », « mobile », « Déploiement ») rendent une chaîne
|
||||
vide : le guide s'arrête alors à « git pull » plutôt que d'annoncer une
|
||||
cible qui n'est pas celle de cette VM."""
|
||||
found = re.findall(r"make\s+(install_odoo\S*)", install_cmd or "")
|
||||
return found[-1] if found else ""
|
||||
|
||||
def _qemu_odoo_service_cmd(self, prod=False):
|
||||
"""Snippet shell (exécuté dans la VM) qui installe ERPLibre/Odoo comme
|
||||
service systemd puis l'active. N'est ajouté QUE pour les profils Odoo.
|
||||
|
|
@ -4992,6 +5016,435 @@ class TODO:
|
|||
+ "fi; "
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Outils de développement d'une VM graphique
|
||||
# ------------------------------------------------------------------ #
|
||||
# Chacun est une case à cocher, indépendante des autres, et chacun pèse sur
|
||||
# le disque — le plan l'annonce AVANT de déployer, sinon l'installation se
|
||||
# termine sur un disque plein après une heure d'attente.
|
||||
#
|
||||
# « disk_gb » compte le PIC, pas l'installé : l'archive téléchargée vit sur
|
||||
# le disque le temps de l'extraction. PyCharm, c'est 1,2 Go d'archive et
|
||||
# ~3 Go déplié ; Android Studio 1,5 Go et 3,5 Go, plus la place du premier
|
||||
# SDK que l'utilisateur téléchargera.
|
||||
#
|
||||
# « arches » n'est pas une précaution : Google ne publie Android Studio
|
||||
# QU'EN x86_64 (vérifié — toutes les variantes aarch64 de l'URL rendent 404,
|
||||
# et le product-info.json de l'archive ne déclare qu'une cible
|
||||
# « Linux/amd64 »). JetBrains, lui, publie bien une archive aarch64.
|
||||
_QEMU_DESKTOP_TOOLS = {
|
||||
"pycharm": {
|
||||
"label": "PyCharm",
|
||||
"hint": "Python IDE, opens the ERPLibre checkout",
|
||||
"disk_gb": 5,
|
||||
"arches": ("amd64", "arm64"),
|
||||
"desktops": (),
|
||||
},
|
||||
"android": {
|
||||
"label": "Android Studio",
|
||||
"hint": "ERPLibre mobile development (x86_64 only)",
|
||||
"disk_gb": 8,
|
||||
"arches": ("amd64",),
|
||||
"desktops": (),
|
||||
},
|
||||
"gnome_ext": {
|
||||
"label": "GNOME extensions",
|
||||
"hint": "suggested extensions + extension manager",
|
||||
"disk_gb": 1,
|
||||
"arches": (),
|
||||
"desktops": ("gnome",),
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _qemu_desktop_tool_choices(cls):
|
||||
"""[(clé, libellé, indice)] pour le formulaire et l'invite en ligne."""
|
||||
return [
|
||||
(key, t(spec["label"]), t(spec["hint"]))
|
||||
for key, spec in cls._QEMU_DESKTOP_TOOLS.items()
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _qemu_tools_for(cls, tools, arch, desktop):
|
||||
"""Outils RÉELLEMENT applicables à cette VM.
|
||||
|
||||
Un outil demandé pour tout le parc ne convient pas forcément à chaque
|
||||
machine : Android Studio n'existe qu'en x86_64, et les extensions GNOME
|
||||
n'ont pas de sens sous Cinnamon. Filtrer ici plutôt que dans la commande
|
||||
distante évite d'annoncer une installation qui ne se fera pas."""
|
||||
if not desktop:
|
||||
return []
|
||||
out = []
|
||||
for key in tools or ():
|
||||
spec = cls._QEMU_DESKTOP_TOOLS.get(key)
|
||||
if not spec:
|
||||
continue
|
||||
if spec["arches"] and arch not in spec["arches"]:
|
||||
continue
|
||||
if spec["desktops"] and desktop not in spec["desktops"]:
|
||||
continue
|
||||
out.append(key)
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def _qemu_tools_disk_gb(cls, tools, arch, desktop):
|
||||
"""Go à ajouter au disque pour les outils applicables à cette VM."""
|
||||
return sum(
|
||||
cls._QEMU_DESKTOP_TOOLS[k]["disk_gb"]
|
||||
for k in cls._qemu_tools_for(tools, arch, desktop)
|
||||
)
|
||||
|
||||
# Archive officielle JetBrains, et non un paquet de distribution : aucun ne
|
||||
# couvre les quatre gestionnaires (Arch l'a dans extra, Debian et Ubuntu ne
|
||||
# l'ont qu'en snap — coupé ici —, Fedora et openSUSE pas du tout).
|
||||
#
|
||||
# L'URL ne porte AUCUN numéro de version : « code=PCC&latest » redirige vers
|
||||
# la dernière stable. Vérifié, les deux : « distribution=linux » sort
|
||||
# pycharm-2025.3.tar.gz et « distribution=linuxARM64 » son équivalent
|
||||
# aarch64. Rien à mettre à jour dans ce dépôt quand JetBrains publie.
|
||||
_QEMU_PYCHARM_URL = (
|
||||
"https://download.jetbrains.com/product?code=PCC&latest&distribution="
|
||||
)
|
||||
|
||||
# Android Studio n'a PAS d'URL « latest » : le répertoire de version
|
||||
# (2026.1.3.8) et le nom de fichier (quail3-patch1) sont deux jetons
|
||||
# INDÉPENDANTS, l'un ne se déduit pas de l'autre, et le flux updates.xml de
|
||||
# Google ne publie ni l'un ni l'autre. On lit donc l'URL sur la page
|
||||
# officielle, qui la porte en clair, et on retombe sur celle-ci si la page
|
||||
# change de forme. Relevée et vérifiée (HTTP 200) le 2026-08-17.
|
||||
_QEMU_ANDROID_URL = (
|
||||
"https://dl.google.com/dl/android/studio/ide-zips/2026.1.3.8/"
|
||||
"android-studio-quail3-patch1-linux.tar.gz"
|
||||
)
|
||||
_QEMU_ANDROID_PAGE = "https://developer.android.com/studio"
|
||||
|
||||
@staticmethod
|
||||
def _qemu_desktop_entry_cmd(name, label, exec_cmd, icon, categories):
|
||||
"""Écrit un lanceur .desktop. Sans lui, un outil déplié dans /opt
|
||||
n'existe pas pour le bureau : il ne se lance qu'en tapant son chemin.
|
||||
"""
|
||||
return (
|
||||
f"sudo tee /usr/share/applications/{name}.desktop >/dev/null <<DESK\n"
|
||||
"[Desktop Entry]\n"
|
||||
"Type=Application\n"
|
||||
"Version=1.0\n"
|
||||
f"Name={label}\n"
|
||||
f"Exec={exec_cmd}\n"
|
||||
f"Icon={icon}\n"
|
||||
"Terminal=false\n"
|
||||
f"Categories={categories}\n"
|
||||
"StartupNotify=true\n"
|
||||
"DESK\n"
|
||||
)
|
||||
|
||||
def _qemu_jetbrains_launcher_cmd(self, root, link, alias=""):
|
||||
"""Lien vers le lanceur de l'archive, quel que soit son nom.
|
||||
|
||||
JetBrains a renommé « bin/pycharm.sh » en « bin/pycharm » (et
|
||||
« studio.sh » en « studio ») : les deux existent selon la version, on
|
||||
prend celui qui est là.
|
||||
|
||||
`alias` : un second nom pour la même commande. L'archive d'Android
|
||||
Studio n'installe que « studio », mais personne ne tape « studio » —
|
||||
on cherche « android-studio », on ne trouve rien, et on conclut que
|
||||
l'installation a échoué alors qu'elle est bien là. Vécu."""
|
||||
return (
|
||||
f"b=$(ls {root}/bin/{link}.sh {root}/bin/{link} 2>/dev/null "
|
||||
"| head -1); "
|
||||
f'[ -n "$b" ] && sudo ln -sf "$b" /usr/local/bin/{link}; '
|
||||
+ (
|
||||
f'[ -n "$b" ] && sudo ln -sf "$b" /usr/local/bin/{alias}; '
|
||||
if alias
|
||||
else ""
|
||||
)
|
||||
)
|
||||
|
||||
def _qemu_pycharm_remote_cmd(self, prod=False):
|
||||
"""Installe PyCharm et lui donne le dépôt ERPLibre comme projet.
|
||||
|
||||
« Configuré sur git/erplibre » veut dire deux choses, et les deux sont
|
||||
faites ici : le lanceur du bureau OUVRE ce dépôt, et
|
||||
pycharm_configuration.py y écrit le .idea/ du projet (interpréteur,
|
||||
configurations d'exécution, dossiers exclus) — la même chose que
|
||||
« make pycharm_configure », mais avec le python du venv d'outils, seul à
|
||||
disposer de xmltodict.
|
||||
|
||||
Tout le bloc est gardé : un IDE qui ne s'installe pas ne doit pas faire
|
||||
échouer l'installation d'ERPLibre, qui elle a duré une heure."""
|
||||
el_dir = self._qemu_install_dir(prod)
|
||||
return (
|
||||
f'echo "== {t("Installing PyCharm (long)")} =="; '
|
||||
"{ "
|
||||
'case "$(uname -m)" in x86_64) jb=linux;; '
|
||||
'aarch64|arm64) jb=linuxARM64;; *) jb="";; esac; '
|
||||
# if/else et non « || { …; false; } » : dans un groupe, un échec
|
||||
# n'interrompt PAS la suite (set -e est suspendu à gauche d'un
|
||||
# « && »), et l'architecture non servie partait quand même
|
||||
# télécharger une URL sans valeur de distribution.
|
||||
'if [ -z "$jb" ]; then '
|
||||
f'echo " {t("no JetBrains build for")} $(uname -m)"; false; '
|
||||
"else "
|
||||
# /var/tmp et non /tmp : sur Fedora et dérivés /tmp est un tmpfs, en
|
||||
# RAM — 1,2 Go d'archive y tueraient une VM de 3 Go.
|
||||
"tmp=$(mktemp -p /var/tmp pycharm-XXXX.tar.gz) && "
|
||||
f'curl -fsSL "{self._QEMU_PYCHARM_URL}$jb" -o "$tmp" && '
|
||||
"sudo mkdir -p /opt/pycharm && "
|
||||
'sudo tar -xzf "$tmp" -C /opt/pycharm --strip-components=1; '
|
||||
'rc=$?; rm -f "$tmp"; [ $rc -eq 0 ]; fi; } && { '
|
||||
+ self._qemu_jetbrains_launcher_cmd("/opt/pycharm", "pycharm")
|
||||
+ self._qemu_desktop_entry_cmd(
|
||||
"pycharm",
|
||||
"PyCharm (ERPLibre)",
|
||||
f"/usr/local/bin/pycharm {el_dir}",
|
||||
"/opt/pycharm/bin/pycharm.svg",
|
||||
"Development;IDE;",
|
||||
)
|
||||
# AUCUN appel à pycharm_configuration.py ici : l'installation
|
||||
# ERPLibre le fait déjà. update_env_version.pycharm_update() teste
|
||||
# « os.path.exists('.idea') » puis lance le script — une seule
|
||||
# autorité, et elle sait se taire quand le projet n'existe pas
|
||||
# encore. Doubler l'appel ne configurait rien de plus : ça écrivait
|
||||
# « Missing ./.idea path » dans le journal d'une VM neuve, où
|
||||
# PyCharm n'a évidemment jamais ouvert le dépôt.
|
||||
+ f'echo " {t("PyCharm installed:")} /opt/pycharm '
|
||||
f'({t("command")} pycharm, {t("project")} {el_dir})"; '
|
||||
f'echo " {t("open the project once and close PyCharm; the .idea "
|
||||
"it writes is what the install configures")}"; '
|
||||
f'}} || echo " ⚠ {t("PyCharm not installed (see above)")}"; '
|
||||
)
|
||||
|
||||
def _qemu_android_studio_remote_cmd(self):
|
||||
"""Installe Android Studio, pour le développement mobile ERPLibre.
|
||||
|
||||
L'émulateur, lui, exige KVM DANS la VM, donc la virtualisation
|
||||
imbriquée : on le dit plutôt que de laisser découvrir l'échec au premier
|
||||
lancement. Compiler et déployer sur un appareil réel par adb n'en
|
||||
dépendent pas."""
|
||||
return (
|
||||
f'echo "== {t("Installing Android Studio (long)")} =="; '
|
||||
"{ "
|
||||
'if [ "$(uname -m)" != x86_64 ]; then '
|
||||
f'echo " {t("Android Studio: Google publishes x86_64 only")}"; '
|
||||
"false; "
|
||||
"else "
|
||||
# La page officielle porte l'URL en clair ; le repli garde une
|
||||
# version connue qui répond, pour le jour où sa forme change.
|
||||
f"url=$(curl -fsSL --max-time 30 {self._QEMU_ANDROID_PAGE} "
|
||||
"| grep -oE 'https://[a-z0-9.-]*gvt1\\.com/[^\"]*linux\\.tar\\.gz' "
|
||||
"| head -1); "
|
||||
f'[ -n "$url" ] || url="{self._QEMU_ANDROID_URL}"; '
|
||||
"tmp=$(mktemp -p /var/tmp android-XXXX.tar.gz) && "
|
||||
'curl -fsSL "$url" -o "$tmp" && '
|
||||
"sudo mkdir -p /opt/android-studio && "
|
||||
'sudo tar -xzf "$tmp" -C /opt/android-studio '
|
||||
"--strip-components=1; "
|
||||
'rc=$?; rm -f "$tmp"; [ $rc -eq 0 ]; fi; } && { '
|
||||
+ self._qemu_jetbrains_launcher_cmd(
|
||||
"/opt/android-studio", "studio", alias="android-studio"
|
||||
)
|
||||
+ self._qemu_desktop_entry_cmd(
|
||||
"android-studio",
|
||||
"Android Studio",
|
||||
"/usr/local/bin/studio",
|
||||
"/opt/android-studio/bin/studio.svg",
|
||||
"Development;IDE;",
|
||||
)
|
||||
+ f'echo " {t("Android Studio installed:")} /opt/android-studio '
|
||||
f'({t("command")} studio / android-studio)"; '
|
||||
"grep -q vmx /proc/cpuinfo 2>/dev/null "
|
||||
"|| grep -q svm /proc/cpuinfo 2>/dev/null "
|
||||
f'|| echo " {t("no nested KVM: the emulator will not run")}"; '
|
||||
f'}} || echo " ⚠ {t("Android Studio not installed (see above)")}"; '
|
||||
)
|
||||
|
||||
# Extensions GNOME suggérées, par gestionnaire de paquets. Les noms ne sont
|
||||
# pas les mêmes d'une famille à l'autre (« dashtodock » sur Debian,
|
||||
# « dash-to-dock » sur Fedora), et aucune liste n'existe en entier partout.
|
||||
#
|
||||
# D'où l'installation UNE PAR UNE : apt, dnf, zypper et pacman échouent tous
|
||||
# sur la commande ENTIÈRE dès qu'un seul nom est inconnu. Un paquet absent
|
||||
# est donc annoncé et sauté, au lieu de faire tomber les autres avec lui.
|
||||
_QEMU_GNOME_EXT_PKGS = {
|
||||
"apt": (
|
||||
"gnome-shell-extension-manager",
|
||||
"gnome-tweaks",
|
||||
"gnome-shell-extensions",
|
||||
"gnome-shell-extension-dashtodock",
|
||||
"gnome-shell-extension-appindicator",
|
||||
"gnome-shell-extension-caffeine",
|
||||
),
|
||||
"dnf": (
|
||||
"gnome-extensions-app",
|
||||
"gnome-tweaks",
|
||||
"gnome-shell-extension-dash-to-dock",
|
||||
"gnome-shell-extension-appindicator",
|
||||
"gnome-shell-extension-caffeine",
|
||||
"gnome-shell-extension-user-theme",
|
||||
),
|
||||
"zypper": (
|
||||
"gnome-shell-extensions",
|
||||
"gnome-tweaks",
|
||||
"gnome-shell-extension-dash-to-dock",
|
||||
"gnome-shell-extension-appindicator",
|
||||
),
|
||||
"pacman": (
|
||||
"extension-manager",
|
||||
"gnome-tweaks",
|
||||
"gnome-shell-extensions",
|
||||
),
|
||||
}
|
||||
|
||||
# Extensions demandées nommément, par leur UUID sur extensions.gnome.org.
|
||||
# Aucune n'est empaquetée par une distribution : on passe donc par le site.
|
||||
#
|
||||
# L'archive dépend de la version de GNOME Shell, et ce n'est pas une
|
||||
# précaution de principe : mesuré le 2026-08-17, le même point d'entrée
|
||||
# sert gTile v59 pour GNOME 46, v62 pour GNOME 48 et v52 pour GNOME 3.38.
|
||||
# Une URL figée poserait donc, tôt ou tard, une archive faite pour une
|
||||
# autre version.
|
||||
#
|
||||
# Ce que le site fait d'une version qu'il ne connaît PAS : il sert la plus
|
||||
# récente (vérifié — « shell_version=99 » rend l'archive des GNOME 49/50),
|
||||
# il ne répond pas 404. Sans conséquence fâcheuse pour autant : GNOME Shell
|
||||
# refuse de CHARGER une extension dont metadata.json ne déclare pas la
|
||||
# version courante. Une archive mal appariée reste donc inerte et affichée
|
||||
# « obsolète » dans le gestionnaire — elle ne casse pas la session.
|
||||
_QEMU_GNOME_EXT_UUIDS = (
|
||||
"gTile@vibou",
|
||||
"freon@UshakovVasilii_Github.yahoo.com",
|
||||
"tracker@aliakseiz.github.com",
|
||||
)
|
||||
_QEMU_GNOME_EXT_SITE = "https://extensions.gnome.org/download-extension"
|
||||
|
||||
def _qemu_gnome_ext_site_cmd(self):
|
||||
"""Installe les extensions nommées depuis extensions.gnome.org.
|
||||
|
||||
Celles-là, on les ACTIVE — à la différence des paquets de la
|
||||
distribution, dont on ne connaît pas l'UUID. Deux raisons, l'une et
|
||||
l'autre vérifiées : le site rend l'archive faite pour le GNOME Shell de
|
||||
cette VM, et une archive mal appariée n'est de toute façon jamais
|
||||
chargée par GNOME, qui compare metadata.json à sa propre version. Ce
|
||||
n'est donc pas l'activation qui peut casser une session.
|
||||
|
||||
Le tout dans un groupe gardé : ni une panne de réseau ni une extension
|
||||
retirée du site ne doivent faire échouer une installation d'une heure.
|
||||
"""
|
||||
uuids = " ".join(self._QEMU_GNOME_EXT_UUIDS)
|
||||
site = self._QEMU_GNOME_EXT_SITE
|
||||
return (
|
||||
"{ "
|
||||
# « gnome-shell --version » rend « GNOME Shell 48.2 » : le dernier
|
||||
# champ suffit, et évite une expression régulière à rallonge.
|
||||
"v=$(gnome-shell --version 2>/dev/null | awk '{print $NF}'); "
|
||||
'if [ -z "$v" ]; then '
|
||||
+ f'echo " {t("GNOME Shell not found, site extensions skipped")}"; '
|
||||
+ "else "
|
||||
# Le site attend le numéro MAJEUR depuis GNOME 40 (« 48 ») et
|
||||
# « majeur.mineur » avant (« 3.38 ») : sans la bonne forme, il ne
|
||||
# renvoie aucune archive.
|
||||
"maj=${v%%.*}; "
|
||||
'if [ "$maj" -ge 40 ] 2>/dev/null; then sv="$maj"; '
|
||||
'else sv=$(echo "$v" | cut -d. -f1,2); fi; '
|
||||
# gnome-extensions écrit dans ~/.local/share, mais l'activation
|
||||
# passe par GSettings : sans bus de session — le cas d'un
|
||||
# « ssh hôte commande » — dconf ne peut rien écrire.
|
||||
# dbus-run-session en fournit un le temps de l'appel, et
|
||||
# l'écriture atterrit bien dans le dconf de l'utilisateur.
|
||||
'gx() { if [ -z "$DBUS_SESSION_BUS_ADDRESS" ] && '
|
||||
"command -v dbus-run-session >/dev/null 2>&1; then "
|
||||
'dbus-run-session -- gnome-extensions "$@"; '
|
||||
'else gnome-extensions "$@"; fi; }; ' + f"for u in {uuids}; do "
|
||||
# « || echo » DANS la substitution : un mktemp qui échoue rendrait
|
||||
# l'affectation non nulle, et « set -e » couperait toute la suite.
|
||||
+ "z=$(mktemp -p /var/tmp gext-XXXX.zip || echo /var/tmp/gext.zip); "
|
||||
+ 'if curl -fsSL --max-time 120 "'
|
||||
+ site
|
||||
+ '/$u.shell-extension.zip?shell_version=$sv" -o "$z" '
|
||||
+ '&& gx install --force "$z" >/dev/null 2>&1; then '
|
||||
+ 'gx enable "$u" >/dev/null 2>&1 || true; '
|
||||
+ f'echo " {t("installed and enabled:")} $u"; else '
|
||||
+ f'echo " {t("not available for this GNOME, skipped:")} '
|
||||
+ '$u (GNOME $sv)"; fi; rm -f "$z"; done; '
|
||||
+ f'echo " {t("log out and back in to load them")}"; '
|
||||
+ "fi; } || true; "
|
||||
)
|
||||
|
||||
def _qemu_gnome_ext_remote_cmd(self):
|
||||
"""Pose les extensions GNOME suggérées.
|
||||
|
||||
Deux sources, et deux politiques, pour une raison :
|
||||
- les paquets de la DISTRIBUTION sont installés sans être activés. On
|
||||
ne connaît pas leur UUID de façon fiable, et activer à l'aveugle une
|
||||
extension incompatible avec la version de GNOME Shell laisse la
|
||||
session sur un écran noir — panne qu'on ne diagnostique pas depuis
|
||||
une console série. Le gestionnaire graphique est posé pour choisir ;
|
||||
- les extensions nommées par leur UUID sont, elles, ACTIVÉES : le site
|
||||
rend l'archive faite pour ce GNOME-là, et une archive mal appariée
|
||||
n'est jamais chargée par GNOME plutôt que de casser la session.
|
||||
"""
|
||||
pkgs = self._QEMU_GNOME_EXT_PKGS
|
||||
return (
|
||||
f'echo "== {t("Suggested GNOME extensions")} =="; '
|
||||
"if command -v apt-get >/dev/null 2>&1; then "
|
||||
f"EXT='{' '.join(pkgs['apt'])}'; "
|
||||
"I='sudo DEBIAN_FRONTEND=noninteractive apt-get "
|
||||
"-o DPkg::Lock::Timeout=600 install -y'; "
|
||||
"elif command -v dnf >/dev/null 2>&1; then "
|
||||
f"EXT='{' '.join(pkgs['dnf'])}'; I='sudo dnf install -y'; "
|
||||
"elif command -v zypper >/dev/null 2>&1; then "
|
||||
f"EXT='{' '.join(pkgs['zypper'])}'; "
|
||||
"I='sudo zypper --non-interactive install "
|
||||
"--auto-agree-with-licenses'; "
|
||||
"elif command -v pacman >/dev/null 2>&1; then "
|
||||
f"EXT='{' '.join(pkgs['pacman'])}'; "
|
||||
"I='sudo pacman -S --needed --noconfirm'; "
|
||||
'else EXT=""; fi; '
|
||||
'for p in $EXT; do $I "$p" >/dev/null 2>&1 '
|
||||
f'|| echo " {t("not in the repos, skipped:")} $p"; done; '
|
||||
f'echo " {t("Enable them from Extension Manager, or:")} '
|
||||
'gnome-extensions enable <uuid>"; '
|
||||
+ self._qemu_gnome_ext_site_cmd()
|
||||
)
|
||||
|
||||
def _qemu_tools_remote_cmd(self, tools, prod=False):
|
||||
"""Bloc d'installation des outils cochés, dans l'ordre du plus utile au
|
||||
plus lourd. Chacun se garde lui-même : aucun ne fait échouer les
|
||||
autres, ni l'installation d'ERPLibre."""
|
||||
blocks = {
|
||||
"gnome_ext": self._qemu_gnome_ext_remote_cmd,
|
||||
"pycharm": lambda: self._qemu_pycharm_remote_cmd(prod),
|
||||
"android": self._qemu_android_studio_remote_cmd,
|
||||
}
|
||||
return "".join(blocks[k]() for k in blocks if k in (tools or ()))
|
||||
|
||||
def _qemu_editor_pkg(self):
|
||||
"""Paquet de l'éditeur de l'hôte, à installer dans la VM.
|
||||
|
||||
L'éditeur atteint déjà la VM par deux chemins, tous deux posés par
|
||||
deploy_qemu.py : « core.editor » dans son ~/.gitconfig, et la ligne
|
||||
« éditer le serveur » du guide de connexion. Encore faut-il que le
|
||||
binaire y soit — les images cloud n'ont ni nano ni vim garantis, et
|
||||
certaines n'ont même pas vi. On l'ajoute donc aux outils d'amorçage, avec
|
||||
curl, git et make, là où les dépôts viennent d'être rafraîchis.
|
||||
|
||||
La table des éditeurs vit dans deploy_qemu.py : une seule autorité décide
|
||||
du paquet installé, de la commande affichée et de core.editor. Sans
|
||||
module importable, on n'installe rien plutôt que de deviner un nom."""
|
||||
try:
|
||||
mod = self._qemu_import_module()
|
||||
return mod.vm_editor(mod.invoking_home())[0]
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _qemu_editor_suffix(self):
|
||||
"""« vim » -> « vim » précédé d'une espace, rien du tout sinon.
|
||||
|
||||
La liste des outils d'amorçage est une chaîne shell entre apostrophes :
|
||||
y concaténer une chaîne vide sans précaution laisserait une espace en
|
||||
trop, inoffensive mais visible dans chaque log d'installation."""
|
||||
pkg = self._qemu_editor_pkg()
|
||||
return f" {pkg}" if pkg else ""
|
||||
|
||||
# mise ne publie de binaire que pour ces architectures : 46 assets à la
|
||||
# v2026.8.4, aucun s390x — son propre script d'installation refuse cette
|
||||
# plateforme. Ailleurs, le choix « mise » est sans objet et on reste sur
|
||||
|
|
@ -5038,6 +5491,7 @@ class TODO:
|
|||
desktop=False,
|
||||
python_provider="",
|
||||
app_store="deb",
|
||||
tools=(),
|
||||
):
|
||||
"""Script exécuté DANS la VM. `branch` à None n'installe QUE le bureau
|
||||
— le choix graphique ne dépend pas d'ERPLibre, et une VM peut être
|
||||
|
|
@ -5047,7 +5501,10 @@ class TODO:
|
|||
installe dans /opt/erplibre (au lieu de ~/git/erplibre) + service
|
||||
SELinux confiné. `desktop` : ajoute GNOME et son accès distant.
|
||||
`python_provider` : « mise » pour un CPython précompilé, sinon le
|
||||
comportement par défaut du dépôt (pyenv, qui compile)."""
|
||||
comportement par défaut du dépôt (pyenv, qui compile). `tools` : outils
|
||||
de développement cochés (PyCharm, Android Studio, extensions GNOME),
|
||||
posés APRÈS ERPLibre — PyCharm a besoin du venv du dépôt pour écrire la
|
||||
configuration du projet."""
|
||||
if not branch:
|
||||
# Bureau seul : ni clone ni make, mais on garde le prologue —
|
||||
# attente de cloud-init et coupure des mises à jour automatiques,
|
||||
|
|
@ -5059,6 +5516,7 @@ class TODO:
|
|||
+ self._qemu_cloud_init_wait()
|
||||
+ self._qemu_no_auto_upgrade(prod, app_store)
|
||||
+ self._qemu_desktop_remote_cmd(desktop, app_store)
|
||||
+ self._qemu_tools_remote_cmd(tools, prod)
|
||||
)
|
||||
if not final_cmd:
|
||||
final_cmd = f"make install_os && make {self.ERPLIBRE_ODOO_TARGET}"
|
||||
|
|
@ -5084,6 +5542,7 @@ class TODO:
|
|||
# apt pendant l'installation. En PROD on ne touche à rien : les
|
||||
# correctifs de sécurité automatiques doivent rester actifs.
|
||||
no_auto_upgrade = self._qemu_no_auto_upgrade(prod, app_store)
|
||||
tools_cmd = self._qemu_tools_remote_cmd(tools, prod)
|
||||
return (
|
||||
"set -e; " + self._qemu_cloud_init_wait()
|
||||
# Coupé AVANT les apt-get ci-dessous : sinon apt-daily peut reprendre
|
||||
|
|
@ -5103,7 +5562,14 @@ class TODO:
|
|||
# la VM soit la plus rapide possible (miroirs à jour / les plus
|
||||
# rapides), puis installe. Supporte apt (Debian/Ubuntu), dnf/yum
|
||||
# (Fedora) et pacman (Arch).
|
||||
"PKGS='curl git make'; "
|
||||
#
|
||||
# L'éditeur de l'hôte voyage avec eux : deploy_qemu.py a déjà écrit
|
||||
# « core.editor » dans le ~/.gitconfig de la VM et l'a nommé dans le
|
||||
# guide de connexion, mais aucune image cloud ne garantit vim ni
|
||||
# nano. Le poser ici plutôt que par cloud-init : les dépôts y sont
|
||||
# déjà rafraîchis, et une installation de paquet au premier boot
|
||||
# retarderait le démarrage sans laisser de trace dans le suivi.
|
||||
f"PKGS='curl git make{self._qemu_editor_suffix()}'; "
|
||||
"if command -v apt-get >/dev/null 2>&1; then "
|
||||
# Au 1er boot, cloud-init (install qemu-guest-agent) et/ou
|
||||
# apt-daily.service tiennent le verrou apt. IMPORTANT :
|
||||
|
|
@ -5167,6 +5633,16 @@ class TODO:
|
|||
'{ echo "Outil manquant apres installation: $t '
|
||||
'(reseau de la VM ?)"; exit 1; }; done; '
|
||||
+ self._qemu_mise_remote_cmd(python_provider)
|
||||
# Les outils AVANT le clone et le make, et l'ordre compte : c'est
|
||||
# PyCharm qui écrit le .idea du dépôt, en l'ouvrant une fois, et
|
||||
# c'est l'installation qui, ensuite, y lance
|
||||
# pycharm_configuration.py. Posés après, ils arrivaient trop tard
|
||||
# pour cette étape-là.
|
||||
#
|
||||
# Le code de sortie de la commande distante reste celui de
|
||||
# l'installation : chaque bloc d'outil se garde lui-même et rend 0,
|
||||
# donc aucun ne peut faire passer un make échoué pour un succès.
|
||||
+ tools_cmd
|
||||
# Clone : /opt/erplibre en PROD (racine, puis chown à l'utilisateur
|
||||
# pour que make/venv s'exécutent sans sudo), ~/git/erplibre en dev.
|
||||
+ (
|
||||
|
|
@ -5199,12 +5675,16 @@ class TODO:
|
|||
desktop=False,
|
||||
python_provider="",
|
||||
app_store="deb",
|
||||
desktop_tools=(),
|
||||
):
|
||||
"""Lance l'install ERPLibre en parallèle DÉTACHÉE sur les VM et ouvre
|
||||
le dashboard Textual. Quitter le dashboard n'arrête pas les installs.
|
||||
`ip_map` : IP déjà résolues (sinon on résout ici, EN PARALLÈLE).
|
||||
`final_cmd` : commande d'install selon le profil choisi.
|
||||
`prod` : install /opt/erplibre + service SELinux confiné."""
|
||||
`prod` : install /opt/erplibre + service SELinux confiné.
|
||||
`desktop_tools` : outils cochés pour tout le parc, filtrés machine par
|
||||
machine (Android Studio n'existe qu'en x86_64, les extensions GNOME
|
||||
n'ont pas de sens sous Cinnamon)."""
|
||||
from script.todo.qemu_install_monitor import (
|
||||
launch_installs,
|
||||
run_monitor,
|
||||
|
|
@ -5253,14 +5733,27 @@ class TODO:
|
|||
"version": v,
|
||||
"arch": a,
|
||||
}
|
||||
if desk_map or branch_map or cmd_map:
|
||||
# Les outils imposent une commande PAR VM même quand tout le
|
||||
# reste est commun : ils dépendent de l'architecture de la
|
||||
# machine et de sa saveur de bureau, que seule cette boucle
|
||||
# connaît.
|
||||
if desk_map or branch_map or cmd_map or desktop_tools:
|
||||
# Le bureau de CETTE VM : sa saveur propre si la carte en
|
||||
# donne une, sinon celle du parc. Prendre « rien » quand la
|
||||
# carte est vide privait de bureau toute VM dont seule la
|
||||
# branche ou le profil différait — la commande par défaut,
|
||||
# elle, l'a toujours porté.
|
||||
vm_desktop = desk_map.get(
|
||||
name, "" if desk_map else desktop
|
||||
)
|
||||
entry["remote_cmd"] = self._qemu_erplibre_remote_cmd(
|
||||
branch_map.get(name, branch_def),
|
||||
cmd_map.get(name, cmd_def),
|
||||
prod,
|
||||
desk_map.get(name, ""),
|
||||
vm_desktop,
|
||||
python_provider,
|
||||
app_store,
|
||||
self._qemu_tools_for(desktop_tools, a, vm_desktop),
|
||||
)
|
||||
vms.append(entry)
|
||||
else:
|
||||
|
|
@ -5306,10 +5799,12 @@ class TODO:
|
|||
desktop=False,
|
||||
python_provider="",
|
||||
app_store="deb",
|
||||
desktop_tools=(),
|
||||
):
|
||||
"""Clone ERPLibre (branche donnée) dans la VM puis exécute la commande
|
||||
d'install du profil choisi (streamé). `ip` : IP déjà résolue ;
|
||||
`final_cmd` : commande d'install ; `prod` : /opt + SELinux confiné."""
|
||||
`final_cmd` : commande d'install ; `prod` : /opt + SELinux confiné ;
|
||||
`desktop_tools` : outils de développement cochés."""
|
||||
if ip is None:
|
||||
ip = self._qemu_vm_ip(name)
|
||||
if not ip:
|
||||
|
|
@ -5327,7 +5822,17 @@ class TODO:
|
|||
)
|
||||
return
|
||||
remote = self._qemu_erplibre_remote_cmd(
|
||||
branch, final_cmd, prod, desktop, python_provider, app_store
|
||||
branch,
|
||||
final_cmd,
|
||||
prod,
|
||||
desktop,
|
||||
python_provider,
|
||||
app_store,
|
||||
# « or amd64 » comme _qemu_vm_meta : une architecture indéterminée
|
||||
# ne doit pas faire disparaître silencieusement Android Studio.
|
||||
self._qemu_tools_for(
|
||||
desktop_tools, self._qemu_vm_arch(name) or "amd64", desktop
|
||||
),
|
||||
)
|
||||
ssh_opts = (
|
||||
"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "
|
||||
|
|
@ -5762,6 +6267,18 @@ class TODO:
|
|||
print(
|
||||
f" {t('VM type:')} {t('Graphical (server + desktop):')} {label}"
|
||||
)
|
||||
tools = spec.get("desktop_tools") or ()
|
||||
if tools:
|
||||
# Les Go sont dits ici parce que c'est le dernier écran avant de
|
||||
# créer les disques : un IDE de plus, c'est un disque plus grand,
|
||||
# et cette page est celle qu'on relit pour s'en apercevoir.
|
||||
named = ", ".join(
|
||||
f"{t(self._QEMU_DESKTOP_TOOLS[k]['label'])} "
|
||||
f"(+{self._QEMU_DESKTOP_TOOLS[k]['disk_gb']} Go)"
|
||||
for k in tools
|
||||
if k in self._QEMU_DESKTOP_TOOLS
|
||||
)
|
||||
print(f" {t('Development tools:')} {named}")
|
||||
prov = spec.get("python_provider")
|
||||
if prov:
|
||||
print(f" {t('Python interpreter:')} {prov}")
|
||||
|
|
@ -5787,6 +6304,9 @@ class TODO:
|
|||
timezone=None,
|
||||
locale=None,
|
||||
desktop=False,
|
||||
prod=False,
|
||||
install_cmd="",
|
||||
desktop_tools=(),
|
||||
):
|
||||
"""Construit la commande deploy_qemu.py d'UNE VM (utilisée pour l'aperçu
|
||||
dry-run ET le déploiement réel)."""
|
||||
|
|
@ -5822,6 +6342,15 @@ class TODO:
|
|||
parts += ["--locale", locale]
|
||||
if desktop:
|
||||
parts.append("--desktop")
|
||||
# Guide affiché à la connexion SSH de la VM : dans la langue du menu, et
|
||||
# avec la section ERPLibre seulement là où ERPLibre sera installé — une
|
||||
# VM déployée nue n'annonce pas un dépôt qui n'existe pas.
|
||||
parts += ["--lang", get_lang()]
|
||||
if branch:
|
||||
parts += ["--erplibre-dir", self._qemu_guide_dir(prod)]
|
||||
target = self._qemu_make_target(install_cmd)
|
||||
if target:
|
||||
parts += ["--erplibre-make", target]
|
||||
extra = 0
|
||||
if branch:
|
||||
# ERPLibre dépasse le minimum : +5 Go de disque.
|
||||
|
|
@ -5830,6 +6359,11 @@ class TODO:
|
|||
# GNOME et ses dépendances pèsent autant qu'ERPLibre : sans cette
|
||||
# marge, le disque se remplit en pleine installation du bureau.
|
||||
extra += self.QEMU_DESKTOP_EXTRA_DISK_GB
|
||||
# Les IDE pèsent plus lourd que tout le reste : PyCharm et Android
|
||||
# Studio, c'est l'archive téléchargée PUIS son contenu déplié. Compté
|
||||
# ici plutôt qu'au petit bonheur, sinon l'installation se termine sur un
|
||||
# disque plein après une heure.
|
||||
extra += self._qemu_tools_disk_gb(desktop_tools, arch, desktop)
|
||||
if extra:
|
||||
bigger = self._parse_disk_gb(disk) + extra
|
||||
parts += ["--disk-size", f"{bigger}G"]
|
||||
|
|
@ -5858,7 +6392,17 @@ class TODO:
|
|||
locale=spec.get("locale"),
|
||||
# Le type suit la VM. Repli sur la valeur de spec pour la CLI,
|
||||
# qui ne pose la question qu'une fois pour tout le parc.
|
||||
desktop=bool(vm.get("desktop", spec.get("desktop"))),
|
||||
#
|
||||
# La SAVEUR, et non un booléen : les extensions GNOME n'ont pas de
|
||||
# sens sous Cinnamon, et c'est ici que se calcule la place disque
|
||||
# des outils. « --desktop » ne regarde que la vérité de la valeur,
|
||||
# une chaîne non vide lui va aussi bien.
|
||||
desktop=vm.get("desktop", spec.get("desktop")) or "",
|
||||
# Les deux servent au guide de connexion : où ERPLibre sera posé, et
|
||||
# quelle cible make le remettra à jour.
|
||||
prod=bool(install and install.get("prod")),
|
||||
install_cmd=(install or {}).get("cmd") or "",
|
||||
desktop_tools=spec.get("desktop_tools") or (),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------- #
|
||||
|
|
@ -6102,6 +6646,16 @@ class TODO:
|
|||
self._qemu_host_timezone()
|
||||
),
|
||||
"snap_distros": self.QEMU_SNAP_DISTROS,
|
||||
"desktop_tools": self._qemu_desktop_tool_choices(),
|
||||
"desktop_tool_disk": {
|
||||
k: v["disk_gb"] for k, v in self._QEMU_DESKTOP_TOOLS.items()
|
||||
},
|
||||
"desktop_tool_arches": {
|
||||
k: v["arches"] for k, v in self._QEMU_DESKTOP_TOOLS.items()
|
||||
},
|
||||
"desktop_tool_desktops": {
|
||||
k: v["desktops"] for k, v in self._QEMU_DESKTOP_TOOLS.items()
|
||||
},
|
||||
"desktop_suffixes": self._qemu_desktop_suffixes(),
|
||||
"desktops": [
|
||||
(k, v["label"]) for k, v in self._QEMU_DESKTOP.items()
|
||||
|
|
@ -6417,6 +6971,43 @@ class TODO:
|
|||
return self.QEMU_APP_STORES[int(answer) - 1][0]
|
||||
return "deb"
|
||||
|
||||
def _qemu_ask_desktop_tools(self, vms):
|
||||
"""Outils de développement des VM graphiques : liste à cocher.
|
||||
|
||||
Ne se pose QUE si au moins une VM porte un bureau — sur un serveur, un
|
||||
IDE graphique n'a rien pour s'afficher. La réponse vaut pour tout le
|
||||
parc et sera filtrée machine par machine.
|
||||
|
||||
Saisie par numéros séparés par des espaces ou des virgules, « tous »
|
||||
pour tout cocher, vide pour rien : trois questions oui/non de plus
|
||||
alourdiraient une séquence d'invites déjà longue."""
|
||||
graphical = [vm for vm in vms if vm.get("desktop")]
|
||||
if not graphical:
|
||||
return ()
|
||||
choices = self._qemu_desktop_tool_choices()
|
||||
print(f"\n{t('Development tools for the graphical VMs:')}")
|
||||
for i, (_key, label, hint) in enumerate(choices, 1):
|
||||
print(f" [{i}] {label} — {hint}")
|
||||
gb = ", ".join(
|
||||
f"{label} +{self._QEMU_DESKTOP_TOOLS[key]['disk_gb']} Go"
|
||||
for key, label, _hint in choices
|
||||
)
|
||||
print(f" {t('Disk needed:')} {gb}")
|
||||
answer = input(
|
||||
f"{t('Numbers separated by spaces, [all], blank = none:')} "
|
||||
).strip()
|
||||
if not answer:
|
||||
return ()
|
||||
if answer.lower() in ("all", "tous", "toutes", "*"):
|
||||
return tuple(key for key, _l, _h in choices)
|
||||
picked = []
|
||||
for token in answer.replace(",", " ").split():
|
||||
if token.isdigit() and 1 <= int(token) <= len(choices):
|
||||
key = choices[int(token) - 1][0]
|
||||
if key not in picked:
|
||||
picked.append(key)
|
||||
return tuple(picked)
|
||||
|
||||
def _qemu_ask_python_provider(self, arches):
|
||||
"""mise (CPython précompilé) ou pyenv (compilation).
|
||||
|
||||
|
|
@ -6513,6 +7104,7 @@ class TODO:
|
|||
_vm.setdefault("desktop", desktop)
|
||||
_vm["name"] = vm_name(_vm["name"], _vm.get("desktop"), suffixes)
|
||||
app_store = self._qemu_ask_app_store(vms)
|
||||
desktop_tools = self._qemu_ask_desktop_tools(vms)
|
||||
python_provider = self._qemu_ask_python_provider(
|
||||
[vm["arch"] for vm in vms]
|
||||
)
|
||||
|
|
@ -6593,6 +7185,7 @@ class TODO:
|
|||
"timezone": timezone,
|
||||
"locale": locale,
|
||||
"desktop": desktop,
|
||||
"desktop_tools": desktop_tools,
|
||||
"python_provider": python_provider,
|
||||
"app_store": app_store,
|
||||
"install": install,
|
||||
|
|
@ -6676,6 +7269,9 @@ class TODO:
|
|||
desktop = next((d for d in desktop_map.values() if d), "")
|
||||
python_provider = spec.get("python_provider") or ""
|
||||
app_store = spec.get("app_store") or "deb"
|
||||
# Outils de développement : cochés une fois pour tout le parc, puis
|
||||
# filtrés machine par machine (architecture, saveur de bureau).
|
||||
desktop_tools = tuple(spec.get("desktop_tools") or ())
|
||||
# Branche par VM : « » sur une VM veut dire « celle du formulaire ».
|
||||
branch_map = {
|
||||
vm["name"]: (vm.get("branch") or install_branch or "")
|
||||
|
|
@ -6767,6 +7363,7 @@ class TODO:
|
|||
desktop=desktop_map,
|
||||
python_provider=python_provider,
|
||||
app_store=app_store,
|
||||
desktop_tools=desktop_tools,
|
||||
)
|
||||
else:
|
||||
print(
|
||||
|
|
@ -6784,6 +7381,7 @@ class TODO:
|
|||
desktop=desktop_map.get(name, ""),
|
||||
python_provider=python_provider,
|
||||
app_store=app_store,
|
||||
desktop_tools=desktop_tools,
|
||||
)
|
||||
|
||||
# Sommaire TOTAL (déploiement + résolution IP + ssh_config + install
|
||||
|
|
|
|||
|
|
@ -7557,6 +7557,122 @@ TRANSLATIONS = {
|
|||
"en": "no recipient: nothing was sent",
|
||||
},
|
||||
"mail_err_send_refused": {"fr": "envoi refusé :", "en": "send refused:"},
|
||||
# QEMU — outils de développement des VM graphiques
|
||||
"Python IDE, ERPLibre project pre-configured": {
|
||||
"fr": "IDE Python, projet ERPLibre pré-configuré",
|
||||
"en": "Python IDE, ERPLibre project pre-configured",
|
||||
},
|
||||
"ERPLibre mobile development (x86_64 only)": {
|
||||
"fr": "développement mobile ERPLibre (x86_64 seulement)",
|
||||
"en": "ERPLibre mobile development (x86_64 only)",
|
||||
},
|
||||
"GNOME extensions": {
|
||||
"fr": "Extensions GNOME",
|
||||
"en": "GNOME extensions",
|
||||
},
|
||||
"suggested extensions + extension manager": {
|
||||
"fr": "extensions suggérées + gestionnaire d'extensions",
|
||||
"en": "suggested extensions + extension manager",
|
||||
},
|
||||
"Development tools for the graphical VMs:": {
|
||||
"fr": "Outils de développement des VM graphiques :",
|
||||
"en": "Development tools for the graphical VMs:",
|
||||
},
|
||||
"Development tools (graphical VMs):": {
|
||||
"fr": "Outils de développement (VM graphiques) :",
|
||||
"en": "Development tools (graphical VMs):",
|
||||
},
|
||||
"Development tools:": {
|
||||
"fr": "Outils de développement :",
|
||||
"en": "Development tools:",
|
||||
},
|
||||
"Disk needed:": {"fr": "Disque nécessaire :", "en": "Disk needed:"},
|
||||
"Numbers separated by spaces, [all], blank = none:": {
|
||||
"fr": "Numéros séparés par des espaces, [tous], vide = aucun :",
|
||||
"en": "Numbers separated by spaces, [all], blank = none:",
|
||||
},
|
||||
"No graphical VM: these tools are not installed.": {
|
||||
"fr": "Aucune VM graphique : ces outils ne seront pas installés.",
|
||||
"en": "No graphical VM: these tools are not installed.",
|
||||
},
|
||||
"Partly skipped (arch or desktop):": {
|
||||
"fr": "En partie écartés (architecture ou bureau) :",
|
||||
"en": "Partly skipped (arch or desktop):",
|
||||
},
|
||||
"Installing PyCharm (long)": {
|
||||
"fr": "Installation de PyCharm (long)",
|
||||
"en": "Installing PyCharm (long)",
|
||||
},
|
||||
"no JetBrains build for": {
|
||||
"fr": "aucune archive JetBrains pour",
|
||||
"en": "no JetBrains build for",
|
||||
},
|
||||
"PyCharm: project config skipped": {
|
||||
"fr": "PyCharm : configuration du projet non écrite",
|
||||
"en": "PyCharm: project config skipped",
|
||||
},
|
||||
"PyCharm installed:": {
|
||||
"fr": "PyCharm installé :",
|
||||
"en": "PyCharm installed:",
|
||||
},
|
||||
"PyCharm not installed (see above)": {
|
||||
"fr": "PyCharm non installé (voir ci-dessus)",
|
||||
"en": "PyCharm not installed (see above)",
|
||||
},
|
||||
"Installing Android Studio (long)": {
|
||||
"fr": "Installation d'Android Studio (long)",
|
||||
"en": "Installing Android Studio (long)",
|
||||
},
|
||||
"Android Studio: Google publishes x86_64 only": {
|
||||
"fr": "Android Studio : Google ne publie que du x86_64",
|
||||
"en": "Android Studio: Google publishes x86_64 only",
|
||||
},
|
||||
"Android Studio installed:": {
|
||||
"fr": "Android Studio installé :",
|
||||
"en": "Android Studio installed:",
|
||||
},
|
||||
"Android Studio not installed (see above)": {
|
||||
"fr": "Android Studio non installé (voir ci-dessus)",
|
||||
"en": "Android Studio not installed (see above)",
|
||||
},
|
||||
"no nested KVM: the emulator will not run": {
|
||||
"fr": "pas de KVM imbriqué : l'émulateur ne démarrera pas",
|
||||
"en": "no nested KVM: the emulator will not run",
|
||||
},
|
||||
"Suggested GNOME extensions": {
|
||||
"fr": "Extensions GNOME suggérées",
|
||||
"en": "Suggested GNOME extensions",
|
||||
},
|
||||
"not in the repos, skipped:": {
|
||||
"fr": "absent des dépôts, ignoré :",
|
||||
"en": "not in the repos, skipped:",
|
||||
},
|
||||
"Enable them from Extension Manager, or:": {
|
||||
"fr": "À activer depuis le gestionnaire d'extensions, ou :",
|
||||
"en": "Enable them from Extension Manager, or:",
|
||||
},
|
||||
"GNOME Shell not found, site extensions skipped": {
|
||||
"fr": "GNOME Shell introuvable, extensions du site ignorées",
|
||||
"en": "GNOME Shell not found, site extensions skipped",
|
||||
},
|
||||
"installed and enabled:": {
|
||||
"fr": "installée et activée :",
|
||||
"en": "installed and enabled:",
|
||||
},
|
||||
"not available for this GNOME, skipped:": {
|
||||
"fr": "indisponible pour ce GNOME, ignorée :",
|
||||
"en": "not available for this GNOME, skipped:",
|
||||
},
|
||||
"log out and back in to load them": {
|
||||
"fr": "se déconnecter puis se reconnecter pour les charger",
|
||||
"en": "log out and back in to load them",
|
||||
},
|
||||
"open the project once and close PyCharm; the .idea it writes is what the install configures": {
|
||||
"fr": "ouvrir le projet une fois puis fermer PyCharm : le .idea qu'il écrit est ce que l'installation configure",
|
||||
"en": "open the project once and close PyCharm; the .idea it writes is what the install configures",
|
||||
},
|
||||
"command": {"fr": "commande", "en": "command"},
|
||||
"project": {"fr": "projet", "en": "project"},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
272
test/test_qemu_desktop_tools.py
Normal file
272
test/test_qemu_desktop_tools.py
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
#!/usr/bin/env python3
|
||||
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
"""Outils de développement des VM graphiques : filtrage, disque, commande.
|
||||
|
||||
Ce qui se vérifie ici sans VM : qu'un outil demandé pour tout le parc n'atterrit
|
||||
que sur les machines qui peuvent le recevoir, que la place disque annoncée suit
|
||||
ce filtrage, et qu'un outil qui échoue ne fait pas tomber l'installation
|
||||
d'ERPLibre avec lui — celle-ci ayant duré une heure.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.argv = ["todo.py"]
|
||||
from script.todo.todo import TODO # noqa: E402
|
||||
|
||||
|
||||
class TestToolFiltering(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.todo = TODO.__new__(TODO)
|
||||
self.all = tuple(TODO._QEMU_DESKTOP_TOOLS)
|
||||
|
||||
def test_a_server_gets_no_graphical_tool(self):
|
||||
"""Un IDE sans bureau n'a rien pour s'afficher."""
|
||||
self.assertEqual([], self.todo._qemu_tools_for(self.all, "amd64", ""))
|
||||
|
||||
def test_android_studio_is_x86_64_only(self):
|
||||
"""Google ne publie aucune archive Linux aarch64 : toutes les variantes
|
||||
de l'URL rendent 404, et product-info.json ne déclare que Linux/amd64.
|
||||
"""
|
||||
self.assertIn(
|
||||
"android", self.todo._qemu_tools_for(self.all, "amd64", "gnome")
|
||||
)
|
||||
for arch in ("arm64", "s390x"):
|
||||
self.assertNotIn(
|
||||
"android",
|
||||
self.todo._qemu_tools_for(self.all, arch, "gnome"),
|
||||
arch,
|
||||
)
|
||||
|
||||
def test_pycharm_follows_jetbrains_two_architectures(self):
|
||||
for arch in ("amd64", "arm64"):
|
||||
self.assertIn(
|
||||
"pycharm",
|
||||
self.todo._qemu_tools_for(self.all, arch, "gnome"),
|
||||
arch,
|
||||
)
|
||||
self.assertNotIn(
|
||||
"pycharm", self.todo._qemu_tools_for(self.all, "s390x", "gnome")
|
||||
)
|
||||
|
||||
def test_gnome_extensions_only_under_gnome(self):
|
||||
self.assertIn(
|
||||
"gnome_ext", self.todo._qemu_tools_for(self.all, "amd64", "gnome")
|
||||
)
|
||||
self.assertNotIn(
|
||||
"gnome_ext",
|
||||
self.todo._qemu_tools_for(self.all, "amd64", "cinnamon"),
|
||||
)
|
||||
|
||||
def test_unknown_key_is_ignored(self):
|
||||
self.assertEqual(
|
||||
[], self.todo._qemu_tools_for(("nope",), "amd64", "gnome")
|
||||
)
|
||||
|
||||
|
||||
class TestToolDisk(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.todo = TODO.__new__(TODO)
|
||||
self.all = tuple(TODO._QEMU_DESKTOP_TOOLS)
|
||||
|
||||
def test_disk_follows_the_filtering(self):
|
||||
"""Une VM qui ne recevra pas Android Studio ne doit pas se voir gonfler
|
||||
son disque de ses 8 Go."""
|
||||
full = self.todo._qemu_tools_disk_gb(self.all, "amd64", "gnome")
|
||||
arm = self.todo._qemu_tools_disk_gb(self.all, "arm64", "gnome")
|
||||
self.assertEqual(
|
||||
full - arm, TODO._QEMU_DESKTOP_TOOLS["android"]["disk_gb"]
|
||||
)
|
||||
|
||||
def test_a_server_costs_nothing(self):
|
||||
self.assertEqual(
|
||||
0, self.todo._qemu_tools_disk_gb(self.all, "amd64", "")
|
||||
)
|
||||
|
||||
def test_the_deploy_command_grows_the_disk(self):
|
||||
"""Le disque demandé à deploy_qemu.py doit inclure les outils : c'est
|
||||
la seule valeur qui compte, celle du qcow2 réellement créé."""
|
||||
spec = {
|
||||
"ssh_key": "",
|
||||
"desktop": "gnome",
|
||||
"desktop_tools": self.all,
|
||||
"install": {
|
||||
"branch": "develop",
|
||||
"prod": False,
|
||||
"cmd": "make install_os && make install_odoo_18",
|
||||
"label": "x",
|
||||
"monitor": True,
|
||||
},
|
||||
}
|
||||
vm = {
|
||||
"distro": "ubuntu",
|
||||
"version": "24.04",
|
||||
"arch": "amd64",
|
||||
"name": "v",
|
||||
"ram": 3072,
|
||||
"vcpus": 4,
|
||||
"disk": "20G",
|
||||
"desktop": "gnome",
|
||||
}
|
||||
parts = self.todo._qemu_deploy_parts_for(vm, spec, dry_run=True)
|
||||
size = parts[parts.index("--disk-size") + 1]
|
||||
expected = (
|
||||
20
|
||||
+ TODO.ERPLIBRE_EXTRA_DISK_GB
|
||||
+ TODO.QEMU_DESKTOP_EXTRA_DISK_GB
|
||||
+ sum(s["disk_gb"] for s in TODO._QEMU_DESKTOP_TOOLS.values())
|
||||
)
|
||||
self.assertEqual(f"{expected}G", size)
|
||||
|
||||
|
||||
class TestToolRemoteCommand(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.todo = TODO.__new__(TODO)
|
||||
self.all = tuple(TODO._QEMU_DESKTOP_TOOLS)
|
||||
|
||||
def _sh_ok(self, script):
|
||||
"""Le shell accepte-t-il ce script ? « bash -n » ne l'exécute pas."""
|
||||
return subprocess.run(
|
||||
["bash", "-n"], input=script, text=True, capture_output=True
|
||||
)
|
||||
|
||||
def test_every_combination_is_valid_shell(self):
|
||||
combos = [
|
||||
(),
|
||||
("pycharm",),
|
||||
("android",),
|
||||
("gnome_ext",),
|
||||
self.all,
|
||||
]
|
||||
for tools in combos:
|
||||
script = self.todo._qemu_erplibre_remote_cmd(
|
||||
"develop", None, False, "gnome", "mise", "deb", tools
|
||||
)
|
||||
res = self._sh_ok(script)
|
||||
self.assertEqual(0, res.returncode, f"{tools} : {res.stderr}")
|
||||
|
||||
def test_tools_come_before_the_clone_and_the_make(self):
|
||||
"""L'ordre est ce qui fait marcher la configuration du projet : PyCharm
|
||||
écrit le .idea du dépôt en l'ouvrant une fois, et c'est l'installation
|
||||
qui, ENSUITE, y lance pycharm_configuration.py."""
|
||||
script = self.todo._qemu_erplibre_remote_cmd(
|
||||
"develop", None, False, "gnome", "", "deb", ("pycharm",)
|
||||
)
|
||||
self.assertLess(script.index("PyCharm"), script.index("git clone"))
|
||||
self.assertLess(
|
||||
script.index("PyCharm"), script.index("make install_os")
|
||||
)
|
||||
|
||||
def test_the_install_owns_the_project_configuration(self):
|
||||
"""update_env_version.pycharm_update() lance déjà le script, et sait se
|
||||
taire sans .idea. Doubler l'appel n'écrivait qu'une erreur dans le
|
||||
journal d'une VM neuve."""
|
||||
script = self.todo._qemu_erplibre_remote_cmd(
|
||||
"develop", None, False, "gnome", "", "deb", ("pycharm",)
|
||||
)
|
||||
self.assertNotIn("pycharm_configuration", script)
|
||||
|
||||
def test_a_failing_tool_never_masks_a_failing_install(self):
|
||||
"""Le code de sortie doit rester celui de l'installation : c'est lui que
|
||||
lit le tableau de bord pour dire ✅ ou ❌."""
|
||||
script = (
|
||||
"set -e\n"
|
||||
"curl() { return 7; }; sudo() { return 7; }\n"
|
||||
+ self.todo._qemu_tools_remote_cmd(("pycharm", "android"), False)
|
||||
+ "\nexit 3\n" # l'installation qui suit, en échec
|
||||
)
|
||||
res = subprocess.run(
|
||||
["bash", "-c", script], capture_output=True, text=True
|
||||
)
|
||||
self.assertEqual(3, res.returncode, res.stdout + res.stderr)
|
||||
|
||||
def test_a_failing_tool_still_returns_zero(self):
|
||||
"""Le bloc d'outils est lui-même gardé : PyCharm indisponible ne doit
|
||||
pas transformer une installation réussie en échec."""
|
||||
script = "set -e\n" + self.todo._qemu_tools_remote_cmd(self.all, False)
|
||||
# Tout ce qui pourrait réussir est neutralisé : ni réseau, ni sudo.
|
||||
stub = (
|
||||
"curl() { return 7; }; sudo() { return 7; }; tar() { return 7; }; "
|
||||
"export -f curl sudo tar 2>/dev/null || true\n"
|
||||
)
|
||||
res = subprocess.run(
|
||||
["bash", "-c", stub + script], capture_output=True, text=True
|
||||
)
|
||||
self.assertEqual(0, res.returncode, res.stderr)
|
||||
self.assertIn("PyCharm", res.stdout)
|
||||
|
||||
|
||||
class TestToolDiscoverability(unittest.TestCase):
|
||||
"""Ce qui est installé doit pouvoir être TROUVÉ. Vécu : Android Studio
|
||||
posé dans /opt, lanceur nommé « studio », et l'utilisateur conclut à un
|
||||
échec parce que « android-studio » ne répond pas."""
|
||||
|
||||
def setUp(self):
|
||||
self.todo = TODO.__new__(TODO)
|
||||
|
||||
def test_android_studio_answers_to_both_names(self):
|
||||
cmd = self.todo._qemu_android_studio_remote_cmd()
|
||||
self.assertIn("/usr/local/bin/studio;", cmd)
|
||||
self.assertIn("/usr/local/bin/android-studio;", cmd)
|
||||
|
||||
def test_the_log_says_where_it_landed(self):
|
||||
pycharm = self.todo._qemu_pycharm_remote_cmd()
|
||||
android = self.todo._qemu_android_studio_remote_cmd()
|
||||
self.assertIn("/opt/pycharm", pycharm)
|
||||
self.assertIn("/opt/android-studio", android)
|
||||
|
||||
def test_pycharm_says_what_creates_the_project(self):
|
||||
"""Le .idea n'existe qu'après une première ouverture de PyCharm : le
|
||||
journal le dit, plutôt que de laisser croire à un échec."""
|
||||
cmd = self.todo._qemu_pycharm_remote_cmd()
|
||||
self.assertIn(".idea", cmd)
|
||||
|
||||
|
||||
class TestGnomeSiteExtensions(unittest.TestCase):
|
||||
"""Extensions posées depuis extensions.gnome.org, par leur UUID."""
|
||||
|
||||
def setUp(self):
|
||||
self.todo = TODO.__new__(TODO)
|
||||
self.block = self.todo._qemu_gnome_ext_remote_cmd()
|
||||
|
||||
def test_the_three_requested_extensions_are_there(self):
|
||||
for uuid in (
|
||||
"gTile@vibou",
|
||||
"freon@UshakovVasilii_Github.yahoo.com",
|
||||
"tracker@aliakseiz.github.com",
|
||||
):
|
||||
self.assertIn(uuid, self.block, uuid)
|
||||
|
||||
def test_the_archive_follows_the_running_gnome(self):
|
||||
"""Le site sert une archive DIFFERENTE selon la version demandée —
|
||||
gTile v59 en GNOME 46, v62 en 48 : figer une URL poserait une archive
|
||||
faite pour une autre version."""
|
||||
self.assertIn("shell_version=$sv", self.block)
|
||||
self.assertIn("gnome-shell --version", self.block)
|
||||
|
||||
def test_valid_shell_and_never_fails_the_install(self):
|
||||
script = "set -e\n" + self.block
|
||||
self.assertEqual(
|
||||
0,
|
||||
subprocess.run(
|
||||
["bash", "-n"], input=script, text=True, capture_output=True
|
||||
).returncode,
|
||||
)
|
||||
# Sans gnome-shell dans le PATH, le bloc doit se taire proprement.
|
||||
res = subprocess.run(
|
||||
["bash", "-c", "PATH=/nonexistent; " + script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(0, res.returncode, res.stderr)
|
||||
|
||||
def test_a_session_bus_is_provided_for_dconf(self):
|
||||
"""Un « ssh hote commande » n'a pas de bus de session : sans lui,
|
||||
l'activation ne peut rien ecrire dans dconf."""
|
||||
self.assertIn("dbus-run-session", self.block)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in a new issue