Eine häufige Anforderung bei Embedded-Applikationen ist der Zugriff auf Datenbanken – sei es zum Lesen von Sensordaten oder zum Speichern von Benutzerkonfigurationen.
Qt bringt mit seinem QtSQL-Modul eine solide Grundlage dafür mit. Allerdings ist die Einbindung von SQL-Treibern unter Yocto nicht ganz trivial, da diese nicht standardmäßig mitgebaut werden – meist aufgrund lizenzrechtlicher Einschränkungen.
In diesem Beitrag zeige ich, wie man mit Yocto ein Image erstellt, das SQL-Treiber für MySQL/MariaDB und ODBC enthält – inklusive einer Demo-Anwendung, die auch Zugriff auf einen Microsoft SQL Server via FreeTDS demonstriert.
SQL-Unterstützung im QtSQL-Modul aktivieren
Im Layer meta-qt6, konkret in qtbase_git.bb, findet sich eine Konfigurationsmöglichkeit für optionale SQL-Treiber:
~/yocto/poky-kirkstone/meta-qt6/recipes-qt/qt6/gtbase_git.bb
...
# sqldrivers
PACKAGECONFIG[sql-mysql] = "-DFEATURE_sql_mysql=ON,-DFEATURE_sql_mysql=OFF,mysql5"
PACKAGECONFIG[sql-odbc] = "-DFEATURE_sql_odbc=ON,-DFEATURE_sql_odbc=OFF,unixodbc"
PACKAGECONFIG[sql-psql] = "-DFEATURE_sql_psql=ON,-DFEATURE_sql_psql=OFF,postgresql"
PACKAGECONFIG[sql-sqlite] = "-DFEATURE_system_sqlite=ON,-DFEATURE_sql_sqlite=OFF,sqlite3"
...
Um beispielsweise MySQL und ODBC zu aktivieren, erweitern wir die Variable PACKAGECONFIG entsprechend.
Für eine bessere Übersicht erstelle ich im Layer meta-raspilab folgende Struktur:
mkdir -p ~/yocto/poky-kirkstone/meta-raspilab/recipes-support/qt6
In diesem Datei erstelle ich nun die Datei qtbase_git.bbappend mit folgendem Inhalt.
~/yocto/poky-kirkstone/meta-raspilab/recipes-support/qt6/qtbase_git.bbappend
PACKAGECONFIG += "sql-mysql"
PACKAGECONFIG += "sql-odbc"
Damit werden automatisch unixodbc, libmysqlclient (bzw. mariadb) sowie die benötigten Qt-Treiber mit ins Image aufgenommen.
Microsoft SQL Server per ODBC/FreeTDS
ODBC ist eine standardisierte Schnittstelle zum Zugriff auf Datenbanken verschiedenster Hersteller.
Microsoft stellt zwar einen eigenen ODBC-Treiber für Linux zur Verfügung, dieser liegt jedoch nur als Binary-Paket vor und ist weder quelloffen noch für ARM-Architekturen verfügbar – und somit für Yocto nicht nutzbar.
Die Lösung: FreeTDS – ein Open-Source-Treiber für das TDS-Protokoll (Tabular Data Stream), das u. a. von Microsoft SQL Server und Sybase verwendet wird.
Da das Rezept aus meta-parallel-php teils veraltete oder problematische Konfigurationen verwendet, stelle ich ein eigenes Rezept in meta-raspilab in folgender Verzeichnisstrutur bereit.
mkdir -p ~/yocto/poky-kirkstone/meta-raspilab/recipes-support/freetds/files
~/yocto/poky-kirkstone/meta-raspilab/recipes-support/freetds/freetds_1.4.26.bb
DESCRIPTION = "Free implementation of Microsoft/Sybase wire protocol for databases"
HOMEPAGE = "http://www.freetds.org"
SECTION = "console/network"
LICENSE = "LGPL-2.0-only"
LIC_FILES_CHKSUM = "file://COPYING_LIB.txt;md5=5f30f0716dfdd0d91eb439ebec522ec2"
DEPENDS = "gnutls libgcrypt unixodbc"
BBCLASSEXTEND = "native"
SRC_URI = " \
http://ftp.freetds.org/pub/freetds/stable/freetds-${PV}.tar.bz2;sha256sum=74641a66cc2bfae302c2a64a4b268a3db8fb0cc7364dc7975c44c57d65cd8d1c \
file://odbcinst.ini.append \
"
S = "${WORKDIR}"
inherit autotools pkgconfig
export LIBS="-lgcrypt"
do_install:append() {
# Template-Dateien installieren (nur für postinst)
install -d ${D}${datadir}/freetds
install -m 0644 ${WORKDIR}/odbcinst.ini.append ${D}${datadir}/freetds/odbcinst.ini.append
}
pkg_postinst_ontarget:${PN}() {
echo "Appending ODBC Driver at runtime..."
if [ -f /etc/odbcinst.ini ]; then
cat ${datadir}/freetds/odbcinst.ini.append >> /etc/odbcinst.ini
else
cp ${datadir}/freetds/odbcinst.ini.append /etc/odbcinst.ini
fi
}
EXTRA_OECONF = " \
--enable-sspi \
--enable-msdblib \
--enable-sybase-compat \
--with-tdsver=auto \
--with-gnutls \
--disable-libiconv \
"
FILES:${PN} += " \
${datadir}/freetds \
${sysconfdir}/odbcinst.ini \
"
Treiber-Registrierung
ODBC benötigt zwei zentrale Konfigurationsdateien, die für den Betrieb unerlässlich sind, odbcinst.ini und odbc.ini. Das Rezept bringt den Teil zur Registrierung des Treibers in der /etc/odbcinst.ini direkt mit
Die Datei odbcinst.ini enthält die Definitionen der verfügbaren ODBC-Treiber auf dem System.
Jeder Treiber (z. B. FreeTDS, PostgreSQL, SQLite) wird hier mit Name und Pfad zur .so-Datei registriert.
Bereitstellung unter Yocto
Da unixodbc bereits beim Paketbau die datei /etc/odbcinst.ini anlegt, dürfen wir sie nicht direkt überschreiben. Stattdessen wird eine Datei odbcinst.ini.append erstellt, die im Recipe freetds enthalten ist. Diese Datei wird bei erster Ausführung auf dem Zielsystem per pkg_postinst_ontarget an /etc/odbcinst.ini angehängt, ohne bestehende Inhalte zu löschen.
~/yocto/poky-kirkstone/meta-raspilab/recipes-support/freetds/files/odbcinst.ini.append
[FreeTDS]
Description = TDS driver (Sybase/MS SQL)
Driver = /usr/lib/libtdsodbc.so.0
UsageCount = 1
Warum nicht direkt /etc/odbc*.ini ersetzen?
Yocto erlaubt es nicht, systemeigene Dateien anderer Pakete direkt zu überschreiben. Würde man das tun, käme es zu QA-Fehlern im Buildprozess (installed-vs-shipped). Durch die Nutzung von *.append-Dateien und Laufzeit-Erweiterung bleibt das System paketkonform und wartbar.
Qt-Demoanwendung für SQL-Datenbanken
Zur Verifikation verwende ich eine kleine Qt6-Anwendung, die sich via ODBC mit einem MS SQL Server verbindet. Für die Demoanwendung erstelle ich unter recipes-applications ein Verzeichnis sqldemo in dem sich alle Dateien zum erstellen der Anwendung befinden.
mkdir -p ~/yocto/poky-kirkstone/meta-raspilab/recipes-applications/sqldemo/files
~/yocto/poky-kirkstone/meta-raspilab/recipes-applications/sqldemo/sqldemo_0.1.bb
SUMMARY = "SQL Demo"
DESCRIPTION = "This is a SQL Demo with Qt6."
LICENSE = "CLOSED"
SRC_URI = " \
file://sqldemo.pro \
file://main.cpp \
file://mainwindow.cpp \
file://mainwindow.h \
file://mainwindow.ui \
file://odbc.ini.append \
"
DEPENDS += "qtbase wayland"
RDEPENDS:${PN} += "qtwayland freetds"
inherit qt6-qmake
do_install:append() {
# Template-Dateien installieren (nur für postinst)
install -d ${D}${datadir}/sqldemo
install -m 0644 ${WORKDIR}/odbc.ini.append ${D}${datadir}/sqldemo/odbc.ini.append
install -d ${D}${bindir}
install -m 0775 sqldemo ${D}${bindir}
}
pkg_postinst_ontarget:${PN}() {
echo "Appending ODBC config at runtime..."
if [ -f /etc/odbc.ini ]; then
cat ${datadir}/sqldemo/odbc.ini.append >> /etc/odbc.ini
else
cp ${datadir}/sqldemo/odbc.ini.append /etc/odbc.ini
fi
}
FILES:${PN} += " \
${bindir}/sqldemo \
${datadir}/sqldemo \
${sysconfdir}/odbc.ini \
"
DSN-Konfiguration
Die Datei /etc/odbc.ini beschreibt die Datenquellen (DSNs), mit denen Programme über ODBC kommunizieren können.
Bereitstellung unter Yocto
Auch hier wird die Datei nicht direkt ersetzt. In der Demo-Anwendung (sqldemo) liegt eine Datei odbc.ini.append. Diese wird zur Laufzeit beim ersten Systemstart (ebenfalls via pkg_postinst_ontarget) an /etc/odbc.ini angehängt.
~/yocto/poky-kirkstone/meta-raspilab/recipes-applications/sqldemo/files/odbc.ini.append
[WinSRV02]
Driver=FreeTDS
Description=Test2 database
Trace=No
Server=192.168.2.120
Port=1433
Database=Test2
Die wichtigsten Informationen sind die DSN (WinSRV02), die IP-Adresse und die zu verwendende Datenbank. Benutzername und Passwort gebe ich hier nicht an, die Daten kann ich später in der Anwendung angeben.
Die Quelltexte der Anwendung hänge ich gleich nach dem Screenshot an diesen Beitrag, sie können aber auch auf meinem Github-Repository gefunden werden.
~/yocto/poky-kirkstone/meta-raspilab/recipes-applications/sqldemo/files/sqldemo.pro
QT += core gui sql
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++17
# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp \
mainwindow.cpp
HEADERS += \
mainwindow.h
FORMS += \
mainwindow.ui
~/yocto/poky-kirkstone/meta-raspilab/recipes-applications/sqldemo/files/main.cpp
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
~/yocto/poky-kirkstone/meta-raspilab/recipes-applications/sqldemo/files/mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QSqlDatabase>
#include <QSqlQuery>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow {
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
private slots:
bool ConnectDatabase();
bool CloseDatabase();
};
#endif // MAINWINDOW_H
~/yocto/poky-kirkstone/meta-raspilab/recipes-applications/sqldemo/files/mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QTime>
#include <QDebug>
#include <QSqlError>
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) {
ui->setupUi(this);
QStringList dbDrivers = QSqlDatabase::drivers();
ui->sbDrivers->insertItems(0, dbDrivers);
connect(ui->pbConnect, &QPushButton::clicked, this, &MainWindow::ConnectDatabase);
connect(ui->pbDisconnect, &QPushButton::clicked, this, &MainWindow::CloseDatabase);
}
bool MainWindow::ConnectDatabase() {
bool ok = false;
QSqlDatabase db = QSqlDatabase::addDatabase(ui->sbDrivers->currentText());
db.setConnectOptions("SQL_ATTR_LOGIN_TIMEOUT=5");
db.setConnectOptions("SQL_ATTR_CONNECTION_TIMEOUT=5");
db.setConnectOptions("MYSQL_OPT_CONNECT_TIMEOUT=5");
db.setHostName(ui->leServer->text());
db.setDatabaseName(ui->leDatabase->text());
db.setUserName(ui->leUser->text());
db.setPassword(ui->lePassword->text());
ok = db.open();
if (!ok) {
ui->laStatus->setText("getrennt");
QSqlError error = db.lastError();
qDebug() << "DatabaseText:" << error.databaseText();
qDebug() << "DriverText:" << error.driverText();
} else {
qDebug() << "Database connected";
ui->laStatus->setText("verbunden");
}
ui->pbConnect->setEnabled(!ok);
ui->pbDisconnect->setEnabled(ok);
return ok;
}
bool MainWindow::CloseDatabase() {
bool ok = false;
QSqlDatabase db = QSqlDatabase::database();
if (db.isOpen()) {
qDebug() << "Disconnect";
db.close();
}
ok = !db.isOpen();
if (ok) {
qDebug() << "getrennt";
QSqlDatabase::removeDatabase(db.connectionName());
ui->laStatus->setText("getrennt");
ui->pbConnect->setEnabled(true);
ui->pbDisconnect->setEnabled(false);
} else {
ui->laStatus->setText("verbunden");
ui->pbConnect->setEnabled(false);
ui->pbDisconnect->setEnabled(true);
}
return ok;
}
MainWindow::~MainWindow() {
delete ui;
}
~/yocto/poky-kirkstone/meta-raspilab/recipes-applications/sqldemo/files/mainwindow.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>428</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="4" column="0">
<widget class="QLabel" name="laPassword">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Passwort</string>
</property>
</widget>
</item>
<item row="6" column="0" colspan="3">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="pbConnect">
<property name="text">
<string>Verbinden</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pbDisconnect">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Trennen</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="laStatus">
<property name="text">
<string>getrennt</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="laServer">
<property name="text">
<string>Server</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="leDrivers">
<property name="text">
<string>Treiber</string>
</property>
</widget>
</item>
<item row="8" column="0" colspan="3">
<widget class="QPushButton" name="pbExit">
<property name="text">
<string>Verlassen</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="3">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>200</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="0">
<widget class="QLabel" name="laDatabase">
<property name="text">
<string>Datenbank</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QLineEdit" name="leUser"/>
</item>
<item row="1" column="2">
<widget class="QLineEdit" name="leServer"/>
</item>
<item row="2" column="2">
<widget class="QLineEdit" name="leDatabase"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="laUser">
<property name="text">
<string>Benutzername</string>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QLineEdit" name="lePassword">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QComboBox" name="sbDrivers"/>
</item>
</layout>
</widget>
</widget>
<tabstops>
<tabstop>sbDrivers</tabstop>
<tabstop>leServer</tabstop>
<tabstop>leDatabase</tabstop>
<tabstop>leUser</tabstop>
<tabstop>lePassword</tabstop>
<tabstop>pbConnect</tabstop>
<tabstop>pbDisconnect</tabstop>
<tabstop>pbExit</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>pbExit</sender>
<signal>clicked()</signal>
<receiver>MainWindow</receiver>
<slot>close()</slot>
<hints>
<hint type="sourcelabel">
<x>399</x>
<y>406</y>
</hint>
<hint type="destinationlabel">
<x>399</x>
<y>213</y>
</hint>
</hints>
</connection>
</connections>
</ui>
