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:
- le informazioni per la compilazione propriamente detta, ad esempio indica quali librerie linkare;
- le informazioni non necessarie per la compilazione ma che servono durante il progetto, ad esempio quali sono i file delle traduzioni oppure come si effettua il deploy dell’applicazione.
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:
TEMPLATE = app
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
. 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)
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
:
- subdirs: usato per specificare che il progetto è suddiviso in sotto directory;
- lib: usato per indicare un progetto che genera una libreria dinamica;
- staticlib: usato per indicare un progetto che genera una libreria statica.
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.
Rimani aggiornato!
Ricevi novità su Develer e sulle tecnologie che utilizziamo iscrivendoti alla nostra newsletter.
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 e undocumented qmake.