Una guía vital para Qmake

Publicado: 2022-03-11

Introducció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.

Una ilustración del proceso de compilación de qmake

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.

La estructura del directorio del proyecto.

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:

  1. Especifique -I para proporcionar rutas de búsqueda para las directivas #include.
  2. Especifique -L para proporcionar rutas de búsqueda para el enlazador.
  3. 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:

  1. 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 usando library.pro . En esta etapa, se produce library.a y se coloca en la carpeta DESTDIR .
  2. Luego, qmake ingresa a la subcarpeta de la aplicación y analiza el archivo application.pro . Encuentra la directiva include(../library/library.pri) , que le indica a qmake que la lea e interprete inmediatamente. Esto agrega nuevas definiciones a las variables INCLUDEPATH y LIBS , 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