Un ghid vital pentru Qmake

Publicat: 2022-03-11

Introducere

qmake este un instrument de sistem de construire livrat cu biblioteca Qt care simplifică procesul de construire pe diferite platforme. Spre deosebire de CMake și Qbs , qmake a făcut parte din Qt încă de la început și va fi considerat un instrument „nativ”. Inutil să spunem că IDE-ul implicit al Qt — Qt Creator — are cel mai bun suport pentru qmake din cutie. Da, puteți alege și sistemele de construcție CMake și Qbs pentru un proiect nou acolo, dar acestea nu sunt atât de bine integrate. Este probabil ca suportul CMake în Qt Creator să fie îmbunătățit în timp, iar acesta va fi un motiv bun pentru a publica cea de-a doua ediție a acestui ghid, care vizează în mod special CMake. Chiar dacă nu intenționați să utilizați Qt Creator, este posibil să doriți totuși să considerați qmake ca un al doilea sistem de construcție în cazul în care construiți biblioteci publice sau pluginuri. Practic, toate bibliotecile sau pluginurile terțe bazate pe Qt furnizează fișiere qmake utilizate pentru a se integra fără probleme în proiecte bazate pe qmake. Doar câteva dintre ele oferă configurație duală, de exemplu, qmake și CMake. Este posibil să preferați să utilizați qmake dacă vi se aplică următoarele:

  • Construiți un proiect multiplatform bazat pe Qt
  • Utilizați Qt Creator IDE și majoritatea caracteristicilor sale
  • Construiți o bibliotecă/plugin autonomă pentru a fi utilizată de alte proiecte qmake

Acest ghid descrie cele mai utile caracteristici qmake și oferă exemple reale pentru fiecare dintre ele. Cititorii care sunt noi în Qt pot folosi acest ghid ca tutorial pentru sistemul de compilare al lui Qt. Dezvoltatorii Qt pot trata acest lucru ca pe o carte de bucate atunci când încep un nou proiect sau pot aplica selectiv unele dintre caracteristici oricăruia dintre proiectele existente cu impact scăzut.

O ilustrare a procesului de construire qmake

Utilizare de bază Qmake

Specificația qmake este scrisă în fișiere .pro („proiect”). Acesta este un exemplu de cel mai simplu fișier .pro posibil:

 SOURCES = hello.cpp

În mod implicit, aceasta va crea un Makefile care ar construi un executabil din fișierul unic de cod sursă hello.cpp .

Pentru a construi binarul (executabil în acest caz), trebuie să rulați mai întâi qmake pentru a produce un Makefile și apoi make (sau nmake , sau mingw32-make în funcție de lanțul dvs. de instrumente) pentru a construi ținta.

Pe scurt, o specificație qmake nu este altceva decât o listă de definiții de variabile amestecate cu instrucțiuni opționale de flux de control. Fiecare variabilă, în general, deține o listă de șiruri. Declarațiile de flux de control vă permit să includeți alte fișiere de specificații qmake, secțiuni condiționale de control și chiar funcții de apelare.

Înțelegerea sintaxei variabilelor

Când învățați proiecte qmake existente, s-ar putea să fiți surprins cum pot fi referite diferite variabile: \(VAR,\){VAR} sau $$(VAR) ...

Folosiți această mini-cheat-sheet în timp ce adoptați regulile:

  • VAR = value Atribuiți valoare lui VAR
  • VAR += value Adăugați valoarea la lista VAR
  • VAR -= value Eliminați valoarea din lista VAR
  • $$VAR sau $${VAR} Obține valoarea VAR în momentul în care qmake rulează
  • $(VAR) Conținutul unui mediu VAR în momentul în care se rulează Makefile (nu qmake).
  • $$(VAR) Conținutul unui mediu VAR în momentul în care qmake (nu Makefile) rulează

Șabloane comune

Lista completă a variabilelor qmake poate fi găsită în specificațiile: http://doc.qt.io/qt-5/qmake-variable-reference.html

Să analizăm câteva șabloane comune pentru proiecte:

 # 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

Doar adăugați SOURCES += … și HEADERS += … pentru a lista toate fișierele de cod sursă și ați terminat.

Până acum, am revizuit șabloane foarte de bază. Proiectele mai complexe includ de obicei mai multe sub-proiecte cu dependențe unul de celălalt. Să vedem cum să gestionăm asta cu qmake.

Subproiecte

Cel mai frecvent caz de utilizare este o aplicație care este livrată cu una sau mai multe biblioteci și proiecte de testare. Luați în considerare următoarea structură:

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

Evident, vrem să putem construi totul deodată, astfel:

 cd project qmake && make

Pentru a atinge acest obiectiv, avem nevoie de un fișier de proiect qmake în folderul /project :

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

NOTĂ: folosirea CONFIG += ordered este considerată o practică proastă — preferați utilizarea .depends .

Această specificație indică qmake să construiască mai întâi un sub-proiect de bibliotecă, deoarece alte obiective depind de el. Apoi poate construi library-tests și aplicația într-o ordine arbitrară, deoarece acestea două sunt dependente.

Structura directorului de proiect

Conectarea bibliotecilor

În exemplul de mai sus, avem o bibliotecă care trebuie conectată la aplicație. În C/C++, asta înseamnă că trebuie să avem mai multe lucruri configurate:

  1. Specificați -I pentru a furniza căi de căutare pentru directivele #include.
  2. Specificați -L pentru a furniza căi de căutare pentru linker.
  3. Specificați -l pentru a furniza ce bibliotecă trebuie conectată.

Deoarece dorim ca toate subproiectele să fie mobile, nu putem folosi căi absolute sau relative. De exemplu, nu vom face acest lucru: INCLUDEPATH += ../library/include și, desigur, nu putem face referire la binarul bibliotecii (fișier .a) dintr-un folder de compilare temporar. Urmând principiul „separării preocupărilor”, putem realiza rapid că fișierul de proiect al aplicației va face abstracție din detaliile bibliotecii. În schimb, este responsabilitatea bibliotecii să spună unde să găsească fișierele de antet etc.

Să folosim directiva include() a qmake pentru a rezolva această problemă. În proiectul de bibliotecă, vom adăuga o altă specificație qmake într-un fișier nou cu extensia .pri (extensia poate fi orice, dar aici i înseamnă include). Deci, biblioteca ar avea două specificații: library.pro și library.pri . Primul este folosit pentru a construi biblioteca, al doilea este folosit pentru a furniza toate detaliile necesare unui proiect consumator.

Conținutul fișierului library.pri ar fi următorul:

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

BASEDIR specifică folderul proiectului bibliotecii (mai exact, locația fișierului de specificații qmake curent, care este library.pri în cazul nostru). După cum ați putea ghici, INCLUDEPATH va fi evaluat la /project/library/include . DESTDIR este directorul în care sistemul de compilare plasează artefactele de ieșire, cum ar fi (fișierele .o .a .so .dll sau .exe). Acest lucru este de obicei configurat în IDE-ul dvs., așa că nu ar trebui să puneți niciodată presupuneri cu privire la locul în care se află fișierele de ieșire.

În fișierul application.pro doar adăugați include(../library/library.pri) și ați terminat.

Să analizăm modul în care proiectul aplicației este construit în acest caz:

  1. Topmost project.pro este un proiect subdirs. Ne spune că proiectul bibliotecii trebuie construit mai întâi. Deci qmake intră în folderul bibliotecii și îl construiește folosind library.pro . În această etapă, library.a este produs și plasat în folderul DESTDIR .
  2. Apoi qmake intră în subdosarul aplicației și analizează fișierul application.pro . Găsește directiva include(../library/library.pri) , care indică qmake să o citească și să o interpreteze imediat. Acest lucru adaugă noi definiții la variabilele INCLUDEPATH și LIBS , astfel încât acum compilatorul și linkerul știu unde să caute fișierele include, binarele bibliotecii și ce bibliotecă să le conecteze.

Am omis construirea proiectului bibliotecă-teste, dar este identic cu proiectul aplicației. Evident, proiectul nostru de testare ar trebui să conecteze și biblioteca pe care ar trebui să o testeze.

Cu această configurare, puteți muta cu ușurință proiectul de bibliotecă într-un alt proiect qmake și îl puteți include, făcând referire astfel la fișierul .pri . Exact așa sunt distribuite bibliotecile terțe de către comunitate.

config.pri

Este foarte comun pentru un proiect complex să aibă niște parametri de configurare partajați care sunt utilizați de multe sub-proiecte. Pentru a evita duplicarea, puteți utiliza din nou directiva include() și puteți crea config.pri în folderul de nivel superior. Este posibil să aveți, de asemenea, „utilități” comune qmake partajate sub-proiectelor, similar cu ceea ce vom discuta în continuare în acest ghid.

Copierea artefactelor în DESTDIR

Adesea, proiectele au câteva „alte” fișiere care trebuie distribuite împreună cu o bibliotecă sau o aplicație. Trebuie doar să putem copia toate astfel de fișiere în DESTDIR în timpul procesului de construire. Luați în considerare următorul fragment:

 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) }

Notă: Folosind acest model, vă puteți defini propriile funcții reutilizabile care funcționează pe fișiere.

Plasați acest cod în /project/copyToDestDir.pri , astfel încât să îl puteți include() în sub-proiectele solicitante, după cum urmează:

 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)

Notă: DISTFILES a fost introdus în același scop, dar funcționează numai în Unix.

Generarea codului

Un exemplu excelent de generare de cod ca pas pre-construit este atunci când un proiect C++ utilizează Google protobuf. Să vedem cum putem injecta execuția protoc în procesul de construire.

Puteți găsi cu ușurință pe Google o soluție potrivită, dar trebuie să fiți conștient de un caz de colț important. Imaginați-vă că aveți două contracte, în care A se referă la B.

 A.proto <= B.proto

Dacă am genera mai întâi cod pentru A.proto (pentru a produce A.pb.h și A.pb.cxx ) și l-am furniza compilatorului, va eșua deoarece dependența B.pb.h nu există încă. Pentru a rezolva acest lucru, trebuie să trecem toată etapa de generare a codului proto înainte de a construi codul sursă rezultat.

Am găsit un fragment grozav pentru această sarcină aici: https://github.com/jmesmon/qmake-protobuf-example/blob/master/protobuf.pri

Este un script destul de mare, dar ar trebui să știți deja cum să îl utilizați:

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

Când căutați în protobuf.pri , este posibil să observați modelul generic care poate fi aplicat cu ușurință oricărei compilații personalizate sau generare de cod:

 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

Domenii și condiții

Adesea, trebuie să definim declarații în mod specific pentru o anumită platformă, cum ar fi Windows sau MacOS. Qmake oferă trei indicatori de platformă predefiniti: win32, macx și unix. Iată sintaxa:

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

Scopurile pot fi imbricate, pot folosi operatori ! , | și chiar metacaracterele:

 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 }

Notă: Unix este definit pe Mac OS! Dacă doriți să testați pentru Mac OS (nu Unix generic), atunci utilizați condiția unix:!macx .

În Qt Creator, condițiile de debug și release ale domeniului de aplicare nu funcționează conform așteptărilor. Pentru ca acestea să funcționeze corect, utilizați următorul model:

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

Funcții utile

Qmake are o serie de funcții încorporate care adaugă mai multă automatizare.

Primul exemplu este funcția files() . Presupunând că aveți un pas de generare a codului care produce un număr variabil de fișiere sursă. Iată cum le puteți include pe toate în SOURCES :

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

Aceasta va găsi toate fișierele cu extensia .c în subdosarul generated și le vor adăuga la variabila SOURCES .

Al doilea exemplu este similar cu precedentul, dar acum generarea de cod a produs un fișier text care conține nume de fișiere de ieșire (lista de fișiere):

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

Acest lucru va citi doar conținutul fișierului și va trata fiecare linie ca o intrare pentru SOURCES .

Notă: Lista completă a funcțiilor încorporate poate fi găsită aici: http://doc.qt.io/qt-5/qmake-function-reference.html

Tratarea avertismentelor ca erori

Următorul fragment utilizează caracteristica de aplicare condiționată descrisă anterior:

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

Motivul acestei complicații este că MSVC are un indicator diferit pentru a activa această opțiune.

Se generează versiunea Git

Următorul fragment este util atunci când trebuie să creați o definiție de preprocesor care să conțină versiunea SW curentă obținută din Git:

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

Acest lucru funcționează pe orice platformă atâta timp cât este disponibilă comanda git . Dacă utilizați etichete Git, aceasta va afișa cea mai recentă etichetă, chiar dacă ramura a continuat. Modificați comanda git describe pentru a obține rezultatul dorit.

Concluzie

Qmake este un instrument grozav care se concentrează pe construirea proiectelor pe mai multe platforme bazate pe Qt. În acest ghid, am trecut în revistă utilizarea instrumentelor de bază și modelele cele mai frecvent utilizate, care vă vor menține structura proiectului flexibilă și vor menține specificațiile ușor de citit și de întreținut.

Doriți să aflați cum să vă faceți aplicația Qt să arate mai bine? Încercați: Cum să obțineți forme de colț rotunjite în C++ folosind Bezier Curves și QPainter: un ghid pas cu pas