Un build system è un programma che aiuta nella compilazione di progetti C++ complessi. Pochissime persone considerano interessante saper usare un build system, ma tutti gli sviluppatori devono conviverci e quindi è necessario conoscerlo.

Ogni sviluppatore C++ si è imbattuto prima o poi in un build system e le reazioni variano dalla noia fino a vera e propria rabbia e frustrazione. In questa guida voglio presentare le funzionalità principali di qmake con esempi concreti per aiutarvi a comprendere file già scritti e modificarli senza che questo influisca sulla vostra salute.

Concetti principali

Le librerie Qt sono nate usando il loro sistema di build, chiamato qmake. Per molto tempo qmake è stato legato a doppio filo con le librerie Qt, ma oggi esistono anche altri sistemi di build che “capiscono” Qt come CMake. Qmake rimane comunque molto diffuso e per il momento è il sistema di build con cui le Qt stesse sono compilate.

Un file di progetto qmake è il punto centrale di un’applicazione Qt e contiene:

Un file di progetto è per sua natura dichiarativo, ossia per la gran parte sarà composto da assegnamenti di variabili e poco altro. Un progetto minimale è il seguente:

TARGET = myApp
QT = core
SOURCES += main.cpp

Questo file di progetto descrive un eseguibile finale (riga 1), il nome dell’eseguibile è myApp (riga 2), usa soltanto QtCore come libreria Qt (riga 3) e la lista dei sorgenti da compilare è composta soltanto dal file main.cpp (riga 4). Lanciando qmake su questo file, otteremo un Makefile che semplicemente compilerà main.cpp e lo linkerà con la versione di Qt con cui qmake è collegato.

Una cosa importante da sapere è che qmake sa con quale versione di Qt è stato compilato e sa anche il path dove le Qt sono installate, quindi è in grado di generare un Makefile giusto. Supponiamo di avere due versioni di Qt installate, le 5.9 e le 5.12. Per “compilare” il programma con la versione “corretta” di Qt basterà usare il qmake giusto.

Un altro programma fondamentale per le applicazioni Qt è il moc (https://doc.qt.io/qt-5/moc.html). Qmake analizza tutti i file presenti nella variabile HEADERS e lancia il binario moc se almeno una classe contiene la macro Q_OBJECT:

HEADERS += fileA.h fileB.h

Altri programmi importanti sono lupdate, lrelease, rcc e uic. Per far sì che vengano lanciati correttamente si devono elencare tutti i file di risorse, i file UI ed i file di traduzioni presenti nel progetto:

TRANSLATIONS += myapp_it.ts
RESOURCES += qml.qrc
UI += mywidget.ui

I file UI verranno compilati con lo UI compiler (uic), i file di risorse con rcc mentre le traduzioni verranno aggiornate lanciando lupdate.

Develer è partner Qt certificato

Moduli e librerie

Per usare moduli aggiuntivi oltre QtCore si usa la variabile QT, come ad esempio:

QT += quick network sql

In questo caso abbiamo specificato che l’applicazione userà i moduli Qt Quick, QtNetwork e QtSql. I valori di queste variabili sono indicate all’interno della documentazione di ogni classe Qt, in alto nella pagina (per esempio, https://doc.qt.io/qt-5/qnetworkaccessmanager.html)

Le variabili LIBS e INCLUDEPATH si usano per specificare la dipendenza da una libreria dinamica. Le directory che contengono i file di include sono specificate dentro INCLUDEPATH, mentre il path della libreria ed il nome vanno specificati dentro LIBS.

INCLUDEPATH += /path/to/include
LIBS += -L/path/to/library -lmylib

Su progetti di grandi dimensioni si ha la necessità di gestire anche le dipendenze del progetto, come ad esempio gli unit test oppure le librerie in cui il progetto è suddiviso. A questo scopo si usano valori diversi per la variabile TEMPLATE:

Facciamo un esempio: supponiamo di avere un progetto composto dall’applicazione principale (magari scritta in Qt Quick), una libreria e degli unit test. Sia l’applicazione principale che gli unit test usano la libreria. Il file di progetto sarà questo:

TEMPLATE = subdirs
SUBDIRS = lib \
    app \
    test
app.depends = lib
test.depends = lib

Con questa sintassi si specificano esplicitamente le dipendenze tra sotto directory affinché qmake generi il corretto ordine di build. In alternativa, si può usare CONFIG += ordered, che esplicita che le variabili elencate dentro SUBDIRS devono essere compilate in ordine. Quando si usa un progetto di tipo subdirs è fondamentale che tutte le stringhe indicate nella variabile SUBDIRS siano effettivamente delle sotto directory rispetto a dove si trova il file di progetto.

È possibile gestire anche dipendenze di librerie esterne alla directory del progetto, ma l’argomento è complesso e trattato esaustivamente nella documentazione ufficiale di qmake.

Compilazione condizionale

Un’altra funzionalità utile è la compilazione condizionale, che consente di specificare regole da applicare soltanto se sono vere alcune condizioni.

La compilazione condizionale è molto utile quando il programma deve essere multi piattaforma ma Qt non fornisce una determinata funzionalità; un esempio potrebbe essere leggere la potenza del segnale wifi della rete a cui siamo collegati. Il modo più elegante per supportare le piattaforme Windows e Linux è creare due file di implementazione diversi, uno per piattaforma, e usare il file di progetto per specificare quale compilare in ogni piattaforma:

HEADERS += wifi_power.h
win32 {
    SOURCES += wifi_power_win.cpp
}
unix {
    SOURCES += wifi_power_unix.cpp
}

Il blocco win32 { ... } viene eseguito soltanto quando si usa un qmake che ha per target Windows.

Le stringhe unix e win32 sono predefinite in qmake, ma è possibile crearne di personalizzate. Ogni stringa presente nella variabile CONFIG diventa un valore testabile. Per esempio, supponiamo di avere una libreria usata in prodotti differenti, alcuni con l’hardware wifi a bordo altri senza. Per abilitare le funzionalità wifi si può scrivere nel file di progetto:

wifi_supported {
    HEADERS += wifi_configuration.h wifi_handler.h
    SOURCES += wifi_configuration.cpp wifi_handler.cpp
}

e lanciare qmake in questo modo:

qmake CONFIG+=wifi_supported myapp.pro

La variabile CONFIG può essere impostata sulla riga di comando di qmake, in questo modo è possibile personalizzare la compilazione della libreria dall’esterno del file di progetto.

Conclusioni

Questa guida descrive le principali funzionalità di qmake con esempi concreti che mi auguro siano di aiuto a chiarire in quali casi usare vanno usate. Qmake fornisce anche altre funzionalità, come ad esempio test di più condizioni contemporaneamente, supporto per pkg-config o la possibilità di scrivere funzioni di test personalizzate. Ulteriori dettagli si possono trovare nella documentazione di qmake (https://doc.qt.io/qt-5/qmake-manual.html) e undocumented qmake (https://wiki.qt.io/Undocumented_QMake).