Como expliqué en mi anterior post, todo proyecto basado en CMake debe contener un script llamado CMakeLists.txt
. Este script define objetivos, pero también puede hacer un montón de otras cosas, como encontrar bibliotecas de terceros o generar archivos de cabecera C++. Los scripts de CMake tienen mucha flexibilidad.
Cada vez que integras una biblioteca externa, y a menudo cuando añades soporte para otra plataforma, necesitarás editar el script. Pasé mucho tiempo editando scripts de CMake sin entender realmente el lenguaje, ya que la documentación es bastante dispersa, pero finalmente, las cosas encajaron. El objetivo de este post es llevarte al mismo punto lo más rápido posible.
Este post no cubrirá todos los comandos incorporados de CMake, ya que hay cientos, pero es una guía bastante completa de la sintaxis y el modelo de programación del lenguaje.
Hola Mundo
Si creas un archivo llamado hello.txt
con el siguiente contenido:
message("Hello world!") # A message to print
…puedes ejecutarlo desde la línea de comandos usando cmake -P hello.txt
. (La opción -P
ejecuta el script dado, pero no genera un pipeline de construcción). Como se esperaba, imprime «¡Hola mundo!».
$ cmake -P hello.txtHello world!
Todas las variables son cadenas
En CMake, cada variable es una cadena. Puede sustituir una variable dentro de un literal de cadena rodeándola con ${}
. Esto se llama una referencia a la variable. Modifique hello.txt
de la siguiente manera:
message("Hello ${NAME}!") # Substitute a variable into the message
Ahora, si definimos NAME
en la línea de comandos cmake
utilizando la opción -D
, el script la utilizará:
$ cmake -DNAME=Newman -P hello.txtHello Newman!
Cuando una variable no está definida, por defecto es una cadena vacía:
$ cmake -P hello.txtHello !
Para definir una variable dentro de un script, utilice el comando set
. El primer argumento es el nombre de la variable a asignar, y el segundo argumento es su valor:
set(THING "funk")message("We want the ${THING}!")
Las comillas alrededor de los argumentos son opcionales, siempre y cuando no haya espacios o referencias a variables en el argumento. Por ejemplo, podría haber escrito set("THING" funk)
en la primera línea de arriba – habría sido equivalente. Para la mayoría de los comandos de CMake (excepto if
y while
, que se describen a continuación), la elección de citar estos argumentos es simplemente una cuestión de estilo. Cuando el argumento es el nombre de una variable, tiendo a no usar comillas.
Puede simular una estructura de datos usando prefijos
CMake no tiene clases, pero puede simular una estructura de datos definiendo un grupo de variables con nombres que comienzan con el mismo prefijo. A continuación, puede buscar variables en ese grupo utilizando referencias de variables anidadas ${}
. Por ejemplo, el siguiente script imprimirá «John Smith vive en el 123 de la calle Fake»:
set(JOHN_NAME "John Smith")set(JOHN_ADDRESS "123 Fake St")set(PERSON "JOHN")message("${${PERSON}_NAME} lives at ${${PERSON}_ADDRESS}.")
Incluso puede utilizar referencias de variables en el nombre de la variable a establecer. Por ejemplo, si el valor de PERSON
sigue siendo «JOHN», lo siguiente establecerá la variable JOHN_NAME
a «John Goodman»:
set(${PERSON}_NAME "John Goodman")
Cada sentencia es un comando
En CMake, cada sentencia es un comando que toma una lista de argumentos de cadena y no tiene valor de retorno. Los argumentos están separados por espacios (sin comillas). Como ya hemos visto, el comando set
define una variable en el ámbito del archivo.
Como otro ejemplo, CMake tiene un comando math
que realiza aritmética. El primer argumento debe ser EXPR
, el segundo argumento es el nombre de la variable a asignar, y el tercer argumento es la expresión a evaluar – todas cadenas. Tenga en cuenta que en la tercera línea de abajo, CMake sustituye el valor de la cadena de MY_SUM
en el argumento adjunto antes de pasar el argumento a math
.
math(EXPR MY_SUM "1 + 1") # Evaluate 1 + 1; store result in MY_SUMmessage("The sum is ${MY_SUM}.")math(EXPR DOUBLE_SUM "${MY_SUM} * 2") # Multiply by 2; store result in DOUBLE_SUMmessage("Double that is ${DOUBLE_SUM}.")
Hay un comando CMake para casi cualquier cosa que necesite hacer. El comando string
le permite realizar una manipulación avanzada de cadenas, incluyendo el reemplazo de expresiones regulares. El comando file
puede leer o escribir archivos, o manipular las rutas del sistema de archivos.
Comandos de control de flujo
Incluso las declaraciones de control de flujo son comandos. Los comandos if
/endif
ejecutan los comandos adjuntos condicionalmente. El espacio en blanco no importa, pero es común sangrar los comandos adjuntos para facilitar la lectura. Lo siguiente comprueba si la variable incorporada de CMake WIN32
está activada:
if(WIN32) message("You're running CMake on Windows.")endif()
CMake también tiene los comandos while
/endwhile
que, como es de esperar, repiten los comandos adjuntos mientras la condición sea verdadera. Aquí hay un bucle que imprime todos los números de Fibonacci hasta un millón:
set(A "1")set(B "1")while(A LESS "1000000") message("${A}") # Print A math(EXPR T "${A} + ${B}") # Add the numeric values of A and B; store result in T set(A "${B}") # Assign the value of B to A set(B "${T}") # Assign the value of T to Bendwhile()
Las condiciones if
y while
de CMake no se escriben igual que en otros lenguajes. Por ejemplo, para realizar una comparación numérica, debe especificar LESS
como argumento de cadena, como se muestra arriba. La documentación explica cómo escribir una condición válida.
if
y while
se diferencian de otros comandos de CMake en que si el nombre de una variable se especifica sin comillas, el comando utilizará el valor de la variable. En el código anterior, aproveché ese comportamiento escribiendo while(A LESS "1000000")
en lugar de while("${A}" LESS "1000000")
– ambas formas son equivalentes. Otros comandos de CMake no lo hacen.
Las listas son sólo cadenas delimitadas por punto y coma
CMake tiene una regla de sustitución especial para los argumentos sin comillas. Si todo el argumento es una referencia a una variable sin comillas, y el valor de la variable contiene puntos y comas, CMake dividirá el valor en los puntos y comas y pasará múltiples argumentos al comando adjunto. Por ejemplo, lo siguiente pasa tres argumentos a math
:
set(ARGS "EXPR;T;1 + 1")math(${ARGS}) # Equivalent to calling math(EXPR T "1 + 1")
Por otro lado, los argumentos entre comillas nunca se dividen en múltiples argumentos, incluso después de la sustitución. CMake siempre pasa una cadena entrecomillada como un solo argumento, dejando los puntos y comas intactos:
set(ARGS "EXPR;T;1 + 1")message("${ARGS}") # Prints: EXPR;T;1 + 1
Si se pasan más de dos argumentos al comando set
, se unen con puntos y comas, y luego se asignan a la variable especificada. Esto crea efectivamente una lista a partir de los argumentos:
set(MY_LIST These are separate arguments)message("${MY_LIST}") # Prints: These;are;separate;arguments
Puede manipular tales listas utilizando el comando list
:
set(MY_LIST These are separate arguments)list(REMOVE_ITEM MY_LIST "separate") # Removes "separate" from the listmessage("${MY_LIST}") # Prints: These;are;arguments
El comando foreach
/endforeach
acepta múltiples argumentos. Recorre todos los argumentos excepto el primero, asignando cada uno de ellos a la variable nombrada:
foreach(ARG These are separate arguments) message("${ARG}") # Prints each word on a separate lineendforeach()
Puede recorrer una lista pasando una referencia de variable no citada a foreach
. Como con cualquier otro comando, CMake dividirá el valor de la variable y pasará múltiples argumentos al comando:
foreach(ARG ${MY_LIST}) # Splits the list; passes items as arguments message("${ARG}") # Prints each item on a separate lineendforeach()
Las funciones se ejecutan en su propio ámbito; las macros no
En CMake, puede utilizar un par de comandos function
/endfunction
para definir una función. Aquí hay una que duplica el valor numérico de su argumento, y luego imprime el resultado:
function(doubleIt VALUE) math(EXPR RESULT "${VALUE} * 2") message("${RESULT}")endfunction()doubleIt("4") # Prints: 8
Las funciones se ejecutan en su propio ámbito. Ninguna de las variables definidas en una función contamina el ámbito de quien la llama. Si quiere devolver un valor, puede pasar el nombre de una variable a su función, y luego llamar al comando set
con el argumento especial PARENT_SCOPE
:
function(doubleIt VARNAME VALUE) math(EXPR RESULT "${VALUE} * 2") set(${VARNAME} "${RESULT}" PARENT_SCOPE) # Set the named variable in caller's scopeendfunction()doubleIt(RESULT "4") # Tell the function to set the variable named RESULTmessage("${RESULT}") # Prints: 8
De forma similar, un par de comandos macro
/endmacro
define una macro. A diferencia de las funciones, las macros se ejecutan en el mismo ámbito que su llamador. Por lo tanto, todas las variables definidas dentro de una macro se establecen en el ámbito de la persona que llama. Podemos sustituir la función anterior por la siguiente:
macro(doubleIt VARNAME VALUE) math(EXPR ${VARNAME} "${VALUE} * 2") # Set the named variable in caller's scopeendmacro()doubleIt(RESULT "4") # Tell the macro to set the variable named RESULTmessage("${RESULT}") # Prints: 8
Tanto las funciones como las macros aceptan un número arbitrario de argumentos. Los argumentos sin nombre se exponen a la función como una lista, a través de una variable especial llamada ARGN
. Aquí hay una función que duplica cada argumento que recibe, imprimiendo cada uno en una línea separada:
function(doubleEach) foreach(ARG ${ARGN}) # Iterate over each argument math(EXPR N "${ARG} * 2") # Double ARG's numeric value; store result in N message("${N}") # Print N endforeach()endfunction()doubleEach(5 6 7 8) # Prints 10, 12, 14, 16 on separate lines
Incluyendo otros scripts
Las variables de Make se definen en el ámbito del fichero. El comando include
ejecuta otro script de CMake en el mismo ámbito que el script que llama. Es muy parecido a la directiva #include
en C/C++. Normalmente se utiliza para definir un conjunto común de funciones o macros en el script de llamada. Utiliza la variable CMAKE_MODULE_PATH
como ruta de búsqueda.
El comando find_package
busca scripts de la forma Find*.cmake
y también los ejecuta en el mismo ámbito. Estos scripts se utilizan a menudo para ayudar a encontrar bibliotecas externas. Por ejemplo, si hay un archivo llamado FindSDL2.cmake
en la ruta de búsqueda, find_package(SDL2)
equivale a include(FindSDL2.cmake)
. (Tenga en cuenta que hay varias maneras de utilizar el comando find_package
– este es sólo uno de ellos.)
El comando add_subdirectory
de CMake, por otro lado, crea un nuevo ámbito, luego ejecuta el script llamado CMakeLists.txt
desde el directorio especificado en ese nuevo ámbito. Normalmente se utiliza para añadir otro subproyecto basado en CMake, como una biblioteca o un ejecutable, al proyecto de llamada. Los objetivos definidos por el subproyecto se añaden a la cadena de construcción a menos que se especifique lo contrario. Ninguna de las variables definidas en la secuencia de comandos del subproyecto contaminará el ámbito de los padres a menos que la opción set
del comando PARENT_SCOPE
se utiliza.
Como un ejemplo, aquí están algunas de las secuencias de comandos involucrados cuando se ejecuta CMake en el proyecto Turf:
Obtención y configuración de propiedades
Una secuencia de comandos CMake define objetivos utilizando los comandos add_executable
, add_library
o add_custom_target
. Una vez creado un objetivo, tiene propiedades que se pueden manipular utilizando los comandos get_property
y set_property
. A diferencia de las variables, los objetivos son visibles en todos los ámbitos, incluso si se definieron en un subdirectorio. Todas las propiedades de los objetivos son cadenas.
add_executable(MyApp "main.cpp") # Create a target named MyApp# Get the target's SOURCES property and assign it to MYAPP_SOURCESget_property(MYAPP_SOURCES TARGET MyApp PROPERTY SOURCES)message("${MYAPP_SOURCES}") # Prints: main.cpp
Otras propiedades de los objetivos son LINK_LIBRARIES
, INCLUDE_DIRECTORIES
y COMPILE_DEFINITIONS
. Esas propiedades son modificadas, indirectamente, por los comandos target_link_libraries
, target_include_directories
y target_compile_definitions
. Al final de la secuencia de comandos, CMake utiliza esas propiedades de destino para generar la tubería de construcción.
Hay propiedades para otras entidades de CMake, también. Hay un conjunto de propiedades de directorio en cada ámbito de archivo. Hay un conjunto de propiedades globales que es accesible desde todos los scripts. Y hay un conjunto de propiedades de archivo de origen para cada archivo de origen C / C ++.