[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
|
download an image, list VMs and show a VM IP address — the menu asks for the
|
||||||
parameters and builds the command for you.
|
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
|
## Main options
|
||||||
|
|
||||||
- `--distro` — `ubuntu` (default), `debian` or `fedora`.
|
- `--distro` — `ubuntu` (default), `debian` or `fedora`.
|
||||||
|
|
@ -279,9 +306,38 @@ parameters and builds the command for you.
|
||||||
- `--no-install-deps` — never auto-install dependencies.
|
- `--no-install-deps` — never auto-install dependencies.
|
||||||
- `--dry-run` — show the commands without executing anything.
|
- `--dry-run` — show the commands without executing anything.
|
||||||
- `--force` — overwrite the existing working qcow2 disk.
|
- `--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.
|
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] -->
|
<!-- [fr] -->
|
||||||
L'utilisateur par défaut est `erplibre` (modifiable avec `--user`).
|
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
|
l'IP d'une VM — le menu demande les paramètres et construit la commande pour
|
||||||
vous.
|
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
|
## Principales options
|
||||||
|
|
||||||
- `--distro` — `ubuntu` (défaut), `debian` ou `fedora`.
|
- `--distro` — `ubuntu` (défaut), `debian` ou `fedora`.
|
||||||
|
|
@ -314,9 +399,37 @@ vous.
|
||||||
- `--no-install-deps` — n'installe jamais les dépendances automatiquement.
|
- `--no-install-deps` — n'installe jamais les dépendances automatiquement.
|
||||||
- `--dry-run` — affiche les commandes sans rien exécuter.
|
- `--dry-run` — affiche les commandes sans rien exécuter.
|
||||||
- `--force` — écrase le disque de travail qcow2 existant.
|
- `--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.
|
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] -->
|
<!-- [en] -->
|
||||||
## Managing VMs
|
## 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
|
l'IP d'une VM — le menu demande les paramètres et construit la commande pour
|
||||||
vous.
|
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
|
## Principales options
|
||||||
|
|
||||||
- `--distro` — `ubuntu` (défaut), `debian` ou `fedora`.
|
- `--distro` — `ubuntu` (défaut), `debian` ou `fedora`.
|
||||||
|
|
@ -166,9 +195,37 @@ vous.
|
||||||
- `--no-install-deps` — n'installe jamais les dépendances automatiquement.
|
- `--no-install-deps` — n'installe jamais les dépendances automatiquement.
|
||||||
- `--dry-run` — affiche les commandes sans rien exécuter.
|
- `--dry-run` — affiche les commandes sans rien exécuter.
|
||||||
- `--force` — écrase le disque de travail qcow2 existant.
|
- `--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.
|
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
|
## Gestion des VM
|
||||||
|
|
||||||
Lister, arrêter et supprimer les VM (le disque qcow2 sous
|
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
|
download an image, list VMs and show a VM IP address — the menu asks for the
|
||||||
parameters and builds the command for you.
|
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
|
## Main options
|
||||||
|
|
||||||
- `--distro` — `ubuntu` (default), `debian` or `fedora`.
|
- `--distro` — `ubuntu` (default), `debian` or `fedora`.
|
||||||
|
|
@ -153,9 +180,38 @@ parameters and builds the command for you.
|
||||||
- `--no-install-deps` — never auto-install dependencies.
|
- `--no-install-deps` — never auto-install dependencies.
|
||||||
- `--dry-run` — show the commands without executing anything.
|
- `--dry-run` — show the commands without executing anything.
|
||||||
- `--force` — overwrite the existing working qcow2 disk.
|
- `--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.
|
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
|
## Managing VMs
|
||||||
|
|
||||||
List, stop and remove VMs (the qcow2 disk under `/var/lib/libvirt/images`
|
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"],
|
"ssh_key": form["ssh_key"],
|
||||||
"timezone": form.get("timezone", ""),
|
"timezone": form.get("timezone", ""),
|
||||||
"desktop": form.get("desktop", ""),
|
"desktop": form.get("desktop", ""),
|
||||||
|
"desktop_tools": tuple(form.get("desktop_tools") or ()),
|
||||||
"python_provider": form.get("python_provider", ""),
|
"python_provider": form.get("python_provider", ""),
|
||||||
"app_store": form.get("app_store", "deb"),
|
"app_store": form.get("app_store", "deb"),
|
||||||
"install": form["install"],
|
"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
|
# {clé de saveur: suffixe de nom}, fourni par todo.py qui décrit les
|
||||||
# saveurs — on ne le redéfinit pas ici.
|
# saveurs — on ne le redéfinit pas ici.
|
||||||
desktop_suffixes = dict(ctx.get("desktop_suffixes") or {})
|
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.
|
# Architectures pour lesquelles mise publie un binaire.
|
||||||
mise_arches = set(ctx.get("mise_arches") or ())
|
mise_arches = set(ctx.get("mise_arches") or ())
|
||||||
# [(clé, libellé)] des magasins d'applications, et les distributions qui
|
# [(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):
|
for i, (_k, label) in enumerate(app_stores):
|
||||||
yield RadioButton(label, value=i == 0)
|
yield RadioButton(label, value=i == 0)
|
||||||
yield Static("", id="storewarn")
|
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 Static("ERPLibre", classes="grouptitle")
|
||||||
yield Checkbox(
|
yield Checkbox(
|
||||||
t("Install ERPLibre"),
|
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
|
# 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
|
# seule : un supplément commun mentait dès que les types
|
||||||
# différaient d'une machine à l'autre.
|
# différaient d'une machine à l'autre.
|
||||||
|
tools = self._desktop_tools()
|
||||||
for row in self.rows:
|
for row in self.rows:
|
||||||
if row["vm"].get("desktop"):
|
if row["vm"].get("desktop"):
|
||||||
row["disk_gb"] += desktop_disk
|
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,
|
# Le plan doit MONTRER qu'une VM a été personnalisée : sans marque,
|
||||||
# deux lignes aux ressources différentes n'ont aucune explication à
|
# deux lignes aux ressources différentes n'ont aucune explication à
|
||||||
# l'écran, et la surcharge est oubliée à la relecture. Le drapeau
|
# 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_plan()
|
||||||
self._render_mise()
|
self._render_mise()
|
||||||
self._render_store()
|
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):
|
def _render_mise(self):
|
||||||
"""Grise le choix quand aucune VM retenue n'est servie par mise,
|
"""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
|
self._recompute() # le disque annoncé inclut le +5 G ERPLibre
|
||||||
elif event.checkbox.id == "f_par_all":
|
elif event.checkbox.id == "f_par_all":
|
||||||
self.query_one("#f_par", Select).disabled = event.value
|
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 ---------------------------------------------------- #
|
# -- actions ---------------------------------------------------- #
|
||||||
def action_select_all(self) -> None:
|
def action_select_all(self) -> None:
|
||||||
|
|
@ -1734,6 +1835,7 @@ def run_deploy_form(ctx, run_app: bool = True):
|
||||||
or ctx.get("timezone")
|
or ctx.get("timezone")
|
||||||
or "",
|
or "",
|
||||||
"desktop": self._default_desktop(),
|
"desktop": self._default_desktop(),
|
||||||
|
"desktop_tools": self._desktop_tools(),
|
||||||
"python_provider": self._python_provider(),
|
"python_provider": self._python_provider(),
|
||||||
"app_store": self._app_store(),
|
"app_store": self._app_store(),
|
||||||
"install": install,
|
"install": install,
|
||||||
|
|
|
||||||
|
|
@ -4508,6 +4508,30 @@ class TODO:
|
||||||
~/git/erplibre (dev)."""
|
~/git/erplibre (dev)."""
|
||||||
return "/opt/erplibre" if prod else "$HOME/git/erplibre"
|
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):
|
def _qemu_odoo_service_cmd(self, prod=False):
|
||||||
"""Snippet shell (exécuté dans la VM) qui installe ERPLibre/Odoo comme
|
"""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.
|
service systemd puis l'active. N'est ajouté QUE pour les profils Odoo.
|
||||||
|
|
@ -4992,6 +5016,435 @@ class TODO:
|
||||||
+ "fi; "
|
+ "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
|
# mise ne publie de binaire que pour ces architectures : 46 assets à la
|
||||||
# v2026.8.4, aucun s390x — son propre script d'installation refuse cette
|
# v2026.8.4, aucun s390x — son propre script d'installation refuse cette
|
||||||
# plateforme. Ailleurs, le choix « mise » est sans objet et on reste sur
|
# plateforme. Ailleurs, le choix « mise » est sans objet et on reste sur
|
||||||
|
|
@ -5038,6 +5491,7 @@ class TODO:
|
||||||
desktop=False,
|
desktop=False,
|
||||||
python_provider="",
|
python_provider="",
|
||||||
app_store="deb",
|
app_store="deb",
|
||||||
|
tools=(),
|
||||||
):
|
):
|
||||||
"""Script exécuté DANS la VM. `branch` à None n'installe QUE le bureau
|
"""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
|
— 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
|
installe dans /opt/erplibre (au lieu de ~/git/erplibre) + service
|
||||||
SELinux confiné. `desktop` : ajoute GNOME et son accès distant.
|
SELinux confiné. `desktop` : ajoute GNOME et son accès distant.
|
||||||
`python_provider` : « mise » pour un CPython précompilé, sinon le
|
`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:
|
if not branch:
|
||||||
# Bureau seul : ni clone ni make, mais on garde le prologue —
|
# Bureau seul : ni clone ni make, mais on garde le prologue —
|
||||||
# attente de cloud-init et coupure des mises à jour automatiques,
|
# attente de cloud-init et coupure des mises à jour automatiques,
|
||||||
|
|
@ -5059,6 +5516,7 @@ class TODO:
|
||||||
+ self._qemu_cloud_init_wait()
|
+ self._qemu_cloud_init_wait()
|
||||||
+ self._qemu_no_auto_upgrade(prod, app_store)
|
+ self._qemu_no_auto_upgrade(prod, app_store)
|
||||||
+ self._qemu_desktop_remote_cmd(desktop, app_store)
|
+ self._qemu_desktop_remote_cmd(desktop, app_store)
|
||||||
|
+ self._qemu_tools_remote_cmd(tools, prod)
|
||||||
)
|
)
|
||||||
if not final_cmd:
|
if not final_cmd:
|
||||||
final_cmd = f"make install_os && make {self.ERPLIBRE_ODOO_TARGET}"
|
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
|
# apt pendant l'installation. En PROD on ne touche à rien : les
|
||||||
# correctifs de sécurité automatiques doivent rester actifs.
|
# correctifs de sécurité automatiques doivent rester actifs.
|
||||||
no_auto_upgrade = self._qemu_no_auto_upgrade(prod, app_store)
|
no_auto_upgrade = self._qemu_no_auto_upgrade(prod, app_store)
|
||||||
|
tools_cmd = self._qemu_tools_remote_cmd(tools, prod)
|
||||||
return (
|
return (
|
||||||
"set -e; " + self._qemu_cloud_init_wait()
|
"set -e; " + self._qemu_cloud_init_wait()
|
||||||
# Coupé AVANT les apt-get ci-dessous : sinon apt-daily peut reprendre
|
# 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
|
# la VM soit la plus rapide possible (miroirs à jour / les plus
|
||||||
# rapides), puis installe. Supporte apt (Debian/Ubuntu), dnf/yum
|
# rapides), puis installe. Supporte apt (Debian/Ubuntu), dnf/yum
|
||||||
# (Fedora) et pacman (Arch).
|
# (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 "
|
"if command -v apt-get >/dev/null 2>&1; then "
|
||||||
# Au 1er boot, cloud-init (install qemu-guest-agent) et/ou
|
# Au 1er boot, cloud-init (install qemu-guest-agent) et/ou
|
||||||
# apt-daily.service tiennent le verrou apt. IMPORTANT :
|
# apt-daily.service tiennent le verrou apt. IMPORTANT :
|
||||||
|
|
@ -5167,6 +5633,16 @@ class TODO:
|
||||||
'{ echo "Outil manquant apres installation: $t '
|
'{ echo "Outil manquant apres installation: $t '
|
||||||
'(reseau de la VM ?)"; exit 1; }; done; '
|
'(reseau de la VM ?)"; exit 1; }; done; '
|
||||||
+ self._qemu_mise_remote_cmd(python_provider)
|
+ 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
|
# Clone : /opt/erplibre en PROD (racine, puis chown à l'utilisateur
|
||||||
# pour que make/venv s'exécutent sans sudo), ~/git/erplibre en dev.
|
# pour que make/venv s'exécutent sans sudo), ~/git/erplibre en dev.
|
||||||
+ (
|
+ (
|
||||||
|
|
@ -5199,12 +5675,16 @@ class TODO:
|
||||||
desktop=False,
|
desktop=False,
|
||||||
python_provider="",
|
python_provider="",
|
||||||
app_store="deb",
|
app_store="deb",
|
||||||
|
desktop_tools=(),
|
||||||
):
|
):
|
||||||
"""Lance l'install ERPLibre en parallèle DÉTACHÉE sur les VM et ouvre
|
"""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.
|
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).
|
`ip_map` : IP déjà résolues (sinon on résout ici, EN PARALLÈLE).
|
||||||
`final_cmd` : commande d'install selon le profil choisi.
|
`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 (
|
from script.todo.qemu_install_monitor import (
|
||||||
launch_installs,
|
launch_installs,
|
||||||
run_monitor,
|
run_monitor,
|
||||||
|
|
@ -5253,14 +5733,27 @@ class TODO:
|
||||||
"version": v,
|
"version": v,
|
||||||
"arch": a,
|
"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(
|
entry["remote_cmd"] = self._qemu_erplibre_remote_cmd(
|
||||||
branch_map.get(name, branch_def),
|
branch_map.get(name, branch_def),
|
||||||
cmd_map.get(name, cmd_def),
|
cmd_map.get(name, cmd_def),
|
||||||
prod,
|
prod,
|
||||||
desk_map.get(name, ""),
|
vm_desktop,
|
||||||
python_provider,
|
python_provider,
|
||||||
app_store,
|
app_store,
|
||||||
|
self._qemu_tools_for(desktop_tools, a, vm_desktop),
|
||||||
)
|
)
|
||||||
vms.append(entry)
|
vms.append(entry)
|
||||||
else:
|
else:
|
||||||
|
|
@ -5306,10 +5799,12 @@ class TODO:
|
||||||
desktop=False,
|
desktop=False,
|
||||||
python_provider="",
|
python_provider="",
|
||||||
app_store="deb",
|
app_store="deb",
|
||||||
|
desktop_tools=(),
|
||||||
):
|
):
|
||||||
"""Clone ERPLibre (branche donnée) dans la VM puis exécute la commande
|
"""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 ;
|
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:
|
if ip is None:
|
||||||
ip = self._qemu_vm_ip(name)
|
ip = self._qemu_vm_ip(name)
|
||||||
if not ip:
|
if not ip:
|
||||||
|
|
@ -5327,7 +5822,17 @@ class TODO:
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
remote = self._qemu_erplibre_remote_cmd(
|
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 = (
|
ssh_opts = (
|
||||||
"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "
|
"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "
|
||||||
|
|
@ -5762,6 +6267,18 @@ class TODO:
|
||||||
print(
|
print(
|
||||||
f" {t('VM type:')} {t('Graphical (server + desktop):')} {label}"
|
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")
|
prov = spec.get("python_provider")
|
||||||
if prov:
|
if prov:
|
||||||
print(f" {t('Python interpreter:')} {prov}")
|
print(f" {t('Python interpreter:')} {prov}")
|
||||||
|
|
@ -5787,6 +6304,9 @@ class TODO:
|
||||||
timezone=None,
|
timezone=None,
|
||||||
locale=None,
|
locale=None,
|
||||||
desktop=False,
|
desktop=False,
|
||||||
|
prod=False,
|
||||||
|
install_cmd="",
|
||||||
|
desktop_tools=(),
|
||||||
):
|
):
|
||||||
"""Construit la commande deploy_qemu.py d'UNE VM (utilisée pour l'aperçu
|
"""Construit la commande deploy_qemu.py d'UNE VM (utilisée pour l'aperçu
|
||||||
dry-run ET le déploiement réel)."""
|
dry-run ET le déploiement réel)."""
|
||||||
|
|
@ -5822,6 +6342,15 @@ class TODO:
|
||||||
parts += ["--locale", locale]
|
parts += ["--locale", locale]
|
||||||
if desktop:
|
if desktop:
|
||||||
parts.append("--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
|
extra = 0
|
||||||
if branch:
|
if branch:
|
||||||
# ERPLibre dépasse le minimum : +5 Go de disque.
|
# 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
|
# GNOME et ses dépendances pèsent autant qu'ERPLibre : sans cette
|
||||||
# marge, le disque se remplit en pleine installation du bureau.
|
# marge, le disque se remplit en pleine installation du bureau.
|
||||||
extra += self.QEMU_DESKTOP_EXTRA_DISK_GB
|
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:
|
if extra:
|
||||||
bigger = self._parse_disk_gb(disk) + extra
|
bigger = self._parse_disk_gb(disk) + extra
|
||||||
parts += ["--disk-size", f"{bigger}G"]
|
parts += ["--disk-size", f"{bigger}G"]
|
||||||
|
|
@ -5858,7 +6392,17 @@ class TODO:
|
||||||
locale=spec.get("locale"),
|
locale=spec.get("locale"),
|
||||||
# Le type suit la VM. Repli sur la valeur de spec pour la CLI,
|
# 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.
|
# 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()
|
self._qemu_host_timezone()
|
||||||
),
|
),
|
||||||
"snap_distros": self.QEMU_SNAP_DISTROS,
|
"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(),
|
"desktop_suffixes": self._qemu_desktop_suffixes(),
|
||||||
"desktops": [
|
"desktops": [
|
||||||
(k, v["label"]) for k, v in self._QEMU_DESKTOP.items()
|
(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 self.QEMU_APP_STORES[int(answer) - 1][0]
|
||||||
return "deb"
|
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):
|
def _qemu_ask_python_provider(self, arches):
|
||||||
"""mise (CPython précompilé) ou pyenv (compilation).
|
"""mise (CPython précompilé) ou pyenv (compilation).
|
||||||
|
|
||||||
|
|
@ -6513,6 +7104,7 @@ class TODO:
|
||||||
_vm.setdefault("desktop", desktop)
|
_vm.setdefault("desktop", desktop)
|
||||||
_vm["name"] = vm_name(_vm["name"], _vm.get("desktop"), suffixes)
|
_vm["name"] = vm_name(_vm["name"], _vm.get("desktop"), suffixes)
|
||||||
app_store = self._qemu_ask_app_store(vms)
|
app_store = self._qemu_ask_app_store(vms)
|
||||||
|
desktop_tools = self._qemu_ask_desktop_tools(vms)
|
||||||
python_provider = self._qemu_ask_python_provider(
|
python_provider = self._qemu_ask_python_provider(
|
||||||
[vm["arch"] for vm in vms]
|
[vm["arch"] for vm in vms]
|
||||||
)
|
)
|
||||||
|
|
@ -6593,6 +7185,7 @@ class TODO:
|
||||||
"timezone": timezone,
|
"timezone": timezone,
|
||||||
"locale": locale,
|
"locale": locale,
|
||||||
"desktop": desktop,
|
"desktop": desktop,
|
||||||
|
"desktop_tools": desktop_tools,
|
||||||
"python_provider": python_provider,
|
"python_provider": python_provider,
|
||||||
"app_store": app_store,
|
"app_store": app_store,
|
||||||
"install": install,
|
"install": install,
|
||||||
|
|
@ -6676,6 +7269,9 @@ class TODO:
|
||||||
desktop = next((d for d in desktop_map.values() if d), "")
|
desktop = next((d for d in desktop_map.values() if d), "")
|
||||||
python_provider = spec.get("python_provider") or ""
|
python_provider = spec.get("python_provider") or ""
|
||||||
app_store = spec.get("app_store") or "deb"
|
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 ».
|
# Branche par VM : « » sur une VM veut dire « celle du formulaire ».
|
||||||
branch_map = {
|
branch_map = {
|
||||||
vm["name"]: (vm.get("branch") or install_branch or "")
|
vm["name"]: (vm.get("branch") or install_branch or "")
|
||||||
|
|
@ -6767,6 +7363,7 @@ class TODO:
|
||||||
desktop=desktop_map,
|
desktop=desktop_map,
|
||||||
python_provider=python_provider,
|
python_provider=python_provider,
|
||||||
app_store=app_store,
|
app_store=app_store,
|
||||||
|
desktop_tools=desktop_tools,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
|
|
@ -6784,6 +7381,7 @@ class TODO:
|
||||||
desktop=desktop_map.get(name, ""),
|
desktop=desktop_map.get(name, ""),
|
||||||
python_provider=python_provider,
|
python_provider=python_provider,
|
||||||
app_store=app_store,
|
app_store=app_store,
|
||||||
|
desktop_tools=desktop_tools,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sommaire TOTAL (déploiement + résolution IP + ssh_config + install
|
# Sommaire TOTAL (déploiement + résolution IP + ssh_config + install
|
||||||
|
|
|
||||||
|
|
@ -7557,6 +7557,122 @@ TRANSLATIONS = {
|
||||||
"en": "no recipient: nothing was sent",
|
"en": "no recipient: nothing was sent",
|
||||||
},
|
},
|
||||||
"mail_err_send_refused": {"fr": "envoi refusé :", "en": "send refused:"},
|
"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