Una guida vitale a Qmake

Pubblicato: 2022-03-11

introduzione

qmake è uno strumento di sistema di compilazione fornito con la libreria Qt che semplifica il processo di compilazione su piattaforme diverse. A differenza di CMake e Qbs , qmake faceva parte di Qt sin dall'inizio e deve essere considerato uno strumento "nativo". Inutile dire che l'IDE predefinito di Qt, Qt Creator , ha il miglior supporto di qmake pronto all'uso. Sì, puoi anche scegliere i sistemi di build CMake e Qbs per un nuovo progetto lì, ma questi non sono così ben integrati. È probabile che il supporto di CMake in Qt Creator sarà migliorato nel tempo, e questo sarà un buon motivo per pubblicare la seconda edizione di questa guida, rivolta specificamente a CMake. Anche se non hai intenzione di usare Qt Creator, potresti comunque considerare qmake come un secondo sistema di build nel caso tu stia costruendo librerie pubbliche o plugin. Praticamente tutte le librerie o i plug-in basati su Qt di terze parti forniscono file qmake utilizzati per integrarsi perfettamente nei progetti basati su qmake. Solo alcuni di essi forniscono una doppia configurazione, ad esempio qmake e CMake. Potresti preferire usare qmake se ti interessa quanto segue:

  • Stai costruendo un progetto multipiattaforma basato su Qt
  • Stai usando Qt Creator IDE e la maggior parte delle sue funzionalità
  • Stai costruendo una libreria/plugin standalone da usare da altri progetti qmake

Questa guida descrive le funzionalità più utili di qmake e fornisce esempi reali per ciascuna di esse. I lettori che non conoscono Qt possono utilizzare questa guida come tutorial per il sistema di build di Qt. Gli sviluppatori Qt possono trattarlo come un libro di cucina quando iniziano un nuovo progetto o possono applicare selettivamente alcune delle funzionalità a qualsiasi progetto esistente con un basso impatto.

Un'illustrazione del processo di compilazione di qmake

Utilizzo di base di Qmake

La specifica qmake è scritta in .pro ("progetto"). Questo è un esempio del file .pro più semplice possibile:

 SOURCES = hello.cpp

Per impostazione predefinita, questo creerà un Makefile che creerà un eseguibile dal singolo file di codice sorgente hello.cpp .

Per costruire il binario (eseguibile in questo caso), devi prima eseguire qmake per produrre un Makefile e poi make (o nmake , o mingw32-make a seconda della tua toolchain) per costruire il target.

In poche parole, una specifica qmake non è altro che un elenco di definizioni di variabili mescolate con istruzioni di flusso di controllo opzionali. Ogni variabile, in generale, contiene un elenco di stringhe. Le istruzioni del flusso di controllo consentono di includere altri file di specifica qmake, controllare le sezioni condizionali e persino chiamare funzioni.

Comprendere la sintassi delle variabili

Quando apprendi progetti qmake esistenti, potresti essere sorpreso di come è possibile fare riferimento a diverse variabili: \(VAR,\){VAR} o $$(VAR)

Usa questo mini cheat sheet mentre adotti le regole:

  • VAR = value Assegna un valore a VAR
  • VAR += value Aggiunge valore all'elenco VAR
  • VAR -= value Rimuove il valore dall'elenco VAR
  • $$VAR o $${VAR} Ottiene il valore di VAR nel momento in cui qmake è in esecuzione
  • $(VAR) Contenuto di un ambiente VAR nel momento in cui Makefile (non qmake) è in esecuzione
  • $$(VAR) Contenuto di un ambiente VAR nel momento in cui qmake (non Makefile) è in esecuzione

Modelli comuni

L'elenco completo delle variabili qmake può essere trovato nelle specifiche: http://doc.qt.io/qt-5/qmake-variable-reference.html

Esaminiamo alcuni modelli comuni per i progetti:

 # Windows application TEMPLATE = app CONFIG += windows # Shared library (.so or .dll) TEMPLATE = lib CONFIG += shared # Static library (.a or .lib) TEMPLATE = lib CONFIG += static # Console application TEMPLATE = app CONFIG += console

Basta aggiungere SOURCES += … e HEADERS += … per elencare tutti i file del codice sorgente e il gioco è fatto.

Finora, abbiamo esaminato modelli molto semplici. I progetti più complessi di solito includono diversi sottoprogetti con dipendenze l'uno dall'altro. Vediamo come gestirlo con qmake.

Sottoprogetti

Il caso d'uso più comune è un'applicazione fornita con una o più librerie e progetti di test. Considera la seguente struttura:

 /project ../library ..../include ../library-tests ../application

Ovviamente, vogliamo essere in grado di costruire tutto in una volta, in questo modo:

 cd project qmake && make

Per raggiungere questo obiettivo, abbiamo bisogno di un file di progetto qmake nella cartella /project :

 TEMPLATE = subdirs SUBDIRS = library library-tests application library-tests.depends = library application.depends = library

NOTA: l'uso di CONFIG += ordered è considerato una cattiva pratica: preferisci usare .depends invece.

Questa specifica indica a qmake di creare prima un sottoprogetto di libreria poiché altri target dipendono da esso. Quindi può creare library-tests e l'applicazione in un ordine arbitrario perché questi due sono dipendenti.

La struttura della directory del progetto

Biblioteche di collegamento

Nell'esempio sopra, abbiamo una libreria che deve essere collegata all'applicazione. In C/C++, questo significa che abbiamo bisogno di avere alcune altre cose configurate:

  1. Specificare -I per fornire percorsi di ricerca per le direttive #include.
  2. Specificare -L per fornire percorsi di ricerca per il linker.
  3. Specificare -l per fornire quale libreria deve essere collegata.

Poiché vogliamo che tutti i sottoprogetti siano mobili, non possiamo utilizzare percorsi assoluti o relativi. Ad esempio, non lo faremo: INCLUDEPATH += ../library/include e ovviamente non possiamo fare riferimento al binario della libreria (file .a) da una cartella di build temporanea. Seguendo il principio della "separazione delle preoccupazioni", possiamo rapidamente renderci conto che il file di progetto dell'applicazione deve astrarsi dai dettagli della libreria. Invece, è responsabilità della libreria dire dove trovare i file di intestazione, ecc.

Sfruttiamo la direttiva include() di qmake per risolvere questo problema. Nel progetto della libreria, aggiungeremo un'altra specifica i in un nuovo file con estensione .pri (l'estensione può essere qualsiasi cosa, ma qui sta per include). Quindi, la libreria avrebbe due specifiche: library.pro e library.pri . Il primo viene utilizzato per costruire la libreria, il secondo viene utilizzato per fornire tutti i dettagli necessari a un progetto di consumo.

Il contenuto del file library.pri sarebbe il seguente:

 LIBTARGET = library BASEDIR = $${PWD} INCLUDEPATH *= $${BASEDIR}/include LIBS += -L$${DESTDIR} -llibrary

BASEDIR specifica la cartella del progetto della libreria (per l'esattezza, la posizione del file delle specifiche qmake corrente, che nel nostro caso è library.pri ). Come puoi immaginare, INCLUDEPATH verrà valutato in /project/library/include . DESTDIR è la directory in cui il sistema di compilazione inserisce gli artefatti di output, come (.o .a .so .dll o .exe file). Questo di solito è configurato nel tuo IDE, quindi non dovresti mai supporre dove si trovano i file di output.

Nel file application.pro aggiungi semplicemente include(../library/library.pri) e il gioco è fatto.

Esaminiamo come viene costruito il progetto dell'applicazione in questo caso:

  1. Topmost project.pro è un progetto di sottodirectory. Ci dice che il progetto della biblioteca deve essere costruito prima. Quindi qmake entra nella cartella della libreria e la costruisce usando library.pro . A questo punto, library.a viene prodotto e inserito nella cartella DESTDIR .
  2. Quindi qmake entra nella sottocartella dell'applicazione e analizza il file application.pro . Trova la direttiva include(../library/library.pri) , che istruisce qmake a leggerla e interpretarla immediatamente. Ciò aggiunge nuove definizioni alle variabili INCLUDEPATH e LIBS , quindi ora il compilatore e il linker sanno dove cercare i file include, i binari della libreria e quale libreria collegare.

Abbiamo saltato la costruzione del progetto library-tests, ma è identico al progetto applicativo. Ovviamente, il nostro progetto di test dovrebbe anche collegare la libreria che dovrebbe testare.

Con questa configurazione, puoi facilmente spostare il progetto della libreria in un altro progetto qmake e includerlo, facendo quindi riferimento al file .pri . Questo è esattamente il modo in cui le librerie di terze parti vengono distribuite dalla comunità.

config.pri

È molto comune in un progetto complesso avere alcuni parametri di configurazione condivisi utilizzati da molti sottoprogetti. Per evitare duplicazioni, puoi nuovamente sfruttare la direttiva include() e creare config.pri nella cartella di primo livello. Potresti anche avere comuni "utilità" qmake condivise con i tuoi sotto-progetti, in modo simile a quanto discuteremo in seguito in questa guida.

Copiare gli artefatti in DESTDIR

Spesso i progetti hanno alcuni "altri" file che devono essere distribuiti insieme a una libreria o un'applicazione. Dobbiamo solo essere in grado di copiare tutti questi file in DESTDIR durante il processo di compilazione. Considera il seguente frammento:

 defineTest(copyToDestDir) { files = $$1 for(FILE, files) { DDIR = $$DESTDIR FILE = $$absolute_path($$FILE) # Replace slashes in paths with backslashes for Windows win32:FILE ~= s,/,\\,g win32:DDIR ~= s,/,\\,g QMAKE_POST_LINK += $$QMAKE_COPY $$quote($$FILE) $$quote($$DDIR) $$escape_expand(\\n\\t) } export(QMAKE_POST_LINK) }

Nota: utilizzando questo modello, puoi definire le tue funzioni riutilizzabili che funzionano sui file.

Inserisci questo codice in /project/copyToDestDir.pri in modo da poterlo include() in sottoprogetti impegnativi come segue:

 include(../copyToDestDir.pri) MYFILES += \ parameters.conf \ testdata.db ## this is copying all files listed in MYFILES variable copyToDestDir($$MYFILES) ## this is copying a single file, a required DLL in this example copyToDestDir($${3RDPARTY}/openssl/bin/crypto.dll)

Nota: DISTFILES è stato introdotto per lo stesso scopo, ma funziona solo in Unix.

Generazione di codice

Un ottimo esempio di generazione di codice come passaggio predefinito è quando un progetto C++ utilizza Google protobuf. Vediamo come possiamo iniettare l'esecuzione del protoc nel processo di compilazione.

Puoi facilmente trovare su Google una soluzione adatta, ma devi essere a conoscenza di un importante caso d'angolo. Immagina di avere due contratti, in cui A fa riferimento a B.

 A.proto <= B.proto

Se dovessimo generare prima il codice per A.proto (per produrre A.pb.h e A.pb.cxx ) e inviarlo al compilatore, fallirà semplicemente perché la dipendenza B.pb.h non esiste ancora. Per risolvere questo problema, dobbiamo superare tutte le fasi di generazione del codice proto prima di creare il codice sorgente risultante.

Ho trovato un ottimo snippet per questo compito qui: https://github.com/jmesmon/qmake-protobuf-example/blob/master/protobuf.pri

È uno script abbastanza grande, ma dovresti già sapere come usarlo:

 PROTOS = A.proto B.proto include(protobuf.pri)

Quando protobuf.pri , potresti notare il modello generico che può essere facilmente applicato a qualsiasi compilazione personalizzata o generazione di codice:

 my_custom_compiler.name = my custom compiler name my_custom_compiler.input = input variable (list) my_custom_compiler.output = output file path + pattern my_custom_compiler.commands = custom compilation command my_custom_compiler.variable_out = output variable (list) QMAKE_EXTRA_COMPILERS += my_custom_compiler

Ambiti e condizioni

Spesso è necessario definire dichiarazioni specifiche per una determinata piattaforma, come Windows o MacOS. Qmake offre tre indicatori di piattaforma predefiniti: win32, macx e unix. Ecco la sintassi:

 win32 { # add Windows application icon, not applicable to unix/macx platform RC_ICONS += icon.ico }

Gli ambiti possono essere annidati, possono utilizzare gli operatori ! , | e anche caratteri jolly:

 macx:debug { # include only on Mac and only for debug build HEADERS += debugging.h } win32|macx { HEADERS += windows_or_macx.h } win32-msvc* { # same as win32-msvc|win32-mscv.net }

Nota: Unix è definito su Mac OS! Se vuoi testare per Mac OS (non Unix generico), usa la condizione unix:!macx .

In Qt Creator, il debug e release delle condizioni dell'ambito non funzionano come previsto. Per farli funzionare correttamente, utilizzare il seguente schema:

 CONFIG(debug, debug|release) { LIBS += ... } CONFIG(release, debug|release) { LIBS += ... }

Funzioni utili

Qmake ha una serie di funzioni integrate che aggiungono più automazione.

Il primo esempio è la funzione files() . Supponendo che tu abbia un passaggio di generazione del codice che produce un numero variabile di file di origine. Ecco come includerli tutti in SOURCES :

 SOURCES += $$files(generated/*.c)

Questo troverà tutti i file con estensione .c nella sottocartella generated e li aggiungerà alla variabile SOURCES .

Il secondo esempio è simile al precedente, ma ora la generazione del codice ha prodotto un file di testo contenente i nomi dei file di output (elenco di file):

 SOURCES += $$cat(generated/filelist, lines)

Questo leggerà solo il contenuto del file e tratterà ogni riga come una voce per SOURCES .

Nota: l'elenco completo delle funzioni integrate può essere trovato qui: http://doc.qt.io/qt-5/qmake-function-reference.html

Trattare gli avvisi come errori

Il frammento di codice seguente utilizza la funzionalità di ambito condizionale descritta in precedenza:

 *g++*: QMAKE_CXXFLAGS += -Werror *msvc*: QMAKE_CXXFLAGS += /WX

Il motivo di questa complicazione è perché MSVC ha un flag diverso per abilitare questa opzione.

Generazione della versione Git

Il seguente snippet è utile quando è necessario creare una definizione del preprocessore contenente la versione SW corrente ottenuta da Git:

 DEFINES += SW_VERSION=\\\"$$system(git describe --always --abbrev=0)\\\"

Funziona su qualsiasi piattaforma purché sia ​​disponibile il comando git . Se usi i tag Git, questo visualizzerà il tag più recente, anche se il ramo è andato avanti. Modifica il comando git describe per ottenere l'output di tua scelta.

Conclusione

Qmake è un ottimo strumento incentrato sulla creazione di progetti multipiattaforma basati su Qt. In questa guida, abbiamo esaminato l'utilizzo di base degli strumenti e i modelli più comunemente utilizzati che manterranno la struttura del progetto flessibile e le specifiche di costruzione facili da leggere e mantenere.

Vuoi imparare a migliorare l'aspetto della tua app Qt? Prova: come ottenere forme di angoli arrotondati in C++ usando le curve di Bezier e QPainter: una guida passo passo