Tutorial CMake
Hasta ahora he estado usando las autotools (autoconf, automake, libtool, etc.) para llevar a cabo la configuración de todos mis proyectos, y la verdad que me ha ido muy bien con estas herramientas una vez conseguí desentrelazar alguna que otra cosilla que me llevó más de un quebradero de cabeza. Pero ya sabéis, uno no se cansa de aprender cosas y ampliar conocimientos, y desde hace tiempo vengo observando que cmake recibe muy buenas críticas, sobre todo debido a la posibilidad que nos ofrece para poder configurar nuestros proyectos software para distintas plataformas (GNU/Linux, Windows, Mac OS/X, etc) y distintos compiladores (GnuC, Visual C++, Borland, MinGW, etc). En esta entrada os hablaré sobre cmake, explicaré algunas de sus principales características y os dejaré algunos ejemplos útiles.
¿Qué es CMake?
CMake es un sistema extensible y abierto que controla el proceso de construcción de manera independiente en diferentes sistemas operativos y compiladores. CMake está diseñado para ser usado en conjunto con el sistema de construcción nativo de un entorno. Se utilizan ficheros simples de configuración en cada directorio fuente (llamados CMakeLists.txt) para generar ficheros de estándar de construcción (Makefiles en Unix y proyectos en Windows MSVC) que se usan de la manera usual. CMake puede compilar código fuente, crear librerías, generar wrappers y construit ejecutables en combinaciones arbitrarias. CMake soporta construcciones in-place y out-of-place, y por lo tanto se puede realizar múltiples construcciones a partir de un único árbol fuente. CMake también soporta la construcción de librerías estáticas y dinámicas. Otra buena característica de CMake es que genera un fichero cache que es diseñado para ser usado con un editor gráfico. Por ejemplo, cuando ejecutamos CMake, este localiza los ficheros de inclusión, librerías, ejecutables y puede encontrar otras directivas de construcción opcionales. Dicha información es reunida en la cache, que puede ser cambiada por el usuario antes de que se generen los ficheros de construcción nativos.
Cmake está diseñado para soportar complejas jerarquías de directorios y aplicaciones que dependen de varias librerías. Por ejemplo, CMake soporta proyectos consistentes en multiples toolkits (es decir, librerías) donde cada toolkit puede contener varios directorios, y la aplicación depende de los toolkits además de un código adicional. CMake también puede manejar situaciones donde se deben construir ejecutables para poder generar código que es después compilado y linkado en una aplicación final.
Usar CMake es simple. El proceso de construcción es controlado por la creación de uno o más ficheros CMakeLists.txt en cada directorio (incluyendo subdirectorios) que forma un proyecto. Cada CMakeLists.txt consiste en uno o más comandos. Cada comando tiene la forma COMANDO(argumentos). CMake proporciona varios comandos predefinidos, pero si lo necesitas puedes añadir tus propios comandos. Además, el usuario avanzado puede añadir otros generadores de Makefiles para una combinación particular de compilador/S.O.
Después de haber estado unos cuantos días trabajando con CMake puedo decir que es bastante más rápido de aprender que las autotools, aunque está afirmación puede estar influenciada por mi desconocimiento en algunos estándares en la programación de proyectos que he ido adquiriendo con el tiempo. También son necesarias, en general, menos líneas de código para un mismo proyecto con autotools.
HelloWorld con CMake
Este primer ejemplo que vamos a ver lo he sacado de la siguiente dirección donde podéis encontrar un breve tutorial en pdf de CMake, eso si, en inglés. He realizado unas breves modificaciones a la estructura de directorios y al contenido de los ficheros para que se ajuste más a cómo suelo trabajar yo. La estructura de directorios con la que vamos a trabajar es la siguiente la podéis ver en la siguiente imagen:
Directorio raíz
Dentro de nuestro directorio de trabajo raíz vamos a crear la carpeta src donde almacenaremos los ficheros fuente que formarán una librería a la que llamaremos Hello, y otra carpeta test donde crearemos un programa que hará uso de la librería creada. Por otro lado, necesitaremos un directorio de nombre aleatorio (en este caso build) donde se crearán los archivos necesarios para generar el proyecto, independientemente del sistema operativo y compilador que usemos. Por último, y más importante, crearemos el archivo CMakeLists.txt el cuál describirá como se organiza el proyecto en el que estamos trabajando. El contenido de dicho fichero será el siguiente:
PROJECT(HELLO) CMAKE_MINIMUM_REQUIRED(VERSION 2.6) ADD_SUBDIRECTORY(src) ADD_SUBDIRECTORY(test)
El significado de los comandos que aparecen en este fichero son los siguientes:
- PROJECT: asigna un nombre que identifica al proyecto. Ahora los ficheros del proyecto podrán referirse al directorio fuente raíz del proyecto como ${NOMBREPROYECTO_SOURCE_DIR} y al directorio binario raíz del proyecto como ${NOMBREPROYECTO_BINARY_DIR}
- CMAKE_MINIMUM_REQUIRED: Establece una versión mínima de cmake para poder generar el proyecto. Si no se especifica puede que cmake nos muestre algún warning al intentar configurar el proyecto.
- ADD_SUBDIRECTORY: Añade un nuevo subdirectorio a la lista de subdirectorios del proyecto independientemente del contenido de este.
Directorio src
En el directorio src vamos a incluir el código fuente de una librería que solamente va a consistir en una clase Hello compuesta por los ficheros hello.h y hello.cpp. El código de los ficheros es el siguiente:
// hello.h
#ifndef HELLO_INC
#define HELLO_INC
class Hello
{
public:
void Print();
}; // ----- end of class Hello -----
#endif // ----- #ifndef HELLO_INC -----
#include "hello.h"
#include <iostream>
using namespace std;
void Hello:: Print()
{
cout << "Hello, World!" << endl;
}
Por último, el contenido del fichero CMakeLists.txt que está dentro del directorio src sería simplemente:
#Añade una librería llamada Hello (libHello.a bajo linux) a partir del fichero fuente hello.cpp ADD_LIBRARY(Hello hello)
Este comando por defecto nos creará una librería llamada Hello con el fichero que le hemos especificado. Aquí también se puede especificar si queremos que la librería sea dinámica o estática, pero ya hablaré sobre eso más adelante.
Directorio test
En el directorio test vamos a incluir el típico programa “Hola Mundo” haciendo uso de la librería Hello que hemos creado en el directorio src. El código fuente de dicho programa es el siguiente:
// test.cpp
#include <iostream>
#include "hello.h"
int main()
{
Hello().Print();
return 0;
}
Y el contenido del fichero CMakeLists.txt que está dentro del directorio test es:
#Asegurarse de que el compilador puede encontrar los ficheros de nuestra librería Hello
INCLUDE_DIRECTORIES(${HELLO_SOURCE_DIR}/src)
#Añade un binario llamado "helloWorld" que es construido del fichero fuente "test.cpp"
#La extensión se encuentra automáticamente
ADD_EXECUTABLE(helloWorld test)
#Enlaza el ejecutable con la librería Hello
TARGET_LINK_LIBRARIES(helloWorld Hello)
Construyendo el proyecto
Ya tenemos todo el código de nuestro proyecto y los ficheros CMakeLists.txt necesarios para poder construirlos mediante cmake. Normalmente para construir un proyecto crearemos una nueva carpeta (en nuestro caso la hemos llamado build) para generar una compilación del proyecto específica para un determinado S.O. (Windows, Linux, Mac), un determinado compilador (gcc, MSVC, icc, mingw, etc), o una determinada configuración (Debug, Release, etc). Esta característica es verdaderamente útil para no tener que re-generar proyectos enteros según la configuración deseada cuando hagamos pequeños cambios en el código. Yo lo que suelo hacer es crearme dos directorios: build-debug y build-release. El primero lo uso durante la fase más intensa de desarrollo, para corregir todos los posibles warning que me lance el compilador y activar el soporte de depuración para los depuradores. El segundo lo uso una vez que estoy seguro (o tengo una gran certeza) de que el código no contiene ningún error, y quiero compilar la librería con todas las optimizaciones posibles y sin soporte de depuración.
Para la generación del proyecto ingresaremos en una terminal de texto y accederemos al directorio build. Una vez allí ejecutaremos el comando:
cmake ..
Básicamente, al comando cmake tenemos que pasarle como argumento la ruta al directorio donde se almacena el proyecto. Si todo ha ido bien tendremos, cmake hará las comprobaciones oportunas y tendremos una salida parecida a la siguiente:
pipo@pipo-laptop:~/cmake-template/build$ cmake ../ -- The C compiler identification is GNU -- The CXX compiler identification is GNU -- Check for working C compiler: /usr/bin/gcc -- Check for working C compiler: /usr/bin/gcc -- works -- Detecting C compiler ABI info -- Detecting C compiler ABI info - done -- Check for working CXX compiler: /usr/bin/c++ -- Check for working CXX compiler: /usr/bin/c++ -- works -- Detecting CXX compiler ABI info -- Detecting CXX compiler ABI info - done -- Configuring done -- Generating done -- Build files have been written to: /home/pipo/cmake-template/build
Una vez hecho esto, se habrán generado una serie de directorios y archivos para que realizar la compilación e instalación del proyecto sea tan sencillo como ejecutar los siguientes comandos:
make sudo make install
También disponemos de una interfaz gráfica para cmake mediante el comando cmake-gui. A la hora de configurar el proyecto en plataformas Windows es mucho más cómodo trabajar mediante esta interfaz para seleccionar el tipo de configuración según el compilador o IDE que usemos.
Opciones avanzadas
El ejemplo Hola-Mundo que os acabo de mostrar, es solo la punta del Iceberg de posibilidades que nos presenta CMake. Podemos tener ficheros de configuración tan sencillos como los que acabo de mostrar o otros más complejos donde tendremos en cuenta la portabilidad de nuestro código a diferentes sistemas operativos, compiladores, uso de programas externos, etc. A continuación os hablaré de algunas cosas más, sobre el uso básico de Cmake, además de otros problemas con los que me he ido encontrando y a los que he ido dando solución.
Cambiando las variables en la cache de cmake
Cuando ejecutamos por consola el comando
cmake ..
Le estamos diciendo a Cmake que realice la configuración estándar del proyecto que estemos compilando. Pero CMake cuenta con algunas variables y opciones internas, además de las que definamos nosotros que podemos modificar fácilmente a la hora de configurar el proyecto. Para cambiar el valor de dichas variables u opciones tenemos que utilizar la opción -D. Por otra parte, la opción -i hará que cmake nos pregunte interactivamente por algunos ajustes a realizar. Por ejemplo, si queremos configurar nuestro proyecto para depuración ejecutaremos el siguiente comando:
cmake -D CMAKE_BUILD_TYPE=Debug ..
De todos modos, si no os sentís cómodos trabajando desde una terminal, siempre podéis modificar las variable y opciones desde la interfaz gráfica de cmake (cmake-gui).
Variables
El uso de variables es fundamental en cmake y una buena comprensión en la manipulación de estas nos facilitará la elaboración de otras arduas tareas. Quizás las características más destacables del manejo de variables con cmake sean las siguientes:
- No necesitamos declararlas. No existe diferencia entre crear y modificar una variable.
- Normalmente no se necesita definir su tipo.
- El comando SET crea y modifica variables.
- El comando SET puede hacer de todo con las variables pero LIST hace que algunas operaciones sean más sencillas (ver función APPEND).
- El comando FILE nos permite definir listas de variables de una forma tan rápida y sencilla como la siguiente: FILE(GLOB hdrs “*.h” ). Con dicho comando asignamos a la variable hdrs todos los ficheros con extensión .h del directorio actual.
- Además de trabajar con variables que puedan ser de distintos tipos podemos tratar con opciones que solo pueden tomar los valores ON y OFF. Por ejemplo podemos establecer la siguiente opción para determinar más adelante si tratar los warning en nuestro código como erróres.
set(WARNINGS_ARE_ERRORS OFF CACHE BOOL "Treat warnings as errors" )
- La opción definida en el punto anterior se almacena en el fichero de cache y su valor puede modificarse posteriormente modificando dicho fichero.
Cambiando los parámetros de construcción
Cmake utiliza parámetros por defecto para el preprocesador, compilador y linkador, pero estos pueden ser modificados sin demasiadas complicaciones. Podemos:
- Modificar la configuración del preprocesador con ADD_DEFINITIONS y REMOVE_DEFINITIONS. Estos cambios se aplican tanto para C como para C++.
- Modificar la configuración del compilador modificando las variables CMAKE_C_FLAGS y CMAKE_CXX_FLAGS.
- Modificar la configuración del linkador mediante
Normalmente en todos los proyectos con los que trabajo siempre incluyo flags de compilación según el modo de trabajo en el que me encuentre (Release y Debug). Para ello podemos modificar las siguientes variables:
SET(CMAKE_CXX_FLAGS_DEBUG "-O0 -g3 -Wall -Werror -W -Wno-return-type" ) SET(CMAKE_CXX_FLAGS_RELEASE "-O2 -pipe -march=core2" )
Además podemos establecer diferentes tipos de construcciones de nuestros proyectos. Las configuraciones más usuales son “Debug” y “Release”. La configuración Debug es útil cuando estamos en pleno proceso de desarrollo y queremos que el compilador nos avise de cualquier posible error o warning que pueda haber en nuestro código. Además no se incluyen optimizaciones en el código para que la depuración del mismo sea más sencilla. La configuración Release tiene una funcionalidad totalmente inversa. Es una configuración que incluye optimizaciones en el código, elimina el soporte de depuración y evita la comprobación agresiva de errores y warnings en el código, para que tanto la compilación como la ejecución del mismo sea lo más rápida posible.
Para configurar el proyecto de una forma u otra solo tenemos que modificar la variable CMAKE_BUILD_TYPE mediante el comando SET. Una forma de establecer el modo de construcción del proyecto por defecto a “Release” podría ser:
IF(NOT CMAKE_BUILD_TYPE ) SET( CMAKE_BUILD_TYPE "Release" ) ENDIF()
De esta forma si no le indicamos a cmake lo contrario compilaremos nuestro proyecto para una versión de lanzamiento. Si queremos indicarle a cmake mediante un argumento que tipo de construcción queremos, ejecutaremos el comando de la siguiente forma:
cmake -D CMAKE_BUILD_TYPE=Release ../
En cuanto a las flags de compilación, se pueden hacer cosas mucho más avanzadas como determinar el tipo de procesador que hay en el sistema y activar unas flags u otras según el mismo. Podéis echar un vistazo al CMakelists.txt que incluye la última versión de OpenCV para comprobar por vuestra cuenta el poder de CMake.
Usando pkg-config con cmake
A pesar de que en la documentación de cmake no recomiendan usar pkg-config ya que puede que no esté instalado en los equipos de los usuarios finales, es innegable que muchos proyectos software actuales en plataformas GNU/Linux, sobre todo los que están relacionados con las librerías GTK+, hacen uso de pkg-config para manejar sus dependencias. Lo cierto es que hasta ahora, todas las librerías que he manejado y desarrollado hacen uso de pkg-config, y vería imposible mi migración a cmake si este no incluyese un modo de manejar las dependencias por medio de una interfaz a pkg-config.
Para hacer uso de esta interfaz vamos a tener que incluir el módulo FindPkgConfig.cmake en nuestro proyecto y después utilizarlo mediante el comando PKG_CHECK_MODULES. Podéis encontrar una amplia documentación sobre dicho comando en el propio fichero del módulo, el cual si usáis Ubuntu se encuentra en: /usr/share/cmake-2.6/Modules/FindPkgConfig.cmake. A continuación un sencillo ejemplo de como comprobar si en nuestro sistema está instalada la librería matio.
FIND_PACKAGE(PkgConfig) #Enable PKG-CONFIG suport PKG_CHECK_MODULES(MATIO matio>=1.3.3)
En este caso pido que la versión de la librería sea al menos la 1.3.3, y el prefijo que voy a usar para las variables definidas con cmake es el mismo nombre de la librería pero con mayúsculas. La función PKG_CHECK_MODULES define las siguientes variables, entre otras, en caso de que la librería se encuentre:
<PREFIX>_LIBRARIES ... only the libraries (w/o the '-l') <PREFIX>_LIBRARY_DIRS ... the paths of the libraries (w/o the '-L') <PREFIX>_LDFLAGS ... all required linker flags <PREFIX>_LDFLAGS_OTHER ... all other linker flags <PREFIX>_INCLUDE_DIRS ... the '-I' preprocessor flags (w/o the '-I') <PREFIX>_CFLAGS ... all required cflags <PREFIX>_CFLAGS_OTHER ... the other compiler flags
Y para determinar si se ha encontrado la librería podemos usar la variable
_FOUND. Por ejemplo, en el desarrollo de una librería podemos decidir si incluir ciertos ficheros o no dependiendo de que se encuentre una dependencia.
IF (MATIO_FOUND)
SET(hdrs ${hdrs} gumatio.h)
SET(srcs ${srcs} gumatio.cpp)
INCLUDE_DIRECTORIES(${MATIO_INCLUDE_DIRS})
SET(libraries ${libraries} ${MATIO_LIBRARIES})
ENDIF()
Generando documentación con Doxygen
Al igual que con las autotools, podemos hacer que mediante un simple comando “make doc” generemos la documentación con doxygen de nuestro código fuente apropiadamente documentado. Para CMake me encontré el siguiente fichero de Jan Woetzel, el cual he modificado a mi antojo definiendo algunas variables al principio de la macro para decidir si habilitar ciertas características o no:
# -helper macro to add a "doc" target with CMake build system.
# and configure doxy.config.in to doxy.config
#
# target "doc" allows building the documentation with doxygen/dot on WIN32 and Linux
# Creates .chm windows help file if MS HTML help workshop
# (available from http://msdn.microsoft.com/workshop/author/htmlhelp)
# is installed with its DLLs in PATH.
#
#
# Please note, that the tools, e.g.:
# doxygen, dot, latex, dvips, makeindex, gswin32, etc.
# must be in path.
#
# Note about Visual Studio Projects:
# MSVS hast its own path environment which may differ from the shell.
# See "Menu Tools/Options/Projects/VC++ Directories" in VS 7.1
#
# author Jan Woetzel 2004-2006
# www.mip.informatik.uni-kiel.de/~jw
#
# Modified by Luis Díaz 2009
# http://plagatux.es
MACRO(GENERATE_DOCUMENTATION DOX_CONFIG_FILE)
FIND_PACKAGE(Doxygen)
IF (DOXYGEN_FOUND)
#Define variables
SET(SRCDIR "${PROJECT_SOURCE_DIR}/src" )
SET(TAGFILE "${PROJECT_BINARY_DIR}/doc/${PROJECT_NAME}.tag" )
IF (USE_CHM AND WIN32)
SET(WIN_CHM "YES" )
SET(CHM_FILE "${PROJECT_SOURCE_DIR}/doc/help.chm" )
SET (BINARY_TOC "YES" )
SET (TOC_EXPAND "YES" )
ELSE()
SET(WIN_CHM "NO" )
SET (BINARY_TOC "NO" )
SET (TOC_EXPAND "NO" )
ENDIF()
IF (USE_LATEX)
SET(GENERATE_PDF "YES" )
SET(GENERATE_LATEX "YES" )
SET(LATEXOUT "latex" )
ELSE()
SET(GENERATE_PDF "NO" )
SET(GENERATE_LATEX "NO" )
ENDIF()
IF (NOT USE_DOT)
SET(DOXYGEN_DOT_FOUND "NO" )
ENDIF()
#click+jump in Emacs and Visual Studio (for doxy.config) (jw)
IF (CMAKE_BUILD_TOOL MATCHES "(msdev|devenv)" )
SET(DOXY_WARN_FORMAT "\"$file($line) : $text \"" )
ELSE (CMAKE_BUILD_TOOL MATCHES "(msdev|devenv)" )
SET(DOXY_WARN_FORMAT "\"$file:$line: $text \"" )
ENDIF (CMAKE_BUILD_TOOL MATCHES "(msdev|devenv)" )
# we need latex for doxygen because of the formulas
FIND_PACKAGE(LATEX)
IF (NOT LATEX_COMPILER)
MESSAGE(STATUS "latex command LATEX_COMPILER not found but usually required. You will probably get warnings and user inetraction on doxy run." )
ENDIF (NOT LATEX_COMPILER)
IF (NOT MAKEINDEX_COMPILER)
MESSAGE(STATUS "makeindex command MAKEINDEX_COMPILER not found but usually required." )
ENDIF (NOT MAKEINDEX_COMPILER)
IF (NOT DVIPS_CONVERTER)
MESSAGE(STATUS "dvips command DVIPS_CONVERTER not found but usually required." )
ENDIF (NOT DVIPS_CONVERTER)
# Check config file
IF (EXISTS "${DOX_CONFIG_FILE}" )
CONFIGURE_FILE(${DOX_CONFIG_FILE} ${CMAKE_CURRENT_BINARY_DIR}/doxy.config @ONLY ) #OUT-OF-PLACE LOCATION
SET(DOXY_CONFIG "${CMAKE_CURRENT_BINARY_DIR}/doxy.config" )
ELSE ()
MESSAGE(SEND_ERROR "Please create configuration file for doxygen in ${CMAKE_CURRENT_SOURCE_DIR}" )
ENDIF()
# Add target
ADD_CUSTOM_TARGET(doc ${DOXYGEN_EXECUTABLE} ${DOXY_CONFIG})
IF (WIN32 AND GENERATE_WIN_CHM STREQUAL "YES" )
FIND_PACKAGE(HTMLHelp)
IF (HTML_HELP_COMPILER)
ADD_CUSTOM_TARGET(winhelp ${HTML_HELP_COMPILER} ${HHP_FILE})
ADD_DEPENDENCIES (winhelp doc)
IF (EXISTS ${CHM_FILE})
IF (PROJECT_NAME)
SET(OUT "${PROJECT_NAME}" )
ELSE ()
SET(OUT "Documentation" ) # default
ENDIF()
IF (${PROJECT_NAME}_VERSION_MAJOR)
SET(OUT "${OUT}-${${PROJECT_NAME}_VERSION_MAJOR}" )
IF (${PROJECT_NAME}_VERSION_MINOR)
SET(OUT "${OUT}.${${PROJECT_NAME}_VERSION_MINOR}" )
IF (${PROJECT_NAME}_VERSION_PATCH)
SET(OUT "${OUT}.${${PROJECT_NAME}_VERSION_PATCH}" )
ENDIF()
ENDIF()
ENDIF()
SET(OUT "${OUT}.chm" )
INSTALL(FILES ${CHM_FILE} DESTINATION "doc" RENAME "${OUT}" )
ENDIF()
ELSE()
MESSAGE(FATAL_ERROR "You have not Microsoft Help Compiler" )
ENDIF()
ENDIF ()
INSTALL(DIRECTORY "${PROJECT_BINARY_DIR}/doc/html/" DESTINATION "share/doc/lib${PROJECT_NAME}" )
ENDIF(DOXYGEN_FOUND)
ENDMACRO(GENERATE_DOCUMENTATION)
Para hacer uso de dicho fichero, además de activar o desactivar algunas opciones en el fichero principal de configuración CMake, hay que añadir las siguientes líneas, teniendo en cuenta que el fichero anterior tiene como nombre generateDoc.cmake:
OPTION(INSTALL_DOC "Set to OFF to skip build/install Documentation" ON)
OPTION(USE_DOT "Set to ON to perform diagram generation with graphviz" OFF)
OPTION(USE_LATEX "Set to ON to build latex documentation" OFF)
OPTION(USE_CHM "Set to ON to build CHM Windows documentation" OFF)
IF (INSTALL_DOC)
INCLUDE("${PROJECT_SOURCE_DIR}/generateDoc.cmake" )
GENERATE_DOCUMENTATION(${PROJECT_SOURCE_DIR}/lib${PROJECT_NAME}.dox.in)
ENDIF()
El fichero libPROJECT.dox.in será el fichero plantilla de configuración doxygen, donde tendremos que hacer uso de algunas de las variables definidas en el fichero generateDoc.cmake encerrando entre arrobas el nombre de dichas variables (p.e. @PROJECT_NAME@, @DOXYGEN_DOT_FOUND@, etc).
Añadir una opción “make uninstall”
Cuando añadimos los comandos INSTALL en nuestro proyecto, se generan comandos install para los makefiles pero sin embargo no se generan los comandos necesarios para desinstalar cada uno de los objetivos del proyecto. Examinando otros proyectos (En concreto el soporte para CMake en OpenCV) me encontré con el siguiente fichero que nos permite desinstalar todo lo que instale nuestro proyecto.
# -----------------------------------------------
# - cmake_uninstall.cmake.in
# File that provides "make uninstall" target
# We use the file 'install_manifest.txt'
# -----------------------------------------------
IF(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt" )
MESSAGE(FATAL_ERROR "Cannot find install manifest: \"@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt\"" )
ENDIF(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt" )
FILE(READ "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt" files)
STRING(REGEX REPLACE "\n" ";" files "${files}" )
FOREACH(file ${files})
MESSAGE(STATUS "Uninstalling \"$ENV{DESTDIR}${file}\"" )
IF(EXISTS "$ENV{DESTDIR}${file}" )
EXEC_PROGRAM(
"@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\""
OUTPUT_VARIABLE rm_out
RETURN_VALUE rm_retval
)
IF(NOT "${rm_retval}" STREQUAL 0)
MESSAGE(FATAL_ERROR "Problem when removing \"$ENV{DESTDIR}${file}\"" )
ENDIF(NOT "${rm_retval}" STREQUAL 0)
ELSE(EXISTS "$ENV{DESTDIR}${file}" )
MESSAGE(STATUS "File \"$ENV{DESTDIR}${file}\" does not exist." )
ENDIF(EXISTS "$ENV{DESTDIR}${file}" )
ENDFOREACH(file)
Incluyendo en el fichero CMakelists.txt principal las siguientes líneas:
CONFIGURE_FILE( "${CMAKE_CURRENT_SOURCE_DIR}/cmake_uninstall.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" IMMEDIATE @ONLY)
ADD_CUSTOM_TARGET(uninstall "${CMAKE_COMMAND}" -P "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" )
Podremos desinstalar nuestro proyecto mediante el comando make uninstall.
Dependencias con otros proyectos
Para buscar librerías tenemos varias alternativas. En el caso de que las dependencias que requerimos incluyan un fichero de configuración pkg-config podemos tratar con ellas de la forma que hemos visto anteriormente. La manera más sencilla de resolver las dependencias con otras librerías es por medio del comando FIND_PACKAGE. Dicho comando busca entre los ficheros existentes en la carpeta de módulos de cmake, para resolver la dependencia deseada. Sin embargo, de momento hay pocos ficheros de configuración de paquetes cmake. Imaginemos que queremos indicar en nuestro proyecto que dependemos obligatoriamente de los paquetes JPEG y ZLIB. Bastaría con incluir estas líneas:
FIND_PACKAGE(JPEG REQUIRED) FIND_PACKAGE(ZLIB REQUIRED)
La opción REQUIRED hace que se lance un error en caso de que no se encuentre el paquete. Si no se especifica dicha opción simplemente aparecerá un mensaje de aviso. Podemos ver en los ficheros que residen en /usr/share/cmake-2.6/Modules/ con la sintaxis FindPACKAGE.cmake que variables se definen en cada paquete. Casi siempre se definen las siguientes:
PACKAGE_INCLUDE_DIR -> Indica donde encontrar las cabeceras de la librería.
PACKAGE_LIBRARIES -> Indica que flags se le deben pasar al linkador.
PACKAGE_FOUND -> Indica si se ha encontrado la librería
Pero como dije anteriormente el comando FIND_PACKAGE solo trata con algunas librerías. Imaginemos que queremos comprobar si contamos en nuestro sistema con la librería pthread. No hay un módulo de cmake que se encargue de esta labor, por lo que tendremos que usar el comando para buscar librerías FIND_LIBRARY. Dicho comando busca en las rutas por defecto de librerías en el sistema (/usr/lib y /usr/local/lib) los nombres que se le indiquen. Para el caso de pthread podríamos utilizar las siguientes líneas:
FIND_LIBRARY(PTHREAD NAMES pthread) IF (NOT PTHREAD) MESSAGE(FATAL_ERROR "Could not find pthread library" ) ENDIF ()
En la variable PTHREAD se definirá la ruta o flag de enlace que se usará en el linkador. En caso de que no se encuentre la librería, no se establecerá ningún valor a la variable por lo que podremos comprobar fácilmente si se ha encontrado o no la librería. En caso de que intentemos configurar nuestro proyecto en Windows las cosas se complican un poco. En mi caso, el comando FIND_PACKAGE(JPEG REQUIRED) no consiguió encontrar la librería jpeg que había instalado previamente en mi sistema. Por lo tanto lo que hago es lo siguiente, aunque seguro que existe una solución mejor.
ELSEIF (WIN32)
SET(GNULIBS_PATH ${PROJECT_SOURCE_DIR}/libraries)
FIND_LIBRARY(JPEG NAMES jpeg PATHS ${GNULIBS_PATH}/lib)
IF (NOT JPEG)
MESSAGE(FATAL_ERROR "Could not find jpeg library" )
ENDIF ()
SET(GNULIBS_INCLUDE_DIR ${GNULIBS_PATH}/include)
ENDIF ()
Creo un directorio libraries, donde coloco las librerías de terceros (en este caso jpeg). Busco en el sub-directorio libs de dicho directorio si se encuentra la librería que busco y por último defino el directorio donde se encuentran las cabeceras para incluirlas posteriormente donde haga falta.
Generar fichero de configuración de proyecto “FindPACKAGE.cmake”
Como ya hemos visto antes, para buscar paquetes populares tenemos el comando FIND_PACKAGE. CMake instala por defecto en las distribuciones GNU/Linux una serie de ficheros en la ruta /usr/share/cmake-2.6/Modules/ con la sintaxis FindPACKAGE.cmake que son los que se encargan realmente de buscar si tenemos un cierto software instalado en nuestro equipo. Si nuestro proyecto software va a tomar cierta importancia y pensamos que puede ser usado por otras personas o por futuros proyectos que realicemos en el futuro, nos interesará generar un fichero de configuración de nuestro proyecto para facilitar el manejo de dependencias con nuestro paquete. Dicho de otra manera, lo que pretendemos es generar un fichero de configuración equivalente a los ficheros con extensión pc que se manejan con pkg-config.
A la hora de crear este fichero de configuración cmake tenemos dos opciones para nombrar el fichero:
1) Que tenga la forma: FindPACKAGE.cmake para que en futuros proyectos que requieran de este paquete podamos resolver la dependencia mediante los comandos CMAKE_MODULE_PATH y FIND_PACKAGE.
2) Que tenga la forma: PACKAGEConfig.cmake para posteriormente definir donde se encuentra instalado dicho fichero mediante el comando PACKAGE_DIR y después poder usar FIND_PACKAGE.
A mi me gusta más trabajar con la primera opción ya que parece algo más “estándar”. Para generar este archivo lo ideal es crearse una plantilla con extensión .in que use variables del proyecto. En mi caso llamo a este fichero siempre config.cmake.in y tiene un contenido similar al ejemplo que os muestro a continuación (Reemplazar PACKAGE con el nombre de vuestro proyecto):
# ===================================================================================
# PACKAGE CMake configuration file
#
# ** File generated automatically, do not modify **
#
# Usage from an external project:
# In your CMakeLists.txt, add these lines:
#
# FIND_PACKAGE(PACKAGE REQUIRED )
# TARGET_LINK_LIBRARIES(MY_TARGET_NAME ${PACKAGE_LIBS})
#
# This file will define the following variables:
# - PACKAGE_LIBS : The list of libraries to links against.
# - PACKAGE_LIB_DIR : The directory where lib files are. Calling LINK_DIRECTORIES
# with this path is NOT needed.
# - PACKAGE_VERSION : The version of this gu build. Example: "1.2.0"
# - PACKAGE_VERSION_MAJOR : Major version part of gu_VERSION. Example: "1"
# - PACKAGE_VERSION_MINOR : Minor version part of gu_VERSION. Example: "2"
# - PACKAGE_VERSION_PATCH : Patch version part of gu_VERSION. Example: "0"
#
# ===================================================================================
# Extract the directory where *this* file has been installed (determined at cmake run-time)
# This variable may or may not be used below, depending on the parsing of PACKAGEConfig.cmake
get_filename_component(THIS_PACKAGE_CONFIG_PATH "${CMAKE_CURRENT_LIST_FILE}" PATH)
# ======================================================
# Include directories to add to the user project:
# ======================================================
INCLUDE_DIRECTORIES(@CMAKE_INCLUDE_DIRS_CONFIGCMAKE@)
# ======================================================
# Link directories to add to the user project:
# ======================================================
LINK_DIRECTORIES("@CMAKE_LIB_DIRS_CONFIGCMAKE@ " )
# Provide the libs directory anyway, it may be needed in some cases.
SET(PACKAGE_LIB_DIR "@CMAKE_LIB_DIRS_CONFIGCMAKE@ " )
# ====================================================================
# Link libraries
# ====================================================================
if (CMAKE_MAJOR_VERSION GREATER 2 OR CMAKE_MINOR_VERSION GREATER 4) # CMake>=2.6 supports the notation "debug XXd optimized XX"
SET(PACKAGE_LIBS debug PACKAGE@PACKAGE_DLLVERSION@@PACKAGE_DEBUG_POSTFIX@ optimized PACKAGE@PACKAGE_DLLVERSION@)
else() # Old CMake:
SET(PACKAGE_LIBS PACKAGE@PACKAGE_DLLVERSION@)
endif()
# ======================================================
# Version variables:
# ======================================================
SET(PACKAGE_VERSION @PACKAGE_VERSION@)
SET(PACKAGE_VERSION_MAJOR @PACKAGE_VERSION_MAJOR@)
SET(PACKAGE_VERSION_MINOR @PACKAGE_VERSION_MINOR@)
SET(PACKAGE_VERSION_PATCH @PACKAGE_VERSION_PATCH@)
Para que se genere este fichero debemos añadir las siguientes líneas a nuestro CMakeLists.txt del directorio raíz, para incluir algunas variables de las que hacemos uso en dicho fichero y para generar el fichero FindPACKAGE.cmake a partir del mismo:
set(${PROJECT_NAME}_VERSION "1.0.0" )
string(REGEX MATCHALL "[0-9]" ${PROJECT_NAME}_VERSION_PARTS "${${PROJECT_NAME}_VERSION}" )
list(GET ${PROJECT_NAME}_VERSION_PARTS 0 ${PROJECT_NAME}_VERSION_MAJOR)
list(GET ${PROJECT_NAME}_VERSION_PARTS 1 ${PROJECT_NAME}_VERSION_MINOR)
list(GET ${PROJECT_NAME}_VERSION_PARTS 2 ${PROJECT_NAME}_VERSION_PATCH)
set(${PROJECT_NAME}_SOVERSION "${${PROJECT_NAME}_VERSION_MAJOR}.${${PROJECT_NAME}_VERSION_MINOR}" )
set(CMAKE_INCLUDE_DIRS_CONFIGCMAKE ${CMAKE_INSTALL_PREFIX}/include/${PROJECT_NAME})
set(CMAKE_LIB_DIRS_CONFIGCMAKE ${CMAKE_INSTALL_PREFIX}/lib CACHE PATH "Output directory for libraries" )
CONFIGURE_FILE("${PROJECT_SOURCE_DIR}/config.cmake.in" "${PROJECT_BINARY_DIR}/Find${PROJECT_NAME}.cmake" )
INSTALL(FILES
"${PROJECT_BINARY_DIR}/Find${PROJECT_NAME}.cmake"
DESTINATION share/cmake/ )
Por último para usar en otro proyecto el que estamos desarrollando actualmente tendremos que incluir las siguientes líneas en el fichero CMakeLists.txt:
SET(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} /usr/local/share/cmake/)
FIND_PACKAGE(PACKAGE REQUIRED)
IF (PACKAGE_VERSION VERSION_LESS X.X.X)
MESSAGE(FATAL_ERROR "Unable to use version ${PACKAGE_VERSION} of PACKAGE ..." )
ENDIF ()
Por supuesto tendremos que cambiar las X por el número de versión que queramos usar, y en caso de haber instalado los ficheros en otra ruta modificar la ruta que establecemos para CMAKE_MODULE_PATH.
En caso de que escogiésemos la 2ª opción para el nombre del fichero de configuración habría que modificar ligeramente algunas de las líneas anteriores, pero no quiero extenderme demasiado con este tema.
Portabilidad Windows & Linux en nuestros proyectos
Si hay una razón de peso por la que haya migrado a cmake ha sido sin duda por las ventajas que presenta para configurar los proyectos en diferentes plataformas. Programando en GNU/Linux y usando las autotools no he tenido ningún problema este último año, pero no he visto la manera de poder configurar los mis proyectos para poder ser compilados con el compilador Visual C++. En todo caso, si que podía trabajar con MinGW + MSYS, pero la idea es poder configurar un proyecto en cualquier compilador y/o IDE sin morir en el intento.
En CMake existen algunas variables predefinidas que se activan según el sistema operativo que estemos usando y/o el compilador que vayamos a usar para compilar el proyecto (este se detecta automáticamente si hacemos uso del comando cmake o podemos especificar el compilador a usar si usamos el comando cmake-gui). Las variables que suelo utilizar son las siguientes:
- Para distinguir entre sistemas operativos:
- WIN32 -> Detecta si usas windows (32 o 64 bits).
- UNIX -> Sistemas Unix o GNU/Linux.
- APPLE -> Mac OS.
- Para distinguir entre diferentes compiladores:
- MSVC -> Microsoft visual c. Esta variable contiene un número de versión que distingue entre distintas versiones de visual studio.
- CMAKE_COMPILER_IS_GNUCXX -> gcc.
- MINGW -> gcc para windows.
Por otra parte en nuestro código C/C++ seguramente también tendremos que escribir algunas directivas del procesador para diferenciar entre partes de código específicas para sistemas Windows y otras partes de código específicas para sistemas Unix. En gcc y mingw se definen las siguientes variables (tener cuidado con los dobles guiones bajos):
- __GNUC__ : Detecta la versión mayor del compilador gcc. Si tenemos por ejemplo la versión 4.4.2, nos retornará el primer 4.
- __GNUC_MINOR__ : Detecta el segundo número de la versión.
- __GNUC_PATCHLEVEL__: Detecta la versión del parche aplicado sobre la versión de gcc.
- linux y __linux: Detecta si estamos usando un sistema basado en GNU/Linux.
- macintosh , __APPLE__ y __MACH__ : Detecta si estamos usando un sistema apple.
- _WIN32 y _WIN64: Detecta si estamos usando un sistema windows. La primera variable se activa tanto si usamos un sistema de 32 como de 64 bits. La segunda variable solo lo hace en el segundo caso.
Conociendo la existencia de estas variables podremos configurar nuestros proyectos fácilmente para que sean portables a distintas plataformas y configurables desde distintos compiladores.
Enlaces de interés
- CMake
- Tutorial Cmake (pdf en español)
- cmake para tus proyectos
- Sobre dependencias en plataformas específicas.
- MinGW + MSYS.
loading...


Últimos comentarios