iTranslated by AI
Building Latest Qt 6 from Source for EGLFS Development on TorizonOS
Introduction
This article outlines the procedure for building the latest version of Qt 6 from source on TorizonOS running on Toradex Verdin SoMs (AM62 / iMX8MP) and running GUI applications using EGLFS (EGL Full Screen).
The official containers provided by Toradex include Qt 6.4.2 (from Debian bookworm packages). Qt 6.4 is not an LTS release, and its OSS support ended in March 2023, with commercial support ending in September 2023 (endoflife.date/qt). The currently supported LTS version is Qt 6.8 (commercial support until October 2029). Building Qt from source is necessary not only to utilize the latest QML features or new modules like CanvasPainter but also for security and bug fixes. This article covers the entire process, from utilizing Docker's multi-stage builds to compile Qt for ARM64 on a host PC, to deploying it to the device.
Target Environment
| Item | Content |
|---|---|
| Host PC | x86_64 Linux (Docker + QEMU user-mode) |
| Target | Toradex Verdin AM62 / Verdin iMX8MP |
| OS | TorizonOS 7.5.0 |
| Qt | Any version (source build via git branch or tag) |
| Display | EGLFS (No Wayland/X11 required) |
| Monitor | 7-inch DSI 1024x600 |
Prerequisites
Docker buildx and QEMU
To build ARM64 containers on an x86_64 host, QEMU user-mode emulation is required.
# Register QEMU binfmt (usually set automatically by Docker Desktop)
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
Disabling ASLR (Kernel 6.8 and later)
In the Linux kernel 6.8.0-51 and later, ASLR is incompatible with QEMU user-mode static binaries, causing compilers inside ARM64 containers to segfault randomly. Be sure to disable this before building.
sudo sysctl kernel.randomize_va_space=0
The symptom is that cc1 or cc1plus repeatedly segfaults at the same address, 0x61c4a0. You can confirm this via dmesg.
Reference: https://github.com/docker/buildx/issues/3170
Architecture Overview
Docker Image Layer Structure
torizon/qt6-wayland-${GPU}:4 ← Toradex official base (with GPU drivers)
└─ torizon-qt6-base:arm64[-es2] ← Qt source build (created in this article)
├─ Dockerfile ← App build + deployment
├─ Dockerfile.debug ← Runtime for SSH/GDB debugging
└─ Dockerfile.sdk ← For VS Code IDE extension
Both build and runtime stages use the same GPU base image. The runtime stage is a lightweight image that excludes build tools.
GPU Variants
| GPU | Base Image | Build Argument |
|---|---|---|
| AM62 (PowerVR IMG BXS-4-64) | torizon/qt6-wayland-am62:4 |
GPU=-am62 |
| iMX8MP (Vivante GC7000UL) | torizon/qt6-wayland-imx8:4 |
GPU=-imx8 |
Base Image Versions and GPU Drivers
Toradex base images utilize different GPU drivers depending on the container tag version. You must match the versions with the kernel and GPU drivers installed on your TorizonOS device.
| Container Tag | iMX8 (Vivante DDK) | AM62 (Mesa) |
|---|---|---|
| 4.0.0 | 6.4.11.p2.4 | 22.3.5 |
| 4.10.0 to 4.14.0 | 6.4.11.p2.8 | 24.0.1 |
If the versions do not match, GPU acceleration will be disabled, and it will fall back to software rendering (LLVMpipe). The Dockerfile in this article uses SDK_BASE_VERSION=4 to specify the major version, but you can specify SDK_BASE_VERSION=4.14.0 if you need to lock to a specific minor version.
Step 1: Building the Qt 6 Base Image
We will build Qt from source to create a base image suitable for both building and running applications.
QT_REF: Branch or Tag
Specify the git branch name or tag from the Qt repository (code.qt.io) for QT_REF in the Dockerfile.
| Example | Type | Content |
|---|---|---|
v6.10.2 |
Tag | Released version. Stable. |
6.9 |
Branch | 6.9 development branch. Bug fixes are merged periodically. |
6.11 |
Branch | 6.11 development branch. Contains the latest features not yet released. |
If you wish to use a released version, please specify a tag such as v6.10.2. This article uses the 6.11 branch to utilize the latest features under development (such as CanvasPainter).
As of February 2025, Qt 6.11 is in beta2. You could specify the v6.11.0-beta2 tag, but the 6.11 branch is further along in development and includes bug fixes. The branch will eventually transition through RC (Release Candidate) to an official release. Once officially released, using the v6.11.0 tag is safer.
Dockerfile.qt6-base
# ARGUMENTS
ARG QT_REF=6.11
ARG SDK_BASE_VERSION=4
ARG IMAGE_ARCH=arm64
ARG GPU=-am62
ARG PARALLEL_JOBS=
# BUILD STAGE
FROM --platform=linux/${IMAGE_ARCH} \
torizon/qt6-wayland${GPU}:${SDK_BASE_VERSION} AS builder
ARG QT_REF
ARG PARALLEL_JOBS
# Build dependencies
RUN apt-get -q -y update && \
apt-get -q -y install \
build-essential cmake ninja-build python3 perl git \
libxkbcommon-dev libfontconfig1-dev libfreetype6-dev \
libegl1-mesa-dev libglib2.0-dev libinput-dev \
libssl-dev libdbus-1-dev libicu-dev libpcre2-dev \
libzstd-dev libz-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# qtbase
RUN git clone --depth 1 --branch ${QT_REF} \
https://code.qt.io/qt/qtbase.git /tmp/qtbase && \
cd /tmp/qtbase && \
./configure \
-release \
-prefix /opt/Qt/${QT_REF}/gcc_arm64 \
-opengl es2 \
-qpa eglfs \
-no-feature-xcb \
-nomake examples \
-nomake tests \
-no-pch \
-no-widgets \
-- \
-DQT_BUILD_EXAMPLES=OFF \
-DQT_BUILD_TESTS=OFF && \
cmake --build . --parallel ${PARALLEL_JOBS:-$(nproc)} && \
cmake --install . && \
rm -rf /tmp/qtbase
# qtshadertools (dependency for qtdeclarative)
RUN git clone --depth 1 --branch ${QT_REF} \
https://code.qt.io/qt/qtshadertools.git /tmp/qtshadertools && \
cd /tmp/qtshadertools && \
/opt/Qt/${QT_REF}/gcc_arm64/bin/qt-configure-module . && \
cmake --build . --parallel ${PARALLEL_JOBS:-$(nproc)} && \
cmake --install . && \
rm -rf /tmp/qtshadertools
# qtdeclarative (QML engine)
RUN git clone --depth 1 --branch ${QT_REF} \
https://code.qt.io/qt/qtdeclarative.git /tmp/qtdeclarative && \
cd /tmp/qtdeclarative && \
/opt/Qt/${QT_REF}/gcc_arm64/bin/qt-configure-module . && \
cmake --build . --parallel ${PARALLEL_JOBS:-$(nproc)} && \
cmake --install . && \
rm -rf /tmp/qtdeclarative
# qtsvg
RUN git clone --depth 1 --branch ${QT_REF} \
https://code.qt.io/qt/qtsvg.git /tmp/qtsvg && \
cd /tmp/qtsvg && \
/opt/Qt/${QT_REF}/gcc_arm64/bin/qt-configure-module . && \
cmake --build . --parallel ${PARALLEL_JOBS:-$(nproc)} && \
cmake --install . && \
rm -rf /tmp/qtsvg
# RUNTIME STAGE
FROM --platform=linux/${IMAGE_ARCH} \
torizon/qt6-wayland${GPU}:${SDK_BASE_VERSION}
ARG QT_REF
ARG GPU
# GPU support packages (for Vivante only)
RUN apt-get -q -y update && \
if [ "${GPU}" = "-vivante" ] || [ "${GPU}" = "-imx8" ]; then \
apt-get -q -y install imx-gpu-viv-wayland-dev; \
fi && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Runtime dependency packages
RUN apt-get -q -y update && \
apt-get -q -y install \
libxkbcommon0 libfontconfig1 libfreetype6 libglib2.0-0 \
libinput10 libssl3 libdbus-1-3 libicu72 libpcre2-16-0 \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Copy Qt and create a version-independent symbolic link
COPY --from=builder /opt/Qt/ /opt/Qt/
RUN ln -sf /opt/Qt/${QT_REF}/gcc_arm64 /opt/Qt/current
# Environment Variables
ENV Qt6_DIR=/opt/Qt/${QT_REF}/gcc_arm64
ENV PATH="/opt/Qt/current/bin:${PATH}"
ENV LD_LIBRARY_PATH="/opt/Qt/current/lib:${LD_LIBRARY_PATH}"
ENV CMAKE_PREFIX_PATH="/opt/Qt/current/lib/cmake:${CMAKE_PREFIX_PATH}"
ENV QT_PLUGIN_PATH="/opt/Qt/current/plugins"
ENV QML2_IMPORT_PATH="/opt/Qt/current/qml"
# EGLFS configuration
COPY kms.conf /etc/kms.conf
ENV QT_QPA_PLATFORM=eglfs
ENV QT_QPA_EGLFS_KMS_CONFIG=/etc/kms.conf
Explanation of configure options
| Option | Reason |
|---|---|
-opengl es2 |
Links directly to libGLESv2.so without going through GLVND. Mandatory for Vivante (non-GLVND). |
-qpa eglfs |
Sets the default platform to EGLFS. |
-no-feature-xcb |
X11 is not needed. Reduces build time and size. |
-no-widgets |
QML only. Excludes QtWidgets. |
-no-pch |
Avoids instability of precompiled headers in QEMU environments. |
Regarding -opengl es2
Embedded GPUs typically support only OpenGL ES. Specifying -opengl es2 causes Qt to link directly to libGLESv2.so. Be sure to include this when building for embedded systems.
kms.conf
This is the display setting for the EGLFS KMS backend. It is copied to /etc/kms.conf inside the container.
{
"device": "/dev/dri/card0",
"hwcursor": false,
"pbuffers": false,
"outputs": [
{
"name": "LVDS-1",
"mode": "1024x600",
"virtualIndex": 0,
"primary": true
}
]
}
Device and connector names vary by hardware. You can use qt-kms-conf to automatically detect connected displays and generate a kms.conf.
Executing the Build
# Disabling ASLR (Required before building)
sudo sysctl kernel.randomize_va_space=0
# For AM62
docker buildx build --load --platform linux/arm64 \
-t localhost:5002/torizon-qt6-base:arm64-am62 \
-f Dockerfile.qt6-base --build-arg GPU=-am62 .
# For iMX8MP
docker buildx build --load --platform linux/arm64 \
-t localhost:5002/torizon-qt6-base:arm64-imx8 \
-f Dockerfile.qt6-base --build-arg GPU=-imx8 .
The initial build takes 1 to 2 hours, depending on machine specifications, as it performs a full Qt build under QEMU emulation. Docker layer caching ensures that subsequent builds only rebuild modified modules.
Step 2: Creating the Application
Project Structure
HelloWorld/
├── CMakeLists.txt
├── main.cpp
├── QML/
│ ├── main.qml
│ ├── Scene.ui.qml
│ └── assets/
│ └── torizon-logo.png
├── kms.conf
├── Dockerfile.qt6-base ← Created in Step 1
├── Dockerfile ← Build + deploy app
├── Dockerfile.debug ← Debugging
└── docker-compose.yml
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(HelloWorld LANGUAGES CXX C)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Qml Quick)
qt_add_executable(HelloWorld WIN32
main.cpp
)
target_link_libraries(HelloWorld PRIVATE
Qt::Core Qt::Gui Qt::Qml Qt::Quick
)
qt_add_qml_module(HelloWorld
URI HelloWorld
VERSION 1.0
QML_FILES
"QML/main.qml"
"QML/Scene.ui.qml"
RESOURCES
"QML/assets/torizon-logo.png"
)
Since qt_add_qml_module embeds QML files and resources into the binary, no additional file transfers are required during deployment.
main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
auto context = engine.rootContext();
context->setContextProperty("qt_version", QT_VERSION_STR);
const QUrl url("qrc:/HelloWorld/QML/main.qml");
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
&app, [url](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl)
QCoreApplication::exit(-1);
}, Qt::QueuedConnection);
engine.load(url);
return app.exec();
}
Step 3: Application Dockerfile
This is a two-stage configuration where the application is built on top of the base image created in Step 1 and then copied into the runtime image.
ARG IMAGE_ARCH=arm64
ARG APP_ROOT=/home/torizon/app
ARG GPU=
# BUILD STAGE
FROM --platform=linux/${IMAGE_ARCH} \
localhost:5002/torizon-qt6-base:${IMAGE_ARCH}${GPU} AS build
ARG IMAGE_ARCH
ARG APP_ROOT
COPY . ${APP_ROOT}
WORKDIR ${APP_ROOT}
RUN rm -rf ${APP_ROOT}/build-${IMAGE_ARCH}
RUN cmake -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=/opt/Qt/current/lib/cmake \
-Bbuild-${IMAGE_ARCH} && \
cmake --build build-${IMAGE_ARCH}
# DEPLOY STAGE
FROM --platform=linux/${IMAGE_ARCH} \
localhost:5002/torizon-qt6-base:${IMAGE_ARCH}${GPU} AS deploy
ARG APP_ROOT
COPY kms.conf /etc/kms.conf
USER torizon
COPY --from=build ${APP_ROOT}/build-${IMAGE_ARCH}/bin ${APP_ROOT}
WORKDIR ${APP_ROOT}
CMD ["./HelloWorld"]
Build and Deployment
# If using docker-compose.yml
docker compose build qt-helloworld
docker compose up -d qt-helloworld
# Manual approach
docker buildx build --load --platform linux/arm64 \
-t qt-helloworld:arm64 \
--build-arg IMAGE_ARCH=arm64 \
--build-arg APP_ROOT=/home/torizon/app \
--build-arg GPU=-am62 .
Step 4: Deploying to the Device
docker save/load method
# Save the image to tar on the host and load it onto the device
docker save qt-helloworld:arm64 | ssh torizon@<device-ip> 'docker load'
# Execute on the device
ssh torizon@<device-ip> \
'docker run --rm \
-v /dev:/dev -v /tmp:/tmp -v /run/udev:/run/udev:ro \
--device-cgroup-rule="c 4:* rmw" \
--device-cgroup-rule="c 13:* rmw" \
--device-cgroup-rule="c 199:* rmw" \
--device-cgroup-rule="c 226:* rmw" \
qt-helloworld:arm64'
High-speed deployment (transferring binary only)
If the Qt base image is already on the device, you can transfer only the application binary and execute it on top of the existing image. Since QML resources are embedded in the binary, only one file needs to be transferred.
# Build in the SDK container on the host
docker run --rm --platform linux/arm64 \
-v $(pwd):/app -w /app \
localhost:5002/torizon-qt6-base:arm64 \
sh -c 'cmake -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=/opt/Qt/current/lib/cmake \
-Bbuild && cmake --build build'
# Transfer the binary
scp build/bin/HelloWorld torizon@<device-ip>:~/
# Execute using the device's base image
ssh torizon@<device-ip> \
"docker run --rm --privileged \
-v /dev:/dev -v /tmp:/tmp -v /run/udev:/run/udev:ro \
-v /home/torizon/HelloWorld:/app/HelloWorld \
localhost:5002/torizon-qt6-base:arm64 /app/HelloWorld"
Device Access Required for the Container
EGLFS accesses DRM/KMS and input devices directly. The container requires the following permissions:
volumes:
- /dev:/dev
- /tmp:/tmp
- /run/udev:/run/udev:ro
device_cgroup_rules:
- "c 4:* rmw" # tty
- "c 13:* rmw" # input devices
- "c 199:* rmw" # GPU (Vivante)
- "c 226:* rmw" # DRM
While --privileged works, we recommend using cgroup rules for minimum required permissions in production environments.
Specific Considerations for GPU
AM62 (PowerVR / Mesa)
Compatible with Mesa GLES and GLVND. No specific issues.
iMX8MP (Vivante)
-
-opengl es2is mandatory: Because GLVND is not supported, Qt must link directly tolibGLESv2.so. -
Do not overwrite Vivante libraries: Installing Mesa packages via
apt-get installwill overwrite Vivante libraries, breaking GPU functionality. - DRM device configuration: card0 = imx-drm (display), card1 = galcore (GPU), renderD128 = GPU render node. Since Vivante's GBM handles PRIME buffer sharing internally, no changes to Qt settings are required.
- DDK version: TorizonOS 7.5.0 uses DDK 25.2. If the base image's DDK version does not match, it will fall back to LLVMpipe (software rendering).
Adding Qt Modules
To add Qt modules to the base image, add a RUN block to Dockerfile.qt6-base.
# Example: Adding qtcanvaspainter
RUN git clone --depth 1 --branch ${QT_REF} \
https://code.qt.io/qt/qtcanvaspainter.git /tmp/qtcanvaspainter && \
cd /tmp/qtcanvaspainter && \
/opt/Qt/${QT_REF}/gcc_arm64/bin/qt-configure-module . && \
cmake --build . --parallel ${PARALLEL_JOBS:-$(nproc)} && \
cmake --install . && \
rm -rf /tmp/qtcanvaspainter
The pattern is the same for all modules: git clone → qt-configure-module → cmake --build → cmake --install. Be aware of dependencies (e.g., qtdeclarative requires qtshadertools first).
Auto-starting the Application
To auto-start the application upon device power-on, utilize the Docker Compose mechanism built into TorizonOS.
docker-compose method (Recommended)
TorizonOS includes a systemd service that automatically executes docker-compose. A Compose file placed at /var/sota/storage/docker-compose/docker-compose.yml is automatically executed at startup.
# Create directory if it does not exist
sudo mkdir -p /var/sota/storage/docker-compose
# Deploy docker-compose.yml
sudo tee /var/sota/storage/docker-compose/docker-compose.yml > /dev/null <<'EOF'
version: "3.9"
services:
qt-app:
image: qt-helloworld:arm64
restart: always
volumes:
- /dev:/dev
- /tmp:/tmp
- /run/udev:/run/udev:ro
device_cgroup_rules:
- "c 4:* rmw"
- "c 13:* rmw"
- "c 199:* rmw"
- "c 226:* rmw"
EOF
The application will be automatically executed from the next boot.
Restart Policy
You can control the container restart behavior using the restart parameter.
| Policy | Behavior |
|---|---|
no |
Does not restart automatically (default). |
on-failure |
Restarts only on abnormal exit. |
always |
Always restarts when stopped. |
unless-stopped |
Restarts except when manually stopped. |
For production environments, specify always or unless-stopped.
Mass Production Deployment
While the above method is sufficient for development, for mass production, you can use TorizonCore Builder to create a custom TorizonOS image that incorporates the application container image. This allows the container to start automatically just by flashing the same image to multiple devices.
See How to Autorun an Application with Torizon OS for details.
Conclusion
- By layering a source-built version of the latest Qt on top of the official Toradex base image, you can use the latest features while maintaining compatibility with GPU drivers.
- Through QEMU user-mode + Docker buildx, you can complete ARM64 native builds entirely on your host PC.
- You can generate images for multiple boards from the same Dockerfile just by switching the
GPUbuild argument. -
-opengl es2is mandatory for non-GLVND environments like Vivante, and since it works seamlessly in Mesa environments as well, it is a safe and unified choice.
Discussion