Utiliser Rust avec Buildroot (construction complète)

Dans un article précédent, nous avons vu comment ajouter le support du langage de programmation Rust dans Buildroot, en utiisant des binaires pré-compilés.

Cette fois-ci, nous ajouterons le support pour Rust en construisant un compilateur croisé dans l'environement Buildroot. Nous reprendrons l'exemple précédent, basé sur un système QEMU ARM Versatile Express.

Construction de l'Image Système

Premièrement, récupérez le code source de Buildroot et initialisez la configuration:

# Clonage du dépôt de Buildroot
git clone https://git.buildroot.net/buildroot
cd buildroot
# Configuration pour le système désiré
make O=$HOME/build/demo-rust/qemu/arm qemu_arm_vexpress_defconfig

Ensuite, modifiez la configuration:

make O=$HOME/build/demo-rust/qemu/arm menuconfig

Allez dans le menu "Toolchain" et sélectionnez "glibc" à la place de "uclibc" en tant que bibliothèque C. Sélectionnez aussi le support du C++. Sauvegardez votre configuration et sortez, puis démarrez la construction:

make O=$HOME/build/demo-rust/qemu/arm

Construction du Compilateur Rust et de la Bibliothèque Standard

Téléchargement du Code Source

Premièrement, récupérez le code source du compilateur Rust et décompressez-le:

# Récupération du dernier code source
pushd dl
wget https://static.rust-lang.org/dist/rustc-1.7.0-src.tar.gz
popd
# Création d'un répertoire de destination
mkdir -p $HOME/build/demo-rust/qemu/arm/build/host-rust-1.7.0
# Extraction du code source
tar -xzf dl/rustc-1.7.0-src.tar.gz \
    -C $HOME/build/demo-rust/qemu/arm/build/host-rust-1.7.0 \
    --strip-components=1

Configuration de la Compilation Croisée

En interne, le compilateur Rust utilise LLVM en tant que moteur. LLVM s'attend à ce que le nom de la chaîne de compilation corresponde à un triplet au format GNU: cpu-vendeur-noyau. Comme expliqué dans la documentation Autotools:

Currently configuration names are permitted to have four parts on systems which distinguish the kernel and the operating system, such as GNU/Linux. In these cases, the configuration name is cpu-manufacturer-kernel-operating_system.

Mais, comme le champ "vendeur" a généralement la valeur "unknown" ("inconnu"), les outils GNU autorisent son omission, ce qui entraîne le résultat ambigü "x86_64-linux-gnu" rapporté par gcc -dumpmachine sur un système Debian Jessie (à la place de "x86_64-unknown-linux-gnu"). Sur Fedora, le résultat est "x86_64-redhat-linux" (cette fois, pas de "système").

Afin que le système de construction prenne en compte le compilateur croisé généré par Buildroot (qui n'a pas de nom ambigü), ainsi que la machine cible, de nouveaux fichiers doivent être ajoutés.

Pour cela, allez dès maintenant dans le répertoire des sources de Rust:

pushd $HOME/build/demo-rust/qemu/arm/build/host-rust-1.7.0

Déclaration de la Cible

Le premier fichier requis est un morceau de Makefile, mk/cfg/arm-buildroot-linux-gnueabihf.mk, qui déclare la nouvelle cible. Ce fichier peut facilement être créé en copiant un des fichiers par défaut, qui corresponde à peu près à la nouvelle cible.

sed -e 's/-unknown-/-buildroot-/g' \
    mk/cfg/arm-unknown-linux-gnueabihf.mk \
    > mk/cfg/arm-buildroot-linux-gnueabihf.mk
sed -i -e 's/arm-linux-gnueabihf-/arm-buildroot-linux-gnueabihf-/g' \
    mk/cfg/arm-buildroot-linux-gnueabihf.mk

Spécification de la Cible

Ensuite, le système de construction doit connaitre l'architecture de la nouvelle cible (type de CPU, etc): c'est la spécification de la cible. Il y a deux méthodes pour y parvenir.

La première méthode consisterait à ajouter un nouveau fichier source Rust, basé sur un existant, afin de donner les détails de la cible:

sed -e 's/unknown/buildroot/g' \
    src/librustc_back/target/arm_unknown_linux_gnueabihf.rs \
    > src/librustc_back/target/arm_buildroot_linux_gnueabihf.rs

Le fichier généré ressemble à ceci:

pub fn target() -> Target {
    let base = super::linux_base::opts();
    Target {
        llvm_target: "arm-buildroot-linux-gnueabihf".to_string(),
        target_endian: "little".to_string(),
        target_pointer_width: "32".to_string(),
        arch: "arm".to_string(),
        target_os: "linux".to_string(),
        target_env: "gnueabihf".to_string(),
        target_vendor: "buildroot".to_string(),

        options: TargetOptions {
            features: "+v6,+vfp2".to_string(),
            .. base
        }
    }
}

Pour compiler le nouveau module, le patch suivant devrait être appliqué:

Index: host-rust-1.7.0/src/librustc_back/target/mod.rs
===================================================================
--- host-rust-1.7.0.orig/src/librustc_back/target/mod.rs
+++ host-rust-1.7.0/src/librustc_back/target/mod.rs
@@ -417,6 +417,7 @@ impl Target {
             powerpc64le_unknown_linux_gnu,
             arm_unknown_linux_gnueabi,
             arm_unknown_linux_gnueabihf,
+            arm_buildroot_linux_gnueabihf,
             aarch64_unknown_linux_gnu,
             x86_64_unknown_linux_musl,

Cette méthode ne serait pas très pratique pour Buildroot, car il serait nécessaire de fournir et d'appliquer un ensemble de patchs qui déclarent toutes les cibles supportées.

La seconde méthode est meilleure. Au lieu d'ajouter un nouveau module Rust, il est possible de fournir un fichier JSON qui contient les mêmes informations, comme il est expliqué dans la documentation non-officielle de la gestion des cibles par rustc et RFC 0131.

Comme on peut le voir dans src/librustc_back/target/mod.rs, le fichier JSON peut être stocké dans un répertoire figurant dans la liste de valeurs, séparées par des virgules, déclarée par la variable d'environement RUST_TARGET_PATH. La destination choisie est $HOME/build/demo-rust/qemu/arm/host/etc/rustc.

Pour créer le fichier JSON de spécification de la nouvelle cible, exécutez:

mkdir -p $HOME/build/demo-rust/qemu/arm/host/etc/rustc
cat <<EOF > $HOME/build/demo-rust/qemu/arm/host/etc/rustc/arm-buildroot-linux-gnueabihf.json
{
    "llvm-target": "arm-buildroot-linux-gnueabihf",
    "target-endian": "little",
    "target-pointer-width": "32",
    "target-env": "gnueabihf",
    "target-vendor": "buildroot",
    "arch": "arm",
    "os": "linux",
    "features": "+v6,+vfp2",
    "dynamic-linking": true,
    "executables": true,
    "morestack": true,
    "linker-is-gnu": true,
    "has-rpath": true,
    "pre-link-args": [
        "-Wl,--as-needed"
    ],
    "position-independent-executables": true,
    "archive-format": "gnu"
}
EOF

Maintentant, configurez et lancer la construction:

export PATH=$HOME/build/demo-rust/qemu/arm/host/usr/bin:$PATH
export RUST_TARGET_PATH=$HOME/build/demo-rust/qemu/arm/host/etc/rustc
./configure --prefix=$HOME/build/demo-rust/qemu/arm/host/usr \
            --localstatedir=$HOME/build/demo-rust/qemu/arm/host/var/lib \
            --sysconfdir=$HOME/build/demo-rust/qemu/arm/host/etc \
            --target=arm-buildroot-linux-gnueabihf
make -j8 VERBOSE=1
make install
popd

La construction prend un certain temps, car LLVM est compilé avec le support de toutes les architectures, et rustc n'est pas très rapide à se compiler lui-même. Quand tout est fini, vérifiez que l'installation est OK:

$ ls -l $HOME/build/demo-rust/qemu/arm/host/usr/bin
rust-gdb  rustc  rustdoc
$ $HOME/build/demo-rust/qemu/arm/host/bin/rustc --version
rustc 1.7.0-dev
$ ls -1 $HOME/build/demo-rust/qemu/arm/host/usr/lib/rustlib
arm-buildroot-linux-gnueabihf
components
etc
install.log
manifest-rust-docs
manifest-rust-std-arm-buildroot-linux-gnueabihf
manifest-rust-std-x86_64-unknown-linux-gnu
manifest-rustc
rust-installer-version
uninstall.sh
x86_64-unknown-linux-gnu

Comme on peut le voir, la bibliothèque standard est disponible pour les architectures ARM and x86_64.

Construction d'un Programme de Test

Il est temps de tester le compilateur. Créez un fichier source Rust pour le programme "Hello World":

mkdir -p $HOME/src/hello-rust
cat <<EOF > $HOME/src/hello-rust/main.rs
fn main() {
    println!("Hello World!");
}
EOF

Pour construire le programme de test hello-rust, exécutez:

export PATH=$HOME/build/demo-rust/qemu/arm/host/usr/bin:$PATH
$HOME/build/demo-rust/qemu/arm/host/usr/bin/rustc \
    --target=arm-buildroot-linux-gnueabihf \
    -C linker=arm-buildroot-linux-gnueabihf-gcc \
    -o $HOME/build/demo-rust/qemu/arm/target/usr/bin/hello-rust \
    $HOME/src/hello-rust/main.rs

Exécution du Programme de Test sur le Système

Reconstruisez l'image système:

make O=$HOME/build/demo-rust/qemu/arm

Maintenant, vous pouvez démarrer votre système avec QEMU:

qemu-system-arm \
    -M vexpress-a9 \
    -m 256 \
    -kernel $HOME/build/demo-rust/qemu/arm/images/zImage \
    -dtb $HOME/build/demo-rust/qemu/arm/images/vexpress-v2p-ca9.dtb \
    -drive file=$HOME/build/demo-rust/qemu/arm/images/rootfs.ext2,if=sd,format=raw \
    -append "console=ttyAMA0,115200 root=/dev/mmcblk0" \
    -serial stdio \
    -net nic,model=lan9118 \
    -net user

Connectez-vous en tant que "root" (pas de mot de passe) et exécutez le programme de test:

Welcome to Buildroot
buildroot login: root
# hello-rust
Hello World!

Parfait! Une fois de plus, vous venez d'exécuter un programme en Rust sur un système Linux embarqué (emulé), mais cette fois-ci en construisant tout de bout-en-bout.