Niezbędny przewodnik po Qmake
Opublikowany: 2022-03-11Wstęp
qmake to narzędzie systemu budowania dostarczane z biblioteką Qt, które upraszcza proces budowania na różnych platformach. W przeciwieństwie do CMake i Qbs , qmake było częścią Qt od samego początku i powinno być uważane za narzędzie „natywne”. Nie trzeba dodawać, że domyślne środowisko IDE Qt — Qt Creator — najlepiej obsługuje qmake zaraz po wyjęciu z pudełka. Tak, możesz tam również wybrać systemy budowania CMake i Qbs dla nowego projektu, ale nie są one tak dobrze zintegrowane. Jest prawdopodobne, że obsługa CMake w Qt Creator z czasem ulegnie poprawie i będzie to dobry powód do wydania drugiej edycji tego przewodnika, skierowanej konkretnie do CMake. Nawet jeśli nie zamierzasz używać Qt Creator, możesz nadal rozważyć qmake jako drugi system kompilacji w przypadku tworzenia bibliotek publicznych lub wtyczek. Praktycznie wszystkie biblioteki lub wtyczki oparte na Qt innych firm dostarczają pliki qmake używane do bezproblemowej integracji z projektami opartymi na qmake. Tylko kilka z nich zapewnia podwójną konfigurację, np. qmake i CMake. Możesz preferować korzystanie z qmake, jeśli dotyczą Ciebie następujące elementy:
- Budujesz wieloplatformowy projekt oparty na Qt
- Używasz IDE Qt Creator i większości jego funkcji
- Budujesz samodzielną bibliotekę/wtyczkę do użytku w innych projektach qmake
Ten przewodnik opisuje najbardziej przydatne funkcje qmake i zawiera przykłady z życia wzięte dla każdej z nich. Czytelnicy, którzy są nowi w Qt, mogą wykorzystać ten przewodnik jako samouczek dotyczący systemu budowania Qt. Deweloperzy Qt mogą traktować to jako książkę kucharską podczas rozpoczynania nowego projektu lub mogą selektywnie zastosować niektóre funkcje do dowolnego z istniejących projektów o niewielkim wpływie.
Podstawowe użycie Qmake
Specyfikacja qmake jest napisana w .pro
(„projekt”). Oto przykład najprostszego możliwego pliku .pro
:
SOURCES = hello.cpp
Domyślnie utworzy to Makefile
, który zbuduje plik wykonywalny z pojedynczego pliku kodu źródłowego hello.cpp
.
Aby zbudować plik binarny (w tym przypadku wykonywalny), musisz najpierw uruchomić qmake, aby utworzyć plik Makefile, a następnie make
(lub nmake
, lub mingw32-make
w zależności od Twojego zestawu narzędzi), aby zbudować cel.
W skrócie, specyfikacja qmake to nic innego jak lista definicji zmiennych zmieszanych z opcjonalnymi instrukcjami przepływu sterowania. Ogólnie rzecz biorąc, każda zmienna zawiera listę ciągów. Instrukcje przepływu sterowania umożliwiają dołączenie innych plików specyfikacji qmake, sekcji warunkowych sterowania, a nawet wywoływania funkcji.
Zrozumienie składni zmiennych
Ucząc się istniejących projektów qmake, możesz być zaskoczony, jak można odwoływać się do różnych zmiennych: \(VAR,\){VAR} lub $$(VAR) …
Korzystaj z tej mini ściągawki podczas przyjmowania zasad:
-
VAR = value
Przypisz wartość do VAR -
VAR += value
Dołącz wartość do listy VAR -
VAR -= value
Usuń wartość z listy VAR -
$$VAR
lub$${VAR}
Pobiera wartość VAR w czasie działania qmake -
$(VAR)
Zawartość środowiska VAR w czasie działania Makefile (nie qmake) -
$$(VAR)
Zawartość środowiska VAR w czasie działania qmake (nie Makefile)
Wspólne szablony
Pełną listę zmiennych qmake można znaleźć w specyfikacji: http://doc.qt.io/qt-5/qmake-variable-reference.html
Przyjrzyjmy się kilku typowym szablonom projektów:
# 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
Po prostu dodaj SOURCES += … i HEADERS += … , aby wyświetlić wszystkie pliki kodu źródłowego i gotowe.
Do tej pory sprawdziliśmy bardzo podstawowe szablony. Bardziej złożone projekty zwykle obejmują kilka podprojektów, które są od siebie zależne. Zobaczmy, jak sobie z tym poradzić za pomocą qmake.
Podprojekty
Najczęstszym przypadkiem użycia jest aplikacja dostarczana z jedną lub kilkoma bibliotekami i projektami testowymi. Rozważ następującą strukturę:
/project ../library ..../include ../library-tests ../application
Oczywiście chcemy móc zbudować wszystko na raz, tak jak to:
cd project qmake && make
Aby osiągnąć ten cel, potrzebujemy pliku projektu qmake w folderze /project
:
TEMPLATE = subdirs SUBDIRS = library library-tests application library-tests.depends = library application.depends = library
UWAGA: używanie CONFIG += ordered
jest uważane za złą praktykę — wolę zamiast tego używać .depends
.
Ta specyfikacja instruuje qmake, aby najpierw zbudował podprojekt biblioteki, ponieważ od niego zależą inne cele. Następnie może budować library-tests
i aplikację w dowolnej kolejności, ponieważ te dwa są zależne.
Łączenie bibliotek
W powyższym przykładzie mamy bibliotekę, którą należy powiązać z aplikacją. W C/C++ oznacza to, że musimy skonfigurować jeszcze kilka rzeczy:
- Określ
-I
, aby podać ścieżki wyszukiwania dla dyrektyw #include. - Określ
-L
, aby zapewnić ścieżki wyszukiwania dla konsolidatora. - Podaj
-l
, aby określić, jaka biblioteka ma być dołączona.
Ponieważ chcemy, aby wszystkie podprojekty były ruchome, nie możemy używać ścieżek bezwzględnych lub względnych. Na przykład nie zrobimy tego: INCLUDEPATH += ../library/include i oczywiście nie możemy odwoływać się do biblioteki binarnej (pliku .a) z tymczasowego folderu kompilacji. Kierując się zasadą „separacji obaw” możemy szybko zorientować się, że plik projektu aplikacji powinien abstrahować od szczegółów bibliotecznych. Zamiast tego obowiązkiem biblioteki jest wskazanie, gdzie znaleźć pliki nagłówkowe itp.
Wykorzystajmy dyrektywę include()
qmake, aby rozwiązać ten problem. W projekcie biblioteki dodamy kolejną specyfikację qmake w nowym pliku z rozszerzeniem .pri
(rozszerzenie może być dowolne, ale tutaj oznacza i
). Tak więc biblioteka miałaby dwie specyfikacje: library.pro
i library.pri
. Pierwszy służy do budowania biblioteki, drugi służy do dostarczenia wszystkich szczegółów potrzebnych do projektu zużywającego.
Zawartość pliku library.pri wyglądałaby następująco:
LIBTARGET = library BASEDIR = $${PWD} INCLUDEPATH *= $${BASEDIR}/include LIBS += -L$${DESTDIR} -llibrary
BASEDIR
określa folder projektu biblioteki (dokładnie lokalizację bieżącego pliku specyfikacji qmake, którym w naszym przypadku jest library.pri
). Jak można się domyślić, INCLUDEPATH
zostanie ocenione jako /project/library/include
. DESTDIR
to katalog, w którym system kompilacji umieszcza artefakty wyjściowe, takie jak (pliki .o .a .so .dll lub .exe). Jest to zwykle konfigurowane w twoim IDE, więc nigdy nie powinieneś zakładać, gdzie znajdują się pliki wyjściowe.
W pliku application.pro
wystarczy dodać include(../library/library.pri)
i gotowe.
Przyjrzyjmy się, jak projekt aplikacji jest budowany w tym przypadku:
- Topmost
project.pro
jest projektem subdirs. Mówi nam, że najpierw należy zbudować projekt biblioteki. Więc qmake wchodzi do folderu biblioteki i buduje go za pomocąlibrary.pro
. Na tym etapielibrary.a
jest tworzona i umieszczana w folderzeDESTDIR
. - Następnie qmake wchodzi do podfolderu aplikacji i analizuje plik
application.pro
. Znajduje dyrektywęinclude(../library/library.pri)
, która nakazuje qmake natychmiastowe jej odczytanie i zinterpretowanie. Dodaje to nowe definicje do zmiennychINCLUDEPATH
iLIBS
, więc teraz kompilator i konsolidator wiedzą, gdzie szukać plików dołączanych, plików binarnych bibliotek i jaką bibliotekę połączyć.
Pominęliśmy budowanie projektu biblioteczno-testowego, ale jest on identyczny z projektem aplikacji. Oczywiście nasz projekt testowy musiałby również połączyć bibliotekę, którą ma testować.

Dzięki tej konfiguracji możesz łatwo przenieść projekt biblioteki do innego projektu qmake i dołączyć go, odwołując się w ten sposób do pliku .pri
. Właśnie w ten sposób społeczność dystrybuuje biblioteki innych firm.
config.pri
W złożonych projektach bardzo często zdarza się, że niektóre współdzielone parametry konfiguracyjne są używane przez wiele podprojektów. Aby uniknąć duplikacji, możesz ponownie wykorzystać dyrektywę include()
i utworzyć config.pri
w folderze najwyższego poziomu. Możesz również mieć wspólne „narzędzia” qmake udostępniane podprojektom, podobnie jak to, co omówimy w dalszej części tego przewodnika.
Kopiowanie artefaktów do DESTDIR
Często projekty zawierają „inne” pliki, które muszą być dystrybuowane wraz z biblioteką lub aplikacją. Musimy tylko móc skopiować wszystkie takie pliki do DESTDIR
podczas procesu budowania. Rozważ następujący 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) }
Uwaga: Używając tego wzorca, możesz zdefiniować własne funkcje wielokrotnego użytku, które działają na plikach.
Umieść ten kod w /project/copyToDestDir.pri
, aby można go było include()
w wymagających podprojektach w następujący sposób:
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)
Uwaga: DISTFILES został wprowadzony w tym samym celu, ale działa tylko w systemie Unix.
Generowanie kodu
Świetnym przykładem generowania kodu jako gotowego kroku jest sytuacja, w której projekt C++ używa protobufa Google. Zobaczmy, jak możemy wstrzyknąć wykonanie protoc
do procesu kompilacji.
Możesz łatwo znaleźć odpowiednie rozwiązanie w Google, ale musisz mieć świadomość jednego ważnego przypadku narożnego. Wyobraź sobie, że masz dwie umowy, w których A odwołuje się do B.
A.proto <= B.proto
Jeśli najpierw wygenerujemy kod dla A.proto
(aby wyprodukować A.pb.h
i A.pb.cxx
) i przekażemy go kompilatorowi, to po prostu się nie powiedzie, ponieważ zależność B.pb.h
jeszcze nie istnieje. Aby rozwiązać ten problem, musimy przejść cały etap generowania kodu przed zbudowaniem wynikowego kodu źródłowego.
Świetny fragment do tego zadania znalazłem tutaj: https://github.com/jmesmon/qmake-protobuf-example/blob/master/protobuf.pri
Jest to dość duży skrypt, ale powinieneś już wiedzieć, jak go używać:
PROTOS = A.proto B.proto include(protobuf.pri)
Patrząc na protobuf.pri
, możesz zauważyć ogólny wzorzec, który można łatwo zastosować do dowolnej niestandardowej kompilacji lub generowania kodu:
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
Zakresy i warunki
Często musimy zdefiniować deklaracje specjalnie dla danej platformy, takiej jak Windows czy MacOS. Qmake oferuje trzy predefiniowane wskaźniki platformy: win32, macx i unix. Oto składnia:
win32 { # add Windows application icon, not applicable to unix/macx platform RC_ICONS += icon.ico }
Zakresy mogą być zagnieżdżane, mogą używać operatorów !
, |
a nawet symbole wieloznaczne:
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 }
Uwaga: Unix jest zdefiniowany w systemie Mac OS! Jeśli chcesz przetestować pod kątem systemu Mac OS (nie ogólnego systemu Unix), użyj warunku unix:!macx
.
W Kreatorze Qt debug
i release
warunków zakresu nie działa zgodnie z oczekiwaniami. Aby działały poprawnie, użyj następującego wzoru:
CONFIG(debug, debug|release) { LIBS += ... } CONFIG(release, debug|release) { LIBS += ... }
Przydatne funkcje
Qmake ma wiele wbudowanych funkcji, które dodają więcej automatyzacji.
Pierwszym przykładem jest funkcja files()
. Zakładając, że masz krok generowania kodu, który generuje zmienną liczbę plików źródłowych. Oto jak możesz uwzględnić je wszystkie w SOURCES
:
SOURCES += $$files(generated/*.c)
Spowoduje to wyszukanie wszystkich plików z rozszerzeniem .c
w generated
podfolderze i dodanie ich do zmiennej SOURCES
.
Drugi przykład jest podobny do poprzedniego, ale teraz generowanie kodu wygenerowało plik tekstowy zawierający nazwy plików wyjściowych (lista plików):
SOURCES += $$cat(generated/filelist, lines)
To po prostu odczyta zawartość pliku i potraktuje każdy wiersz jako wpis dla SOURCES
.
Uwaga: pełną listę wbudowanych funkcji można znaleźć tutaj: http://doc.qt.io/qt-5/qmake-function-reference.html
Traktowanie ostrzeżeń jako błędów
Poniższy fragment kodu używa opisanej wcześniej funkcji zakresu warunkowego:
*g++*: QMAKE_CXXFLAGS += -Werror *msvc*: QMAKE_CXXFLAGS += /WX
Powodem tej komplikacji jest to, że MSVC ma inną flagę, aby włączyć tę opcję.
Generowanie wersji Gita
Poniższy fragment kodu jest przydatny, gdy trzeba utworzyć definicję preprocesora zawierającą bieżącą wersję oprogramowania uzyskaną z Git:
DEFINES += SW_VERSION=\\\"$$system(git describe --always --abbrev=0)\\\"
Działa to na każdej platformie, o ile dostępne jest polecenie git
. Jeśli użyjesz tagów Git, to wyświetli najnowszy tag, nawet jeśli gałąź poszła naprzód. Zmodyfikuj polecenie git describe
, aby uzyskać wybrane dane wyjściowe.
Wniosek
Qmake to świetne narzędzie, które koncentruje się na budowaniu wieloplatformowych projektów opartych na Qt. W tym przewodniku omówiliśmy podstawowe użycie narzędzi i najczęściej używane wzorce, które zapewnią elastyczność struktury projektu i łatwą do odczytania i utrzymania specyfikację kompilacji.
Chcesz dowiedzieć się, jak poprawić wygląd swojej aplikacji Qt? Wypróbuj: Jak uzyskać zaokrąglone kształty narożników w C++ za pomocą krzywych Beziera i QPainter: przewodnik krok po kroku