Dans les articles précédents de cette série consacrée à Eris Linux, nous avons commencé par examiner les principes et les avantages d’Eris. Nous avons ensuite étudié comment installer Eris Linux sur une carte standard (un Raspberry Pi 5) puis comment déployer depuis le Device Manager des containers de test sur notre équipement.
Nous allons voir dans cet article comment développer du code embarqué dans un container, et comment le déployer sur notre carte cible.
Au programme : services Linux, accès matériel, C/C++, Qt, Rust, Python et génération multiarchitecture avec Docker Buildx
Pour comprendre comment créer du code pour Eris Linux, je vous propose de nous appuyer sur les exemples contenus dans le repository Git suivant : https://github.com/Eris-Linux/Eris-Linux-Containers.
[~]$ git clone https://github.com/Eris-Linux/Eris-Linux-Containers
Cloning into 'Eris-Linux-Containers'...
remote: Enumerating objects: 183, done.
remote: Counting objects: 100% (183/183), done.
remote: Compressing objects: 100% (118/118), done.
remote: Total 183 (delta 79), reused 164 (delta 60), pack-reused 0 (from 0)
Receiving objects: 100% (183/183), 95.24 KiB | 2.51 MiB/s, done.
Resolving deltas: 100% (79/79), done.
[~]$ cd Eris-Linux-Containers/
La version 1.0.0 de ce repository contient 11 exemples successifs, couvrant un maximum de besoins classiques pour les systèmes embarqués :
- Mise en œuvre de services classiques de Linux (serveurs SSH et NGINX) avec des configurations personnalisées :
01-ssh-serveret02-api-test-web; - Scripts shell accédant à des ressources matérielles :
03-led-trigger-setupet04-turns-leds-off; - Programme C compilé avec éventuellement accès à la bibliothèque d’API d’Eris Linux :
05-hello-world-in-cet06-api-test-tcp; - Application graphique utilisant la bibliothèque Qt :
07-api-test-graphical; - Programme Rust compilé, avec différents types de containers :
08-hello-world-in-rust,09-hello-rust-distrolesset10-hello-rust-minimal; - Application en Python :
11-hello-world-in-python.
Les premiers exemples correspondent aux containers de démonstration que nous avons vus dans l’article précédent, mais des exemples supplémentaires sont présents. Il est probable que d’autres exemples seront ajoutés dans l’avenir et je compléterai cet article au besoin.
Table des matières
- 1 – Installation de services Linux
- 2 – Scripts shell accédant au matériel
- 3 – Cross-compilation
- 4 – Programmes en Rust
- 5 – Exemple en Python
- Conclusion
1 – Installation de services Linux
Nous allons commencer par l’installation d’un simple service classique prédéfini ne nécessitant pas d’ajout de code supplémentaire. Nous allons examiner la production de l’image, son enregistrement sur le serveur Eris Linux et son déploiement sur des cartes cibles. Une bonne partie de ces étapes se retrouvera à l’identique pour les autres exemples.
1.1 – Serveur SSH
1.1.1 – Préparation de l’image
Dans le dossier 01-ssh-server se trouve un simple fichier Dockerfile (accompagné bien sûr du README.md habituel) qui installe dans l’image de container un serveur openssh, génère une clé pour le serveur, et affine son fichier de configuration. Ce même Dockerfile crée un compte utilisateur nommé eris avec le mot de passe linux et fixe le mot de passe root pour l’utilisateur root du container. En voici une version légèrement simplifiée, je vous encourage à consulter le fichier d’origine.
FROM alpine:latest
RUN apk update \
&& apk add --no-cache openssh \
&& ssh-keygen -A \
&& echo "PermitRootLogin yes" >> /etc/ssh/sshd_config \
&& echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config \
&& adduser -D eris \
&& echo "root:root" | chpasswd \
&& echo "eris:linux" | chpasswd
EXPOSE 22
CMD [ "/usr/sbin/sshd", "-D" ]
La simplicité de ce Dockerfile vous permettra de l’adapter à vos besoins pour installer tout autre service Linux. Pour créer le container, il suffit d’appeler le script prévu à cet effet en suivant la ligne de commande indiquée dans le fichier README.md :
$ ls
01-ssh-server 04-turn-leds-off 07-api-test-graphical 10-hello-rust-minimal README.md
02-api-test-web 05-hello-world-in-c 08-hello-world-in-rust 11-hello-world-in-python create-container
03-led-trigger-setup 06-api-test-tcp 09-hello-rust-distroless LICENSE
$ ./create-container arm64 ./01-ssh-server ssh-server
[...]
#5 5.150 chpasswd: password for 'root' changed
#5 5.190 chpasswd: password for 'eris' changed
#5 DONE 5.2s
#6 exporting to client tarball
#6 sending tarball 0.1s done
#6 DONE 0.1s
/home/cpb/Eris-Linux-Containers
$ ls
01-ssh-server 04-turn-leds-off 07-api-test-graphical 10-hello-rust-minimal README.md
02-api-test-web 05-hello-world-in-c 08-hello-world-in-rust 11-hello-world-in-python create-container
03-led-trigger-setup 06-api-test-tcp 09-hello-rust-distroless LICENSE ssh-server.tar.bz2
$
Nous voyons qu’un fichier ssh-server.tar.bz2 est apparu, c’est l’image du container que nous allons ensuite envoyer vers le Device Manager.
1.1.2 – Enregistrement de l’image
L’image au format .tar.bz2 que nous avons produite peut être stockée sur le serveur d’Eris Linux puis déployée vers un groupe d’équipements. Pour cela, il faut se connecter sur le Device Manager à l’adresse https://www.eris-linux.net/ puis se rendre sur l’onglet « My containers » et cliquer sur le bouton « Upload a container » .
Dans la fenêtre qui s’ouvre, cliquez sur le bouton « Browse » et naviguez jusqu’au fichier ssh-server.tar.bz2 que nous venons de produire.
Vous pouvez choisir de protéger votre image de container par un mot de passe. Le fichier sera alors chiffré avant d’être enregistré sur le serveur Eris Linux. Le mot de passe ne sera pas stocké. Lorsque vous choisirez d’installer cette image de container sur un groupe d’équipements il faudra re-saisir le mot de passe.
Une fois le fichier d’image sélectionné, cliquez sur « Upload » .
De retour dans l’onglet « My containers » sélectionnez votre container nouvellement chargé et cliquez sur « Setup » pour le configurer.
Remplissez les champs :
- « Container name » : Choisissez un nom explicite qu’il vous sera facile d’identifier. Pendant la mise au point d’un projet embarqué, le nombre de containers de test augmente rapidement, il est important de les reconnaître facilement.
- « Container version » : Remplir et maintenir ce champ vous permet de conserver plusieurs versions du même container au cours des essais successifs.
- « Compatible boards » : Un container est compilé pour une carte donnée. Si votre container est prévu pour une gamme de différentes cartes, téléchargez autant d’exemplaires que de types de cartes concernées et jouez sur « Container name » pour les distinguer.
- « Graphical display » : Si votre container est prévu pour fonctionner sur des cartes disposant d’écrans graphiques, cochez cette case.
- « Privileged container » : Un container privilégié a accès à toutes les entrées des répertoires
/devet/sysde votre équipement. Il peut ainsi communiquer directement avec le matériel sans passer par l’API Eris Linux. Nous en verrons un exemple ci-dessous. - « Exported ports » : Si votre image propose des connexions réseau depuis l’extérieur du container, indiquez-les ici sous la forme «
<ext>:<int>/<proto>» avec<ext>le numéro du port à utiliser à l’extérieur du container,<int>le numéro du port interne au container et<proto>le protocole (tcpouudp, avectcppar défaut). - « Comments« : un champ de commentaires libres concernant l’image de container.
Pour le champ « Exported ports » j’ai indiqué ici « 12345:22/tcp » ainsi lorsqu’on se connectera sur le port TCP 12345 de l’équipement, la communication sera établie avec le port 22 à l’intérieur du container (le serveur SSH).
Une fois la fenêtre de « Setup » refermée, les informations sont affichées pour rappel.
1.1.3 – Déploiement du container
Le déploiement du container sur un groupe d’équipements se déroule comme pour les containers de démonstrations de l’article précédent. On se rend sur l’onglet « My devices » et on clique sur un slot libre du groupe qui nous intéresse.
Dans la liste des images de containers, la nouvelle apparaît juste après la liste des containers de démonstration. Comme nous l’avions vu dans l’article précédent, il est possible de masquer les containers de démonstration pour un groupe d’équipements.
Au bout de quelques instants, le container est indiqué comme « Running » en bas à droite de la fenêtre.
1.1.4 – Vérification du fonctionnement
On peut tester la connexion en appelant un client ssh en lui indiquant le numéro de port 12345 précédé de l’option -p et l’adresse IP indiquée dans le quart inférieur droit de l’écran :
$ ssh -p 12345 eris@192.168.3.36
The authenticity of host '[192.168.3.36]:12345 ([192.168.3.36]:12345)' can't be established.
ED25519 key fingerprint is: SHA256:vQAhsmy6cRlfUkjCq4t49njKXpzyw97qeuzk7CLC7YI
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[192.168.3.36]:12345' (ED25519) to the list of known hosts.
eris@192.168.3.36's password: linux
Welcome to Alpine!
The Alpine Wiki contains a large amount of how-to guides and general
information about administrating Alpine systems.
See <https://wiki.alpinelinux.org/>.
You can setup the system with the command: setup-alpine
You may change this message by editing /etc/motd.
2cd5e9876032:~$ ps aux
PID USER TIME COMMAND
1 root 0:00 /sbin/docker-init -- /usr/sbin/sshd -D
7 root 0:00 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
8 root 0:00 sshd-session: eris [priv]
10 eris 0:00 sshd-session: eris@pts/1
11 eris 0:00 -sh
12 eris 0:00 ps aux
2cd5e9876032:~$
Nous pouvons alors passer des commandes comme nous l’avons vu dans l’article précédent.
1.2 – Serveur Web avec configuration et pages HTML personnalisées
Pour notre second exemple de service Linux standard, nous allons nous intéresser au répertoire 02-api-test-web. Il contient un fichier Dockerfile présenté en version un peu simplifiée ci-dessous et deux sous-répertoires :
deploy/contenant le fichier de configuration et le script de lancement du serveur Nginx,frontend/contenant des pages HTML, des fichiers graphiques et une feuille de style CSS.
En faisant appel à l’API Rest d’Eris Linux, nous commençons à voir le développement de code métier personnalisé. Le Dockerfile contient :
FROM alpine:latest
RUN apk add --no-cache nginx busybox-suid
COPY frontend/ /usr/share/nginx/html
COPY deploy/nginx.conf /etc/nginx/http.d/default.conf
COPY deploy/start.sh /start.sh
RUN chmod +x /start.sh
EXPOSE 80
CMD ["/start.sh"]
Pour créer le fichier image du container on emploie :
$ ./create-container arm64 ./02-api-test-web api-test-web
#0 building with "default" instance using docker driver
[...]
#11 exporting to client tarball
#11 sending tarball 0.0s done
#11 DONE 0.0s
/home/cpb/Eris-Linux-Containers
$ ls
[...] api-test-web.tar.bz2 [...]
Le mécanisme de chargement de l’image sur le serveur Eris Linux est identique à celui observé précédemment. On obtient une nouvelle image disponible.
Une fois l’image déployée sur un groupe d’équipements, on peut se connecter sur le serveur Web avec un navigateur en utilisant l’URL : http://<ip-carte>:8888
Ce container propose une IHM pour communiquer avec l’API Rest d’Eris Linux qui donne accès à diverses fonctionnalités système.
Dans cet exemple comme dans le précédent, il n’y a pas d’accès direct au matériel de la carte embarquée, on passe par l’API Eris Linux. Dans l’exemple suivant nous allons accéder au matériel via l’arborescence /sys/ de Linux.
2 – Scripts shell accédant au matériel
2.1 – Accès aux GPIO
Dans un container, le code est isolé du système. Il ne voit que les processus s’exécutant dans le même container. L’arborescence de fichiers dans lequel il évolue est également spécifique à ce container. Mais il est néanmoins possible de réclamer un accès aux périphériques en utilisant un container privilégié.
Sous Linux, lorsqu’un processus veut communiquer avec un périphérique matériel, il doit – sauf dans de très rares cas – passer par l’intermédiaire d’un driver (code dédié au périphérique s’exécutant dans le kernel Linux). Pour cela, l’approche la plus courante est d’invoquer un « appel système » (syscall) comme read(), write(), ioctl(), poll(), mmap() etc. sur un fichier spécial se trouvant dans le répertoire /dev/ du système. Une autre possibilité est de faire des requêtes de plus haut niveau au driver en envoyant des consignes par l’intermédiaire de l’arborescence de fichiers /sys/.
La plupart des systèmes embarqués offrent des possibilités d’entrées/sorties électroniques en utilisant les broches GPIO (General Purpose Input Output) du processeur. Ces lignes électriques peuvent être configurées à la demande en entrée ou en sortie, puis leur état peut être consulté ou modifié depuis une application.
Nous n’avons pas de container de démonstration utilisant directement les GPIO car il s’agit d’opérations assez sensibles, et une erreur de manipulation peut vite détruire une carte électronique. Par exemple configurer en sortie une broche normalement prévue pour être une entrée et lui forcer un état électrique différent de celui imposé par le circuit externe déclenche un court-circuit sur le processeur, ce qui peut endommager la sortie GPIO, voire le processeur, avec à la clé une petite fumée bleutée et la commande d’une nouvelle carte électronique.
Si vous souhaitez expérimenter les entrées-sorties de votre carte électronique, vous pouvez facilement ajouter la bibliothèque libgpiod (qui est automatiquement accompagnée de ses utilitaires) dans le Dockerfile que nous avons utilisé pour le serveur SSH.
FROM alpine:latest
RUN apk update \
&& apk add --no-cache libgpiod openssh \
&& ssh-keygen -A \
[...]
La bibliothèque libgpiod communique avec les contrôleurs d’entrée-sortie par l’intermédiaire des fichiers spéciaux /dev/gpiochip##. Pour que cela soit possible depuis l’intérieur d’un container il est indispensable que celui ci ait été déclaré comme privilégié :
Une fois l’image produite, installée sur le serveur Eris Linux et le container déployé sur votre carte, vous pourrez vous connecter en utilisant SSH puis appeler les commandes gpiodetect, gpioinfo, gpioget, gpioset et gpiomon. Le comportement est parfois un peu inattendu (surtout celui de gpioset), on trouvera une explication détaillée dans cet article.
2.2 – Configuration des triggers de LEDs
2.2.1 – Principe
Les LEDs connues du noyau Linux peuvent être pilotées en configurant un trigger (actionneur) qui allumera ou éteindra la LED en fonction de circonstances externes. Il ne s’agit plus d’allumer ou d’éteindre explicitement une sortie GPIO, mais de demander à un composant logiciel du kernel de prendre en charge la broche de sortie électrique et de la piloter suivant les circonstances en cours.
Par exemple, il existe des triggers pour allumer ou éteindre des LEDs en fonction de l’activité d’un disque dur ou d’un SSD, ou encore en fonction du trafic réseau Ethernet. Des triggers peuvent piloter des LEDs en fonction de l’état des touches CAPS Lock, NUM Lock, SCROLL Lock, etc. Des triggers peuvent refléter sur des LEDs la charge d’un ou plusieurs cœurs de CPU ou le déclenchement d’un kernel panic.
Les triggers qui vont nous intéresser ici sont plus simples :
- «
default-on» allume la LED concernée de manière persistante - «
timer» la fait clignoter à une fréquence d’un Hz. - «
heartbeat» simule un battement de cœur représentatif de la charge du processeur. - «
none» éteint la LED indiquée.
L’avantage de ces triggers de haut-niveau est qu’ils sont indépendants du système sous-jacent, le script shell que nous allons voir peut fonctionner sur toutes les machines Linux abritant une ou plusieurs LEDs.
La configuration de ces triggers se fait en lisant et écrivant dans le pseudo-fichier /sys/class/leds/<led-name>/trigger : la lecture de ce fichier affiche la liste de tous les triggers connus, celui sélectionné étant encadré. L’écriture d’un nom de trigger dans ce fichier permet de le sélectionner.
2.2.2 – Script et Dockerfile
Le script shell que nous allons installer est très simple : il sélectionne les triggers de toutes les LEDs du système toutes les 15 secondes en alternant entre les quatre indiqués ci-dessus.
#! /bin/sh
while true
do
for trigger in timer default-on heartbeat none
do
for led in /sys/class/leds/*
do
if [ -f "${led}/trigger" ]
then
echo "${trigger}" > "${led}/trigger"
fi
done
sleep 15
done
done
Le fichier Dockerfile doit copier ce script sur le système cible et lui donner les droits d’exécution adéquats.
FROM alpine:latest
COPY led-trigger-setup.sh /led-trigger-setup.sh
RUN chmod +x /led-trigger-setup.sh
CMD ["/led-trigger-setup.sh"]
Comme pour l’exemple précédent, l’image de container doit être dotée de l’attribut « privileged » qui lui permettra de disposer d’une arborescence /sys complète (au même titre que le répertoire /dev de l’exemple précédent).
Une fois l’image construite, installée sur le serveur Eris Linux et déployée vers une carte cible, on pourra observer que toutes les LEDs changent de comportement toutes les 15 secondes : clignotement -> allumage -> battement de cœur -> extinction.
Les principes que nous avons vus pour l’installation des scripts shell sont évidemment valables pour tout langage interprété (Python, LUA, NodeJS, etc.) à condition d’installer le package de l’interpréteur dans l’image en ajoutant une ligne comme
RUN apk add --no-cache python3
dans le Dockerfile à la suite de la ligne FROM Alpine...
Nous reparlerons de Python un peu plus loin, mais nous allons à présent nous concentrer sur un deuxième type d’applications : celles dont le code source original est compilé pour produire un fichier exécutable directement exploitable par le processeur de la carte cible.
3 – Cross-compilation
La plupart des développeurs travaillent sur une machine (PC ou serveur distant) fonctionnant sur un processeur de la famille x86-64. Les systèmes embarqués les plus courants utilisent des processeurs de la famille ARM64 et parfois RISC-V. Pour produire du code exécutable fonctionnant sur la cible embarquée, il faudra donc réaliser une étape de cross-compilation.
Pour la plupart des systèmes embarqués, on passe par une phase un peu fastidieuse de téléchargement et d’installation d’un cross-compiler et d’un SDK (Software Development Kit). Bonne nouvelle : Avec Eris Linux vous n’avez pas besoin d’installer de chaîne de cross-compilation ni de SDK, tout va être pris en charge directement par le mode Buildx de Docker !
3.1 – Docker Buildx
Docker Buildx s’appuie sur QEMU et le mécanisme Linux binfmt_misc. Ce dernier permet d’associer certains formats d’exécutables à un interpréteur : lorsque sur une machine x86-64 on demande au noyau Linux de lancer un exécutable au format ARM64, il fait appel l’émulateur qemu-aarch64 approprié. Cette émulation est transparente pour l’utilisateur. Elle est aussi transparente lorsqu’il s’agit de lancer des commandes depuis un Dockerfile. Il est par exemple possible d’utiliser dans le container un compilateur GCC ARM64 natif, lui-même exécuté sous QEMU, pour compiler notre application. Il ne s’agit donc pas véritablement de cross-compilation, Buildx recrée plutôt, par émulation, un environnement ARM64 sur la machine x86-64. Cette méthode, légèrement plus lente qu’une cross-compilation traditionnelle, est extrêmement pratique.
Pour faire appel à Buildx de manière simplifiée, un script est fourni dans notre repository d’exemples : create-container. La ligne de commande exacte à utiliser est précisée dans le fichier README.md de chaque sous-répertoire d’exemple.
3.2 – Prérequis
Pour le bon fonctionnement de l’environnement de production, plusieurs packages doivent être installés sur la machine de développement, en utilisant la commande apt, yum, dnf, etc. correspondant à votre distribution.
docker-cedocker-ce-clidocker-buildx-plugin
Les noms des packages dépendent de la distribution Linux utilisée.
3.3 – Programme compilé en C
Traditionnellement, l’exploration d’un environnement de développement commence par l’écriture d’un programme affichant « Hello World! » sur la console. Ceci permet de valider la bonne configuration de la chaîne d’outils de compilation, d’autant plus dans un environnement embarqué où le processeur de compilation et celui d’exécution sont d’architectures différentes. Ne dérogeons pas à cette habitude, et préparons un container faisant fonctionner une application affichant un message, compilée depuis un fichier source en langage C.
Le programme proposé dans le sous-répertoire 05-hello-world-in-c est très simple :
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
for (;;) {
printf("Hello from Eris Linux container! (C language program)\n");
sleep(5);
}
return 0;
}
Il est accompagné d’un fichier Makefile pour le compiler. Vous pouvez lui préférer un autre outil de production plus moderne comme Cmake ou meson.build. Ce Makefile est très générique, il suffit essentiellement de modifier les quatre premières lignes pour pouvoir le réutiliser pour d’autres projets :
APP = hello-world-in-c
OBJS = hello-world-in-c.o
CFLAGS += -Wall -g
LDFLAGS =
all: $(APP)
$(APP): $(OBJS)
$(CC) $(CFLAGS) $(LDFLAGS) $^ -o $@ $(LDLIBS)
%.o: %.c $(INC)
$(CC) -c $(CFLAGS) $<
clean:
rm -rf out *.o $(APP)
Le point le plus important est un Dockerfile qui va assurer la compilation pour la cible désirée. Ce Dockerfile est composé en deux parties successives. La première partie fait juste la compilation du fichier source ci-dessus en s’exécutant dans un container dédié (sur la machine de production) qui est porté par l’émulateur Qemu.
FROM alpine:latest AS build
RUN apk add --no-cache build-base file
WORKDIR /src
COPY hello-world-in-c.c Makefile .
RUN make
La seconde partie du Dockerfile crée l’image du container pour la cible en embarquant le code compilé précédent :
FROM alpine:latest
COPY --from=build /src/hello-world-in-c /hello-world-in-c
CMD ["/hello-world-in-c"]
C’est le script create-container que nous avons vu plus haut qui appellera docker buildx en lui indiquant le type de cible désiré (argument --platform)
Lorsqu’on appelle :
$ ./create-container arm64 05-hello-world-in-c/ hello-world-in-c
Le script utilise docker buildx pour nous préparer un fichier hello-world-in-c.tar.bz2 représentant le container à déployer. Une fois cette image de container envoyée sur le serveur Eris Linux, et déployée sur une cible, on peut voir le message « Hello from Eris Linux container! (C language program) » s’afficher toutes les cinq secondes.
Nous pouvons également observer l’installation des fichiers pour Qemu utilisés par binfmt_misc durant la première exécution du script. Avant l’exécution du script :
$ ls /proc/sys/fs/binfmt_misc/
python3.14 register status
Après exécution du script :
$ ls /proc/sys/fs/binfmt_misc/
python3.14 qemu-aarch64 register status
L’émulateur qemu-aarch64 permet de faire fonctionner un exécutable prévu pour ARM64 sur une architecture différente.
3.4 – Programme C utilisant le réseau
Pour voir les messages de l’exemple ci-dessus il est nécessaire de disposer d’un écran sur la cible qui ne soit pas occupé par un affichage graphique. Si notre cible n’a pas d’afficheur on peut préférer un programme qui envoie son message sur une connexion réseau TCP/IP. Il suffit de modifier le fichier source ainsi :
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define PORT_NUMBER 3500
int main(void)
{
int sock;
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
int reuse = 1;
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT_NUMBER);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
perror("bind");
exit(EXIT_FAILURE);
}
if (listen(sock, 128) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
for (;;) {
const char message[] = "Hello from Eris Linux container! (C language program)\n";
int new_sock;
if ((new_sock = accept(sock, NULL, NULL)) < 0)
continue;
(void) send(new_sock, message, strlen(message), MSG_NOSIGNAL);
shutdown(new_sock, SHUT_RDWR);
close(new_sock);
}
}
Bien sûr lors du déploiement du container il sera nécessaire d’exporter le port TCP pour qu’il soit accessible depuis l’extérieur du container.
On pourra se connecter sur le serveur TCP avec n’importe quel client générique comme telnet ou nc par exemple :
$ nc 192.168.3.36 3500
Hello from Eris Linux container! (C language program)
3.5 – Programme C appelant l’API d’Eris Linux
Nous avons vu comment un programme fonctionnant dans un container peut accéder au matériel en communiquant avec les fichiers spéciaux de /dev ou avec les points d’entrée virtuels de l’arborescence /sys. Ceci à condition que le container soit privilégié.
Cette situation doit toutefois être évitée autant que possible car l’utilisation d’un container privilégié dégrade la sécurité du système, basée sur l’isolation entre le système Eris Linux de base et les containers applicatifs.
Il est nettement préférable, pour communiquer avec le système Eris Linux et le matériel sous-jacent de faire appel à l’API Eris Linux. Cette interface de programmation sera détaillée dans le prochain article. Elle est relativement complète mais nous continuons à l’enrichir régulièrement.
L’API Eris Linux est proposée sous deux formes aux possibilités identiques :
- une API Rest proposée par un serveur HTTP inclus dans le système Eris Linux, nous en avons eu un aperçu dans le paragraphe 1.2.
- une bibliothèque pour les applications C/C++ disponible dans les containers sous le nom
liberis.
La mise à disposition de la bibliothèque pour les applications écrites en Python ou en Rust fait partie des prochaines évolutions d’Eris Linux.
Nous allons examiner le contenu du répertoire 06-api-test-tcp/ qui implémente un service TCP, à la manière du précédent exemple, qui offre une petite interface pour interroger l’API Eris Linux, représentée par la bibliothèque liberis.
Les sources du programme se trouvent dans un sous-répertoire dédié et reposent sur huit fichiers source et autant de fichiers headers (d’extension .h)
$ ls 06-api-test-tcp/sources/
eris-api-test.c include network-api-test.c system-api-test.c update-api-test.c
gpio-api-test.c Makefile sbom-api-test.c time-api-test.c wdog-api-test.c
$ ls 06-api-test-tcp/sources/include/
eris-api-test.h network-api-test.h system-api-test.h update-api-test.h
gpio-api-test.h sbom-api-test.h time-api-test.h wdog-api-test.h
Le fichier Makefile présent avec les sources est un peu plus long que les précédents car il liste les fichiers objets à produire et les fichiers headers à inclure :
APP = eris-api-test
OBJS = \
eris-api-test.o \
gpio-api-test.o \
network-api-test.o \
sbom-api-test.o \
system-api-test.o \
time-api-test.o \
update-api-test.o \
wdog-api-test.o \
INC = liberis.h \
eris-api-test.h \
gpio-api-test.h \
network-api-test.h \
sbom-api-test.h \
system-api-test.h \
time-api-test.h \
update-api-test.h \
wdog-api-test.h \
CFLAGS += -Wall -g -I include/
LDLIBS += -leris -lcurl
all: $(APP)
$(APP): $(OBJS)
$(CC) $(CFLAGS) $(LDFLAGS) $^ -o $@ $(LDLIBS)
%.o: %.c $(INC)
$(CC) -c $(CFLAGS) $<
clean:
rm -rf out *.o $(APP)
On notera la présence dans la variable LDLIBS de l’argument -leris qui indique au linker que l’application fait appel à la bibliothèque liberis et de -lcurl car liberis fait à son tour appel à la bibliothèque libcurl.
Le Dockerfile est un peu plus complexe que le précédent. Dans sa première moitié, il commence par télécharger puis compiler la bibliothèque liberis, puis il compile l’application qui en dépend. La bibliothèque liberis est compilée sous forme statique. Ainsi ses fonctions nécessaires pour l’application seront directement copiées dans l’exécutable. Ceci évite de devoir gérer l’installation de la bibliothèque dynamique dans le container. La bibliothèque liberis dépendant de la libcurl, on remarquera la présence de curl-dev dans la liste des packages installés avec apk add à la deuxième ligne du fichier.
FROM alpine:latest AS build
RUN apk add --no-cache build-base file curl-dev git ca-certificates
WORKDIR /src
ARG LIBERIS_REPO=https://github.com/Eris-Linux/liberis.git
ARG LIBERIS_BRANCH=master
RUN git clone --depth 1 --branch "${LIBERIS_BRANCH}" "${LIBERIS_REPO}" /src/liberis
WORKDIR /src/liberis
RUN make static
RUN make static_install
WORKDIR /src
COPY sources/* .
RUN make
La seconde moitié du Dockerfile installe l’exécutable obtenu dans un container dédié. La libcurl nécessaire pour liberis est ajoutée depuis les packages standards de la distribution Alpine.
FROM alpine:latest
RUN apk add --no-cache libcurl
COPY --from=build /src/eris-api-test /eris-api-test
CMD ["/eris-api-test"]
Une fois le code compilé, l’image produite et installée sur le serveur Eris Linux (en exportant le port TCP 10000), puis déployée sur une carte cible, on peut s’y connecter depuis une autre machine :
$ nc 192.168.3.36 10000
**** Eris Linux API test *****
1: System Identification 6: Software Bill of Materials
2: System & Containers Update 7: Network Interfaces
3: Time Setup 8: General Purpose I/O
4: Watchdog Configuration 9: Display Features(*)
5: Audio Features(*)
0: Quit
(*) Coming soon
Your choice: 7
**** Eris Linux Network Interfaces Setup *****
1: Get list of interfaces 7: Get nameserver address
2: Get interface status 8: Set nameserver address
3: Set interface status 9: Scan Wifi Access Points
4: Get interface config 10: Connect to Wifi AP (BROKEN?)
5: Set interface config 11: Disconnect from Wifi A.P.
6: Is interface wireless 12: Get Wifi quality
0: Return
Your choice: 1
List of network interfaces : wlan0 eth0
Your choice: 0
**** Eris Linux API test *****
1: System Identification 6: Software Bill of Materials
2: System & Containers Update 7: Network Interfaces
3: Time Setup 8: General Purpose I/O
4: Watchdog Configuration 9: Display Features(*)
5: Audio Features(*)
0: Quit
(*) Coming soon
Your choice: 0
$
Cette communication en mode texte est pratique lorsqu’on doit se connecter à distance sur une carte, mais il faut reconnaître que c’est un peu le « degré 0 » de l’interface utilisateur.
Dans de nombreux cas, notamment lorsque le système est destiné à un utilisateur grand-public, on préférera proposer une interface graphique plus esthétique et intuitive. C’est ce que nous allons examiner à présent.
3.6 – Programme C++ utilisant la bibliothèque graphique Qt
Il existe plusieurs bibliothèques d’interface graphique fonctionnant sous Linux. Les plus répandues sont probablement GTK et surtout Qt.
Les images d’installation d’Eris Linux (que nous avons vues dans le deuxième article de cette série) proposent deux variantes : graphical et headless selon la présence ou non d’un écran graphique sur les systèmes cibles. Dans le cas d’un système graphical, la bibliothèque Qt 6 (6.11 lors de la rédaction de ces lignes) est installée avec un support X11.
Si le besoin s’en fait sentir, il est possible de créer une image dédiée incorporant une autre version de Qt, ou incorporant GTK, ou encore s’appuyant sur Wayland plutôt que X11. Contactez-nous pour en savoir plus.
L’exemple se trouvant dans le répertoire 07-api-test-graphical/ propose une interface graphique pour réaliser les mêmes tâches de test de l’API Eris Linux que précédemment. L’organisation de ce répertoire est très proche de celui de l’exemple précédent.
$ ls 07-api-test-graphical/
Dockerfile README.md sources
$ ls 07-api-test-graphical/sources/
api-test.qss network-api-window.cpp update-api-window.cpp
gpio-api-window.cpp network-api-window.h update-api-window.h
gpio-api-window.h sbom-api-window.cpp virtual-keyboard.cpp
graphic-api-test.cpp sbom-api-window.h virtual-keyboard.h
license-window.cpp system-api-window.cpp virtual-keyboard.qss
license-window.h system-api-window.h watchdog-api-window.cpp
logo.png time-api-window.cpp watchdog-api-window.h
Makefile time-api-window.h
Le fichier Makefile offre la particularité d’appeler la commande shell pkg-config pour obtenir la liste des flags à passer au compilateur C++ et la liste des bibliothèques à lier pour produire le fichier exécutable (outre liberis et libcurl qui sont explicitement mentionnées).
EXE=graphic-api-test
OBJS= \
graphic-api-test.o \
virtual-keyboard.o \
gpio-api-window.o \
license-window.o \
network-api-window.o \
sbom-api-window.o \
system-api-window.o \
time-api-window.o \
update-api-window.o \
watchdog-api-window.o \
CXXFLAGS += -Wall -Wextra -Os
CXXFLAGS += $(shell pkg-config --cflags Qt6Widgets)
CXXFLAGS += -Iliberis/include
LDFLAGS += -L.
LDLIBS += -leris
LDLIBS += $(shell pkg-config --libs Qt6Widgets)
LDLIBS += -lcurl
.PHONY: all
all: $(EXE)
$(EXE): $(OBJS)
$(CXX) $(LDFLAGS) -o $@ $^ $(LDLIBS)
%.o: %.cpp
$(CXX) -c $(CXXFLAGS) $<
.PHONY: clean
clean:
rm -f $(EXE) $(OBJS)
Pour que l’appel de pkg-config fonctionne, il faut que cette commande soit installée et disponible dès la compilation de l’image. C’est là que la « magie » de Docker buildx prend tout son sens : on ne fait pas de cross-compilation, on compile l’application dans un container avec un environnement semblable à celui de l’exécution finale (dans lequel on ajoute simplement les outils de compilation build-base). Voici le Dockerfile :
FROM alpine:latest AS build
RUN apk add --no-cache build-base file curl-dev libstdc++ pkgconf qt6-qtbase-dev git ca-certificates
WORKDIR /src
ARG LIBERIS_REPO=https://github.com/Eris-Linux/liberis.git
ARG LIBERIS_BRANCH=master
RUN git clone --depth 1 --branch "${LIBERIS_BRANCH}" "${LIBERIS_REPO}" /src/liberis
WORKDIR /src/liberis
RUN make static
RUN make static_install
WORKDIR /src
COPY sources/* .
RUN make
FROM alpine:latest
RUN apk add --no-cache libcurl
RUN apk add --no-cache qt6-qtbase-x11 qt6-qtdeclarative fontconfig ttf-dejavu
COPY --from=build /src/graphic-api-test /graphic-api-test
COPY --from=build /src/api-test.qss /api-test.qss
CMD ["/graphic-api-test"]
Après compilation de l’image, il ne faut pas oublier en l’installant sur le serveur Eris Linux d’activer la coche « Graphical ».
Une fois l’image déployée sur une carte cible dotée d’un affichage graphique, on doit disposer d’une interface simple mais esthétique. (Suivant la résolution de votre afficheur, les résultats peuvent être très variables, voire totalement inutilisables).
4 – Programmes en Rust
4.1 – Avertissement
Nous allons examiner quelques exemples d’utilisation de Rust dans différents types de containers.
Je dois préciser que je n’ai pas une très grande expérience en Rust, elle se limite surtout à des tutoriaux d’apprentissage et des exercices du challenge Advent of Code. Je n’ai pas encore utilisé Rust pour des projets clients, embarqués ou non. Je me limiterai donc à commenter un exemple simple écrit par mon collègue Alexandre.
En revanche, nous allons profiter de ces exemples pour voir des containers basés sur d’autres arborescences que celle d’Alpine.
4.2 – Compilation d’un programme Rust
Le premier exemple avec Rust se trouve dans le répertoire 08-hello-world-in-rust/ :
$ ls 08-hello-world-in-rust/
Cargo.toml Dockerfile README.md src
$ ls 08-hello-world-in-rust/src/
main.rs
Nous avons ici un unique fichier source dans src/, dont le contenu est un peu plus complet que les « Hello World! » minimaux précédents :
use axum::{Router, response::Html, routing::get};
use std::time::Duration;
async fn std_out_hello() {
let mut interval = tokio::time::interval(Duration::from_secs(5));
loop {
interval.tick().await;
println!("Hello, World! (from Rust)");
}
}
#[tokio::main]
async fn main() {
tokio::spawn(std_out_hello());
// build our application with a route
let app = Router::new().route("/", get(handler));
// run it
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
println!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await;
}
async fn handler() -> Html<&'static str> {
Html("<h1>Hello, World! (from Rust)</h1>")
}
Notre programme utilise l’environnement d’exécution Tokio qui lui permet d’exécuter une tâche asynchrone affichant un message sur sa sortie standard toutes les cinq secondes, puis de créer un serveur TCP sur le port 3000 qui affiche une ligne de message HTML quand on se connecte. Pour offrir un serveur HTTP correct (prêt à recevoir la connexion d’un navigateur web par exemple) le programme s’appuie sur la bibliothèque Axum.
Le fichier Cargo.toml représente le manifeste du programme regroupant les versions de bibliothèque à employer :
[package]
name = "example-hello-world"
version = "0.1.0"
edition = "2024"
[dependencies]
axum = { version = "0.8.9" }
tokio = { version = "1.0", features = ["full"] }
Enfin un Dockerfile va permettre de créer l’image de container. Comme d’habitude il est construit en deux parties, la première permet de compiler le programme :
FROM alpine:latest AS build
RUN apk add --no-cache build-base file rust cargo
WORKDIR /src
COPY . .
RUN cargo build --release
La seconde partie construit le container d’exécution :
FROM alpine:latest
RUN apk add --no-cache libgcc
COPY --from=build /src/target/release/example-hello-world /example-hello-world
CMD ["/example-hello-world"]
Pour la compilation de l’application en Rust, on fait appel à cargo build avec l’option --release pour activer l’optimisation du code en production.
Comme pour les autres exemples, on utilisera
$ create-container <architecture> <directory> <container-name>
pour produire l’image, puis on l’enverra sur le serveur Eris Linux (en exportant le port TCP 3000) avant de la déployer vers les cartes cibles.
4.3 – Image Distroless pour le container d’exécution
Nous avons utilisé jusqu’à présent pour le container produit une image basée sur une distribution Alpine. Celle-ci est très légère mais complète. Elle contient un shell, des utilitaires, des bibliothèques suffisantes pour permettre une connexion de l’utilisateur et sa navigation sur le système. Elle contient aussi un gestionnaire de paquets (apk).
Ceci n’est pas toujours nécessaire. On peut par exemple employer une image basée sur « Google Distroless » : une distribution ultra-minimale centrée sur l’application unique. Impossible de se connecter (pas de shell), ni d’ajouter dynamiquement des packages.
Pour cela la seule modification à apporter (voir le sous-répertoire 09-hello-rust-distroless/) est de transformer la première lignes de la première partie du Dockerfile en :
FROM rust:1.96.0 AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
et la première ligne de la seconde partie ainsi :
FROM gcr.io/distroless/cc-debian13
COPY --from=builder /app/target/release/example-hello-world /example-hello-world
EXPOSE 3000
CMD ["/example-hello-world"]
La compilation, le transfert sur le serveur Eris Linux (avec l’exportation de port 3000) et le déploiement sont identiques à l’exemple précédent.
4.4 – Container avec application unique
On peut réduire encore la taille du container en n’embarquant plus d’arborescence, mais en construisant une image limité à l’exécutable compilé.
Pour cela on va utiliser un container vide (« scratch » dans la terminologie Docker) dans lequel on ne mettra que notre application. Classiquement, les exécutables sous Linux dépendent tous de la libC qui fournit les points d’accès aux appels système, mais comme on n’installera aucune bibliothèque dans l’arborescence de l’image, il faudra compiler notre application avec un link statique.
Ceci s’obtient en modifiant légèrement la première partie du Dockerfile :
FROM rust:1.96.0 AS builder
WORKDIR /app
COPY . .
ARG FULLARCH
RUN rustup target add "${FULLARCH}"
RUN cargo build --target "${FULLARCH}" --release
pour la seconde partie du Dockerfile, on part d’une distribution vide et n’installe que l’exécutable produit :
FROM scratch
ARG FULLARCH
COPY --from=builder /app/target/${FULLARCH}/release/example-hello-world /example-hello-world
EXPOSE 3000
CMD ["/example-hello-world"]
4.5 – Comparaisons
Les trois containers précédents ont été produits avec des méthodes différentes. Voici les résultats en terme de taille de fichier image de container (décompressée) :
- Container incluant une distribution Alpine : 11 Mo.
- Container basé sur une distribution Distroless : 40 Mo.
- Container embarquant uniquement l’application : 2 Mo.
Il est un peu surprenant a priori de voir que le container Distroless est plus volumineux que celui basé sur Alpine. Il faut toutefois savoir que la libC utilisée par Alpine est la bibliothèque Musl (très optimisée pour la réduction de taille) et celle employée par Distroless est GlibC (très riche et complète mais plutôt volumineuse). L’intérêt de Distroless sera surtout une amélioration de la sécurité par réduction du périmètre d’attaque.
À l’extrême, le container n’embarquant que l’application permet d’obtenir une réduction impressionnante de la taille de l’image.
5 – Exemple en Python
Pour finir nous allons examiner un dernier container, écrit par ChatGPT en Python.
Il s’agit à nouveau d’un serveur affichant un message « Hello World! », mais cette fois le service est HTTPS avec gestion d’un certificat généré à la création de l’image. Voici le contenu du répertoire concerné :
$ ls 11-hello-world-in-python/
Dockerfile hello-world-in-python.py README.md
Le script est un peu plus complet que ceux que nous avons vus précédemment. Il s’appuie sur les modules Python http.server et ssl.
#!/usr/bin/env python3
#
# Simple Python HTTPS "Hello World" example for Eris Linux containers.
#
# ChatGPT 2026
#
# License: MIT
#
import ssl
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
HOST = "0.0.0.0"
PORT = 443
CERT_FILE = "/etc/eris-linux/ssl/hello-world.crt"
KEY_FILE = "/etc/eris-linux/ssl/hello-world.key"
class HelloWorldHandler(BaseHTTPRequestHandler):
def do_GET(self):
print("Hello World", flush=True)
body = b"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Eris Linux Python container</title>
</head>
<body>
<h1>Hello World</h1>
<p>This page is served by a Python HTTPS server running in an Eris Linux container.</p>
</body>
</html>
"""
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, fmt, *args):
print("%s - %s" % (self.address_string(), fmt % args), flush=True)
def main():
print("Hello World (from Python)", flush=True)
print(f"Starting HTTPS server on port {PORT}", flush=True)
server = ThreadingHTTPServer((HOST, PORT), HelloWorldHandler)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(certfile=CERT_FILE, keyfile=KEY_FILE)
server.socket = context.wrap_socket(server.socket, server_side=True)
server.serve_forever()
if __name__ == "__main__":
main()
Le fichier Dockerfile qui l’accompagne est en deux parties. La première partie génère le certificat dans un container fonctionnant nativement sur la plateforme de développement. Cette opération étant indépendante de l’architecture cible, elle ne nécessite aucune émulation.
Cette méthode convient à cet exemple de démonstration. Sur un produit réel, chaque équipement devrait disposer de son propre certificat et de sa propre clé privée, générés ou provisionnés individuellement.
ARG ALPINE_VERSION=3.22
FROM --platform=$BUILDPLATFORM alpine:${ALPINE_VERSION} AS certificates
RUN apk add --no-cache openssl
RUN mkdir -p /out \
&& openssl req \
-x509 \
-newkey rsa:2048 \
-nodes \
-keyout /out/hello-world.key \
-out /out/hello-world.crt \
-days 3650 \
-subj "/CN=eris-linux-python-hello-world" \
&& chmod 600 /out/hello-world.key
La seconde partie du Dockerfile installe les certificats et le script dans l’image de container finale :
FROM alpine:${ALPINE_VERSION}
RUN apk add --no-cache python3
COPY --from=certificates /out/hello-world.crt /etc/eris-linux/ssl/hello-world.crt
COPY --from=certificates /out/hello-world.key /etc/eris-linux/ssl/hello-world.key
COPY hello-world-in-python.py /hello-world-in-python.py
RUN chmod +x /hello-world-in-python.py
EXPOSE 443
CMD [ "python3", "-u", "/hello-world-in-python.py" ]
Après génération de l’image, enregistrement sur le serveur Eris Linux (en exportant le port TCP 443 – celui de HTTPS), et déploiement sur une cible, on peut se connecter en utilisant l’URL https://<target-address>.
Comme c’est souvent le cas en embarqué industriel, on peut considérer que les cartes cibles sont installées sur un sous-réseau dédié, sans accès direct à Internet. Pour cet exemple autonome, nous utilisons donc un certificat autosigné. Il est donc normal que le navigateur affiche un avertissement lors de la première connexion à notre cible.
Dans un produit industriel, on pourra préférer un certificat propre à chaque équipement, éventuellement délivré par une autorité de certification interne.
Conclusion
Nous avons pu étudier dans cet article plusieurs exemples de code applicatif hébergé dans des containers pour Eris Linux qui couvrent une bonne partie des cas d’usage dans le domaine embarqué :
- l’installation de services standards de Linux (comme OpenSSH) éventuellement avec une configuration spécifique (comme Nginx),
- l’installation de programmes interprétés (scripts shell et Python), pouvant même accéder directement aux ressources matérielles du système,
- la mise en œuvre d’applications compilées (en C, en C++, en Rust), utilisant éventuellement une bibliothèque graphique (Qt),
- l’appel des fonctions de l’API Eris Linux, depuis une page Web (API Rest) ou depuis un programme compilé (API
liberis), - utilisation de containers embarquant une distribution Alpine, Google Distroless ou encore ne contenant que l’exécutable applicatif.
Dans le prochain article nous allons examiner plus précisément l’API de programmation proposée par Eris Linux.
Nous recherchons actuellement des entreprises partenaires souhaitant expérimenter Eris Linux sur un cas d’usage réel. Ces premiers utilisateurs pourront bénéficier de conditions commerciales avantageuses et leurs retours d’expérience contribueront directement à l’évolution du projet.












