Una guía vital para Qmake
Publicado: 2022-03-11Introducción
qmake es una herramienta de sistema de compilación incluida con la biblioteca Qt que simplifica el proceso de compilación en diferentes plataformas. A diferencia de CMake y Qbs , qmake fue parte de Qt desde el principio y debe considerarse como una herramienta "nativa". No hace falta decir que el IDE predeterminado de Qt, Qt Creator , tiene el mejor soporte de qmake listo para usar. Sí, también puede elegir los sistemas de compilación CMake y Qbs para un nuevo proyecto allí, pero estos no están tan bien integrados. Es probable que la compatibilidad con CMake en Qt Creator mejore con el tiempo, y esta será una buena razón para publicar la segunda edición de esta guía, dirigida específicamente a CMake. Incluso si no tiene la intención de usar Qt Creator, es posible que desee considerar qmake como un segundo sistema de compilación en caso de que esté creando complementos o bibliotecas públicas. Prácticamente todas las bibliotecas o complementos basados en Qt de terceros proporcionan archivos qmake que se utilizan para integrarse sin problemas en proyectos basados en qmake. Sólo unos pocos ofrecen configuración dual, por ejemplo, qmake y CMake. Es posible que prefiera usar qmake si lo siguiente se aplica a usted:
- Está construyendo un proyecto basado en Qt multiplataforma
- Está utilizando Qt Creator IDE y la mayoría de sus funciones
- Está creando una biblioteca/complemento independiente para que lo utilicen otros proyectos de qmake
Esta guía describe las características más útiles de qmake y proporciona ejemplos del mundo real para cada una de ellas. Los lectores que son nuevos en Qt pueden usar esta guía como un tutorial para el sistema de compilación de Qt. Los desarrolladores de Qt pueden tratar esto como un libro de cocina al iniciar un nuevo proyecto o pueden aplicar selectivamente algunas de las características a cualquiera de los proyectos existentes con bajo impacto.
Uso básico de Qmake
La especificación qmake está escrita en archivos .pro
("proyecto"). Este es un ejemplo del archivo .pro
más simple posible:
SOURCES = hello.cpp
De manera predeterminada, esto creará un Makefile
que generaría un ejecutable a partir del archivo de código fuente único hello.cpp
.
Para construir el binario (ejecutable en este caso), primero debe ejecutar qmake para producir un Makefile y luego make
(o nmake
o mingw32-make
dependiendo de su cadena de herramientas) para construir el objetivo.
En pocas palabras, una especificación qmake no es más que una lista de definiciones de variables combinadas con declaraciones de flujo de control opcionales. Cada variable, en general, contiene una lista de cadenas. Las declaraciones de flujo de control le permiten incluir otros archivos de especificación qmake, secciones condicionales de control e incluso funciones de llamada.
Comprender la sintaxis de las variables
Al aprender proyectos qmake existentes, puede que se sorprenda de cómo se pueden hacer referencia a diferentes variables: \(VAR,\){VAR} o $$(VAR) ...
Use esta mini hoja de trucos mientras adopta las reglas:
-
VAR = value
Asignar valor a VAR -
VAR += value
Agregar valor a la lista de VAR -
VAR -= value
Eliminar valor de la lista de VAR -
$$VAR
o$${VAR}
Obtiene el valor de VAR en el momento en que se ejecuta qmake -
$(VAR)
Contenido de un entorno VAR en el momento en que se ejecuta Makefile (no qmake) -
$$(VAR)
Contenido de un entorno VAR en el momento en que qmake (no Makefile) se está ejecutando
Plantillas comunes
La lista completa de variables qmake se puede encontrar en la especificación: http://doc.qt.io/qt-5/qmake-variable-reference.html
Revisemos algunas plantillas comunes para proyectos:
# 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
Simplemente agregue SOURCES += … y HEADERS += … para enumerar todos sus archivos de código fuente, y listo.
Hasta ahora, hemos revisado plantillas muy básicas. Los proyectos más complejos suelen incluir varios subproyectos con dependencias entre sí. Veamos cómo manejar esto con qmake.
Sub-proyectos
El caso de uso más común es una aplicación que se envía con una o varias bibliotecas y proyectos de prueba. Considere la siguiente estructura:
/project ../library ..../include ../library-tests ../application
Obviamente, queremos poder construir todo a la vez, así:
cd project qmake && make
Para lograr este objetivo, necesitamos un archivo de proyecto qmake en la carpeta /project
:
TEMPLATE = subdirs SUBDIRS = library library-tests application library-tests.depends = library application.depends = library
NOTA: usar CONFIG += ordered
se considera una mala práctica; prefiera usar .depends
en su lugar.
Esta especificación le indica a qmake que cree primero un subproyecto de biblioteca porque otros objetivos dependen de él. Luego puede construir library-tests
y la aplicación en un orden arbitrario porque estos dos son dependientes.
Vinculación de bibliotecas
En el ejemplo anterior, tenemos una biblioteca que debe vincularse a la aplicación. En C/C++, esto significa que necesitamos tener algunas cosas más configuradas:
- Especifique
-I
para proporcionar rutas de búsqueda para las directivas #include. - Especifique
-L
para proporcionar rutas de búsqueda para el enlazador. - Especifique
-l
para proporcionar qué biblioteca debe vincularse.
Como queremos que todos los subproyectos se puedan mover, no podemos usar rutas absolutas o relativas. Por ejemplo, no haremos esto: INCLUDEPATH += ../library/include y, por supuesto, no podemos hacer referencia a la biblioteca binaria (archivo .a) desde una carpeta de compilación temporal. Siguiendo el principio de "separación de preocupaciones", podemos darnos cuenta rápidamente de que el archivo del proyecto de la aplicación se abstraerá de los detalles de la biblioteca. En cambio, es responsabilidad de la biblioteca decir dónde encontrar archivos de encabezado, etc.
Aprovechemos la directiva include()
de qmake para resolver este problema. En el proyecto de la biblioteca, agregaremos otra especificación qmake en un nuevo archivo con la extensión .pri
(la extensión puede ser cualquier cosa, pero aquí i
significa incluir). Entonces, la biblioteca tendría dos especificaciones: library.pro
y library.pri
. El primero se usa para construir la biblioteca, el segundo se usa para proporcionar todos los detalles que necesita un proyecto de consumo.
El contenido del archivo library.pri sería el siguiente:
LIBTARGET = library BASEDIR = $${PWD} INCLUDEPATH *= $${BASEDIR}/include LIBS += -L$${DESTDIR} -llibrary
BASEDIR
especifica la carpeta del proyecto de biblioteca (para ser exactos, la ubicación del archivo de especificación qmake actual, que es library.pri
en nuestro caso). Como puede suponer, INCLUDEPATH
se evaluará como /project/library/include
. DESTDIR
es el directorio donde el sistema de compilación coloca los artefactos de salida, como (archivos .o .a .so .dll o .exe). Esto generalmente se configura en su IDE, por lo que nunca debe hacer suposiciones sobre dónde se encuentran los archivos de salida.
En el archivo application.pro
simplemente agregue include(../library/library.pri)
y listo.

Revisemos cómo se construye el proyecto de la aplicación en este caso:
- Topmost
project.pro
es un proyecto de subdirectorios. Nos dice que el proyecto de la biblioteca debe construirse primero. Entonces qmake ingresa a la carpeta de la biblioteca y la construye usandolibrary.pro
. En esta etapa, se producelibrary.a
y se coloca en la carpetaDESTDIR
. - Luego, qmake ingresa a la subcarpeta de la aplicación y analiza el archivo
application.pro
. Encuentra la directivainclude(../library/library.pri)
, que le indica a qmake que la lea e interprete inmediatamente. Esto agrega nuevas definiciones a las variablesINCLUDEPATH
yLIBS
, por lo que ahora el compilador y el vinculador saben dónde buscar los archivos de inclusión, los binarios de la biblioteca y qué biblioteca vincular.
Nos saltamos la construcción del proyecto de pruebas de la biblioteca, pero es idéntico al proyecto de la aplicación. Obviamente, nuestro proyecto de prueba también necesitaría vincular la biblioteca que se supone que debe probar.
Con esta configuración, puede mover fácilmente el proyecto de la biblioteca a otro proyecto qmake e incluirlo, haciendo así referencia al archivo .pri
. Así es exactamente como la comunidad distribuye las bibliotecas de terceros.
config.pri
Es muy común que un proyecto complejo tenga algunos parámetros de configuración compartidos que son utilizados por muchos subproyectos. Para evitar la duplicación, puede aprovechar nuevamente la directiva include()
y crear config.pri
en la carpeta de nivel superior. También puede tener “utilidades” qmake comunes compartidas con sus subproyectos, similar a lo que discutimos a continuación en esta guía.
Copia de artefactos a DESTDIR
A menudo, los proyectos tienen algunos "otros" archivos que deben distribuirse junto con una biblioteca o aplicación. Solo necesitamos poder copiar todos esos archivos en DESTDIR
durante el proceso de compilación. Considere el siguiente fragmento:
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: con este patrón, puede definir sus propias funciones reutilizables que funcionan en archivos.
Coloque este código en /project/copyToDestDir.pri
para que pueda include()
en subproyectos exigentes de la siguiente manera:
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 se introdujo con el mismo propósito, pero solo funciona en Unix.
Codigo de GENERACION
Un gran ejemplo de generación de código como un paso preconstruido es cuando un proyecto de C++ usa Google protobuf. Veamos cómo podemos inyectar la ejecución del protoc
en el proceso de compilación.
Puede buscar fácilmente en Google una solución adecuada, pero debe tener en cuenta un caso de esquina importante. Imagina que tienes dos contratos, donde A hace referencia a B.
A.proto <= B.proto
Si primero generamos código para A.proto
(para producir A.pb.h
y A.pb.cxx
) y lo alimentamos al compilador, simplemente fallará porque la dependencia B.pb.h
aún no existe. Para resolver esto, necesitamos pasar toda la etapa de generación de código de prototipo antes de construir el código fuente resultante.
Encontré un excelente fragmento para esta tarea aquí: https://github.com/jmesmon/qmake-protobuf-example/blob/master/protobuf.pri
Es un script bastante grande, pero ya deberías saber cómo usarlo:
PROTOS = A.proto B.proto include(protobuf.pri)
Al examinar protobuf.pri
, es posible que observe el patrón genérico que se puede aplicar fácilmente a cualquier compilación personalizada o generación 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
Alcances y Condiciones
A menudo, necesitamos definir declaraciones específicamente para una plataforma determinada, como Windows o MacOS. Qmake ofrece tres indicadores de plataforma predefinidos: win32, macx y unix. Aquí está la sintaxis:
win32 { # add Windows application icon, not applicable to unix/macx platform RC_ICONS += icon.ico }
¡Los ámbitos se pueden anidar, se pueden usar operadores !
, |
e incluso comodines:
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 está definido en Mac OS! Si desea probar Mac OS (no Unix genérico), utilice la condición unix:!macx
.
En Qt Creator, las condiciones de alcance de debug
y release
no funcionan como se esperaba. Para que funcionen correctamente, utilice el siguiente patrón:
CONFIG(debug, debug|release) { LIBS += ... } CONFIG(release, debug|release) { LIBS += ... }
Funciones útiles
Qmake tiene una serie de funciones integradas que agregan más automatización.
El primer ejemplo es la función files()
. Suponiendo que tiene un paso de generación de código que produce una cantidad variable de archivos fuente. Así es como puede incluirlos todos en SOURCES
:
SOURCES += $$files(generated/*.c)
Esto encontrará todos los archivos con la extensión .c
en la subcarpeta generated
y los agregará a la variable SOURCES
.
El segundo ejemplo es similar al anterior, pero ahora la generación de código produjo un archivo de texto que contiene los nombres de los archivos de salida (lista de archivos):
SOURCES += $$cat(generated/filelist, lines)
Esto solo leerá el contenido del archivo y tratará cada línea como una entrada para SOURCES
.
Nota: La lista completa de funciones incrustadas se puede encontrar aquí: http://doc.qt.io/qt-5/qmake-function-reference.html
Tratar las advertencias como errores
El siguiente fragmento utiliza la característica de alcance condicional descrita anteriormente:
*g++*: QMAKE_CXXFLAGS += -Werror *msvc*: QMAKE_CXXFLAGS += /WX
El motivo de esta complicación es que MSVC tiene un indicador diferente para habilitar esta opción.
Generación de la versión de Git
El siguiente fragmento es útil cuando necesita crear una definición de preprocesador que contenga la versión SW actual obtenida de Git:
DEFINES += SW_VERSION=\\\"$$system(git describe --always --abbrev=0)\\\"
Esto funciona en cualquier plataforma siempre que el comando git
esté disponible. Si usa etiquetas de Git, esto mostrará la etiqueta más reciente, aunque la rama haya seguido adelante. Modifique el comando git describe
para obtener el resultado de su elección.
Conclusión
Qmake es una gran herramienta que se enfoca en construir sus proyectos basados en Qt multiplataforma. En esta guía, revisamos el uso básico de herramientas y los patrones más utilizados que mantendrán la estructura de su proyecto flexible y las especificaciones de compilación fáciles de leer y mantener.
¿Quiere aprender cómo hacer que su aplicación Qt se vea mejor? Pruebe: Cómo obtener formas de esquinas redondeadas en C++ usando Bezier Curves y QPainter: una guía paso a paso