Um guia vital para o Qmake
Publicados: 2022-03-11Introdução
qmake é uma ferramenta de sistema de compilação fornecida com a biblioteca Qt que simplifica o processo de compilação em diferentes plataformas. Ao contrário do CMake e do Qbs , o qmake fazia parte do Qt desde o início e deve ser considerado uma ferramenta “nativa”. Desnecessário dizer que o IDE padrão do Qt — Qt Creator — tem o melhor suporte ao qmake pronto para uso. Sim, você também pode escolher os sistemas de compilação CMake e Qbs para um novo projeto, mas eles não são tão bem integrados. É provável que o suporte ao CMake no Qt Creator seja aprimorado com o tempo, e esse será um bom motivo para publicar a segunda edição deste guia, voltada especificamente para o CMake. Mesmo que você não pretenda usar o Qt Creator, você ainda pode considerar o qmake como um segundo sistema de compilação caso esteja construindo bibliotecas públicas ou plugins. Praticamente todas as bibliotecas ou plugins baseados em Qt de terceiros fornecem arquivos qmake usados para integração em projetos baseados em qmake sem problemas. Apenas alguns deles fornecem configuração dupla, por exemplo, qmake e CMake. Você pode preferir usar o qmake se o seguinte se aplicar a você:
- Você está construindo um projeto multiplataforma baseado em Qt
- Você está usando o Qt Creator IDE e a maioria de seus recursos
- Você está construindo uma biblioteca/plugin independente para ser usado por outros projetos qmake
Este guia descreve os recursos mais úteis do qmake e fornece exemplos do mundo real para cada um deles. Os leitores que são novos no Qt podem usar este guia como um tutorial para o sistema de compilação do Qt. Os desenvolvedores do Qt podem tratar isso como um livro de receitas ao iniciar um novo projeto ou podem aplicar seletivamente alguns dos recursos a qualquer um dos projetos existentes com baixo impacto.
Uso Básico do Qmake
A especificação qmake é escrita em arquivos .pro
(“projeto”). Este é um exemplo do arquivo .pro
mais simples possível:
SOURCES = hello.cpp
Por padrão, isso criará um Makefile
que compilaria um executável a partir do arquivo de código-fonte único hello.cpp
.
Para construir o binário (executável neste caso), você precisa executar o qmake primeiro para produzir um Makefile e então make
(ou nmake
ou mingw32-make
dependendo da sua cadeia de ferramentas) para construir o alvo.
Em poucas palavras, uma especificação qmake nada mais é do que uma lista de definições de variáveis misturadas com instruções opcionais de fluxo de controle. Cada variável, em geral, contém uma lista de strings. As instruções de fluxo de controle permitem incluir outros arquivos de especificação qmake, seções condicionais de controle e até mesmo chamar funções.
Entendendo a Sintaxe das Variáveis
Ao aprender projetos qmake existentes, você pode se surpreender como diferentes variáveis podem ser referenciadas: \(VAR,\){VAR} ou $$(VAR) …
Use esta mini folha de dicas ao adotar as regras:
-
VAR = value
Atribuir valor a VAR -
VAR += value
Acrescentar valor à lista VAR -
VAR -= value
Remover valor da lista VAR -
$$VAR
ou$${VAR}
Obtém o valor do VAR no momento em que o qmake está em execução -
$(VAR)
Conteúdo de um VAR de ambiente no momento em que o Makefile (não o qmake) está sendo executado -
$$(VAR)
Conteúdo de um VAR de ambiente no momento em que qmake (não Makefile) está sendo executado
Modelos comuns
A lista completa de variáveis qmake pode ser encontrada na especificação: http://doc.qt.io/qt-5/qmake-variable-reference.html
Vamos analisar alguns modelos comuns para projetos:
# 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 adicionar SOURCES += … e HEADERS += … para listar todos os seus arquivos de código-fonte e pronto.
Até agora, analisamos modelos muito básicos. Projetos mais complexos geralmente incluem vários subprojetos com dependências entre si. Vamos ver como gerenciar isso com o qmake.
Subprojetos
O caso de uso mais comum é um aplicativo fornecido com uma ou várias bibliotecas e projetos de teste. Considere a seguinte estrutura:
/project ../library ..../include ../library-tests ../application
Obviamente, queremos poder construir tudo de uma vez, assim:
cd project qmake && make
Para atingir esse objetivo, precisamos de um arquivo de projeto qmake na pasta /project
:
TEMPLATE = subdirs SUBDIRS = library library-tests application library-tests.depends = library application.depends = library
NOTA: usar CONFIG += ordered
é considerado uma prática ruim—prefira usar .depends
em vez disso.
Esta especificação instrui o qmake a construir um subprojeto de biblioteca primeiro porque outros alvos dependem dele. Em seguida, ele pode criar library-tests
e o aplicativo em uma ordem arbitrária porque esses dois são dependentes.
Como vincular bibliotecas
No exemplo acima, temos uma biblioteca que precisa ser vinculada à aplicação. Em C/C++, isso significa que precisamos ter mais algumas coisas configuradas:
- Especifique
-I
para fornecer caminhos de pesquisa para diretivas #include. - Especifique
-L
para fornecer caminhos de pesquisa para o vinculador. - Especifique
-l
para fornecer qual biblioteca precisa ser vinculada.
Como queremos que todos os subprojetos sejam móveis, não podemos usar caminhos absolutos ou relativos. Por exemplo, não devemos fazer isso: INCLUDEPATH += ../library/include e é claro que não podemos referenciar o binário da biblioteca (arquivo .a) de uma pasta de compilação temporária. Seguindo o princípio de “separação de interesses”, podemos perceber rapidamente que o arquivo de projeto do aplicativo deve abstrair dos detalhes da biblioteca. Em vez disso, é responsabilidade da biblioteca informar onde encontrar arquivos de cabeçalho, etc.
Vamos aproveitar a diretiva include()
do qmake para resolver este problema. No projeto da biblioteca, adicionaremos outra especificação qmake em um novo arquivo com a extensão .pri
(a extensão pode ser qualquer coisa, mas aqui i
significa include). Assim, a biblioteca teria duas especificações: library.pro
e library.pri
. O primeiro é usado para construir a biblioteca, o segundo é usado para fornecer todos os detalhes necessários para um projeto consumidor.
O conteúdo do arquivo library.pri seria o seguinte:
LIBTARGET = library BASEDIR = $${PWD} INCLUDEPATH *= $${BASEDIR}/include LIBS += -L$${DESTDIR} -llibrary
BASEDIR
especifica a pasta do projeto da biblioteca (para ser exato, a localização do arquivo de especificação atual do qmake, que é library.pri
em nosso caso). Como você pode imaginar, INCLUDEPATH
será avaliado para /project/library/include
. DESTDIR
é o diretório onde o sistema de compilação está colocando os artefatos de saída, como (arquivos .o .a .so .dll ou .exe). Isso geralmente é configurado em seu IDE, portanto, você nunca deve fazer suposições sobre onde os arquivos de saída estão localizados.
No arquivo application.pro
basta adicionar include(../library/library.pri)
e pronto.

Vamos revisar como o projeto do aplicativo está sendo construído neste caso:
- Topmost
project.pro
é um projeto de subdiretórios. Ele nos diz que o projeto da biblioteca precisa ser construído primeiro. Então o qmake entra na pasta da biblioteca e a compila usandolibrary.pro
. Neste estágio,library.a
é produzido e colocado na pastaDESTDIR
. - Em seguida, o qmake entra na subpasta do aplicativo e analisa o arquivo
application.pro
. Ele encontra a diretivainclude(../library/library.pri)
, que instrui o qmake a lê-la e interpretá-la imediatamente. Isso adiciona novas definições às variáveisINCLUDEPATH
eLIBS
, então agora o compilador e o vinculador sabem onde procurar arquivos de inclusão, os binários da biblioteca e qual biblioteca vincular.
Pulamos a construção do projeto library-tests, mas ele é idêntico ao projeto do aplicativo. Obviamente, nosso projeto de teste também precisaria vincular a biblioteca que deveria testar.
Com esta configuração, você pode facilmente mover o projeto de biblioteca para outro projeto qmake e incluí-lo, referenciando assim o arquivo .pri
. É exatamente assim que as bibliotecas de terceiros são distribuídas pela comunidade.
config.pri
É muito comum em um projeto complexo ter alguns parâmetros de configuração compartilhados que são usados por muitos subprojetos. Para evitar a duplicação, você pode usar novamente a diretiva include()
e criar config.pri
na pasta de nível superior. Você também pode ter “utilitários” comuns do qmake compartilhados com seus subprojetos, semelhante ao que discutiremos a seguir neste guia.
Copiando Artefatos para DESTDIR
Muitas vezes, os projetos têm alguns “outros” arquivos que precisam ser distribuídos junto com uma biblioteca ou aplicativo. Nós só precisamos ser capazes de copiar todos esses arquivos para DESTDIR
durante o processo de compilação. Considere o seguinte trecho:
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: Usando este padrão, você pode definir suas próprias funções reutilizáveis que funcionam em arquivos.
Coloque este código em /project/copyToDestDir.pri
para que você possa include()
em subprojetos exigentes da seguinte forma:
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 foi introduzido para o mesmo propósito, mas só funciona em Unix.
Geração de código
Um ótimo exemplo de geração de código como uma etapa pré-criada é quando um projeto C++ está usando o Google protobuf. Vamos ver como podemos injetar a execução do protoc
no processo de compilação.
Você pode facilmente pesquisar no Google uma solução adequada, mas precisa estar ciente de um caso importante. Imagine que você tenha dois contratos, onde A está referenciando B.
A.proto <= B.proto
Se gerarmos código para A.proto
primeiro (para produzir A.pb.h
e A.pb.cxx
) e alimentarmos o compilador, ele simplesmente falhará porque a dependência B.pb.h
ainda não existe. Para resolver isso, precisamos passar por todo o estágio de geração de protocódigo antes de construir o código-fonte resultante.
Encontrei um ótimo trecho para esta tarefa aqui: https://github.com/jmesmon/qmake-protobuf-example/blob/master/protobuf.pri
É um script bastante grande, mas você já deve saber como usá-lo:
PROTOS = A.proto B.proto include(protobuf.pri)
Ao olhar para protobuf.pri
, você pode notar o padrão genérico que pode ser facilmente aplicado a qualquer compilação personalizada ou geração de código:
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
Escopos e Condições
Muitas vezes, precisamos definir declarações especificamente para uma determinada plataforma, como Windows ou MacOS. O Qmake oferece três indicadores de plataforma predefinidos: win32, macx e unix. Aqui está a sintaxe:
win32 { # add Windows application icon, not applicable to unix/macx platform RC_ICONS += icon.ico }
Os escopos podem ser aninhados, podem usar operadores !
, |
e até curingas:
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 é definido no Mac OS! Se você quiser testar para Mac OS (não Unix genérico), use a condição unix:!macx
.
No Qt Creator, as condições de debug
e release
do escopo não estão funcionando conforme o esperado. Para fazê-los funcionar corretamente, use o seguinte padrão:
CONFIG(debug, debug|release) { LIBS += ... } CONFIG(release, debug|release) { LIBS += ... }
Funções úteis
O Qmake possui várias funções incorporadas que adicionam mais automação.
O primeiro exemplo é a função files()
. Supondo que você tenha uma etapa de geração de código que produz um número variável de arquivos de origem. Aqui está como você pode incluir todos eles em SOURCES
:
SOURCES += $$files(generated/*.c)
Isso encontrará todos os arquivos com a extensão .c
na subpasta generated
e os adicionará à variável SOURCES
.
O segundo exemplo é semelhante ao anterior, mas agora a geração do código produziu um arquivo de texto contendo os nomes dos arquivos de saída (lista de arquivos):
SOURCES += $$cat(generated/filelist, lines)
Isso apenas lerá o conteúdo do arquivo e tratará cada linha como uma entrada para SOURCES
.
Nota: A lista completa de funções incorporadas pode ser encontrada aqui: http://doc.qt.io/qt-5/qmake-function-reference.html
Tratando avisos como erros
O snippet a seguir usa o recurso de escopo condicional descrito anteriormente:
*g++*: QMAKE_CXXFLAGS += -Werror *msvc*: QMAKE_CXXFLAGS += /WX
O motivo dessa complicação é que o MSVC tem um sinalizador diferente para habilitar essa opção.
Gerando a versão do Git
O snippet a seguir é útil quando você precisa criar uma definição de pré-processador contendo a versão atual do SW obtida do Git:
DEFINES += SW_VERSION=\\\"$$system(git describe --always --abbrev=0)\\\"
Isso funciona em qualquer plataforma, desde que o comando git
esteja disponível. Se você usar tags do Git, isso exibirá a tag mais recente, mesmo que a ramificação tenha avançado. Modifique o comando git describe
para obter a saída de sua escolha.
Conclusão
O Qmake é uma ótima ferramenta focada na construção de seus projetos multiplataforma baseados em Qt. Neste guia, revisamos o uso básico da ferramenta e os padrões mais usados que manterão a estrutura do projeto flexível e as especificações de construção fáceis de ler e manter.
Quer aprender a melhorar a aparência do seu aplicativo Qt? Experimente: Como obter formas de cantos arredondados em C++ usando curvas de Bezier e QPainter: um guia passo a passo