Como explicado no meu post anterior, todo projeto baseado em CMake deve conter um script chamado CMakeLists.txt
. Este script define alvos, mas também pode fazer muitas outras coisas, tais como encontrar bibliotecas de terceiros ou gerar arquivos de cabeçalho C++. Os scripts CMake têm muita flexibilidade.
A cada vez que você integrar uma biblioteca externa, e muitas vezes ao adicionar suporte para outra plataforma, você precisará editar o script. Eu passei muito tempo editando scripts CMake sem realmente entender a linguagem, já que a documentação está bastante dispersa, mas eventualmente, as coisas clicaram. O objetivo deste post é levá-lo ao mesmo ponto o mais rápido possível.
Este post não irá cobrir todos os comandos embutidos do CMake, pois existem centenas, mas é um guia bastante completo para a sintaxe e modelo de programação da linguagem.
Hello World
Se você criar um arquivo chamado hello.txt
com o seguinte conteúdo:
message("Hello world!") # A message to print
…você pode executá-lo a partir da linha de comando usando cmake -P hello.txt
. (A opção -P
executa o script dado, mas não gera um pipeline de compilação). Como esperado, ele imprime “Hello world!”.
$ cmake -P hello.txtHello world!
Todas as variáveis são Strings
No CMake, cada variável é uma string. Você pode substituir uma variável dentro de uma string literal, circundando-a com ${}
. Isto é chamado de variável de referência. Modifique hello.txt
da seguinte forma:
message("Hello ${NAME}!") # Substitute a variable into the message
Agora, se definirmos NAME
na linha de comando cmake
usando a opção -D
, o script irá usá-la:
$ cmake -DNAME=Newman -P hello.txtHello Newman!
Quando uma variável é indefinida, o padrão é uma string vazia:
$ cmake -P hello.txtHello !
Para definir uma variável dentro de um script, use o comando set
. O primeiro argumento é o nome da variável a atribuir, e o segundo argumento é o seu valor:
set(THING "funk")message("We want the ${THING}!")
Citações em torno de argumentos são opcionais, desde que não haja espaços ou referências a variáveis no argumento. Por exemplo, eu poderia ter escrito set("THING" funk)
na primeira linha acima – teria sido equivalente. Para a maioria dos comandos CMake (exceto if
e while
, descritos abaixo), a escolha de citar ou não tais argumentos é simplesmente uma questão de estilo. Quando o argumento é o nome de uma variável, eu tendo a não usar aspas.
Você pode simular uma estrutura de dados usando prefixos
CMake não tem classes, mas você pode simular uma estrutura de dados definindo um grupo de variáveis com nomes que começam com o mesmo prefixo. Em seguida, é possível procurar variáveis nesse grupo usando referências de variáveis aninhadas ${}
. Por exemplo, o seguinte script irá imprimir “John Smith vive em 123 Fake St.”:
set(JOHN_NAME "John Smith")set(JOHN_ADDRESS "123 Fake St")set(PERSON "JOHN")message("${${PERSON}_NAME} lives at ${${PERSON}_ADDRESS}.")
Você pode até mesmo usar referências de variáveis no nome da variável a ser definida. Por exemplo, se o valor de PERSON
ainda for “JOHN”, o seguinte irá definir a variável JOHN_NAME
para “John Goodman”:
set(${PERSON}_NAME "John Goodman")
Cada declaração é um comando
No CMake, cada declaração é um comando que leva uma lista de argumentos de string e não tem valor de retorno. Os argumentos são separados por espaços (não cotados). Como já vimos, o comando set
define uma variável no escopo do arquivo.
Como outro exemplo, o CMake tem um comando math
que executa aritmética. O primeiro argumento deve ser EXPR
, o segundo argumento é o nome da variável a atribuir, e o terceiro argumento é a expressão a avaliar – todas as strings. Note que na terceira linha abaixo, CMake substitui o valor da string de MY_SUM
no argumento de fechamento antes de passar o argumento para 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}.")
Existe um comando CMake para quase tudo que você precisará fazer. O comando string
permite-lhe efectuar uma manipulação avançada de strings, incluindo substituição regular de expressões. O comando file
pode ler ou escrever ficheiros, ou manipular caminhos do sistema de ficheiros.
Comandos de Controlo de Fluxo
As declarações de controlo de fluxo são comandos. Os comandos if
/endif
executam condicionalmente os comandos anexos. Os espaços em branco não importam, mas é comum indentar os comandos incluídos para readablidade. O seguinte verifica se a variável interna do CMake WIN32
está definida:
if(WIN32) message("You're running CMake on Windows.")endif()
CMake também tem while
/endwhile
comandos que, como você pode esperar, repetem os comandos incluídos desde que a condição seja verdadeira. Aqui está um loop que imprime todos os números Fibonacci até um milhão:
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()
CMake’s if
e while
as condições não são escritas da mesma forma que em outras línguas. Por exemplo, para realizar uma comparação numérica, você deve especificar LESS
como um argumento string, como mostrado acima. A documentação explica como escrever uma condição válida.
if
e while
são diferentes de outros comandos CMake, pois se o nome de uma variável for especificado sem aspas, o comando usará o valor da variável. No código acima, eu aproveitei esse comportamento escrevendo while(A LESS "1000000")
ao invés de while("${A}" LESS "1000000")
– ambas as formas são equivalentes. Outros comandos CMake não fazem isso.
Lists are Just Semicolon-Delimited Strings
CMake tem uma regra especial de substituição para argumentos não citados. Se todo o argumento é uma referência de variável sem aspas, e o valor da variável contém ponto-e-vírgula, CMake dividirá o valor nos pontos-e-vírgula e passará múltiplos argumentos para o comando de delimitação. Por exemplo, o seguinte passa três argumentos para math
:
set(ARGS "EXPR;T;1 + 1")math(${ARGS}) # Equivalent to calling math(EXPR T "1 + 1")
Por outro lado, argumentos citados nunca são divididos em múltiplos argumentos, mesmo após a substituição. CMake sempre passa uma string citada como um único argumento, deixando ponto-e-vírgula intacto:
set(ARGS "EXPR;T;1 + 1")message("${ARGS}") # Prints: EXPR;T;1 + 1
Se mais de dois argumentos forem passados para o comando set
, eles são unidos por ponto-e-vírgula, então atribuídos à variável especificada. Isto efetivamente cria uma lista a partir dos argumentos:
set(MY_LIST These are separate arguments)message("${MY_LIST}") # Prints: These;are;separate;arguments
Você pode manipular tais listas usando o comando list
comando:
set(MY_LIST These are separate arguments)list(REMOVE_ITEM MY_LIST "separate") # Removes "separate" from the listmessage("${MY_LIST}") # Prints: These;are;arguments
O comando foreach
/endforeach
aceita múltiplos argumentos. Iteratiza sobre todos os argumentos excepto o primeiro, atribuindo cada um à variável nomeada:
foreach(ARG These are separate arguments) message("${ARG}") # Prints each word on a separate lineendforeach()
Pode iterar sobre uma lista passando uma referência de variável não cotada para foreach
. Como com qualquer outro comando, CMake irá dividir o valor da variável e passar múltiplos argumentos para o comando:
foreach(ARG ${MY_LIST}) # Splits the list; passes items as arguments message("${ARG}") # Prints each item on a separate lineendforeach()
Functions Run In Their Own Scope; Macros Don’t
No CMake, você pode usar um par de comandos function
/endfunction
para definir uma função. Aqui está um que duplica o valor numérico do seu argumento, depois imprime o resultado:
function(doubleIt VALUE) math(EXPR RESULT "${VALUE} * 2") message("${RESULT}")endfunction()doubleIt("4") # Prints: 8
Functions run in their own scope. Nenhuma das variáveis definidas em uma função polui o escopo do autor da chamada. Se você quiser retornar um valor, você pode passar o nome de uma variável para sua função, então chame o comando set
com o 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
Simplesmente, um par de comandos macro
/endmacro
define uma macro. Ao contrário das funções, as macros são executadas no mesmo escopo que o seu chamador. Portanto, todas as variáveis definidas dentro de uma macro são definidas no escopo do autor da chamada. Podemos substituir a função anterior com o seguinte:
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
Todas as funções e macros aceitam um número arbitrário de argumentos. Os argumentos sem nome são expostos à função como uma lista, através de uma variável especial chamada ARGN
. Aqui está uma função que dobra cada argumento recebido, imprimindo cada um em uma linha 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
Incluindo Outros Scripts
CMake variables are defined at file scope. O comando include
executa outro script CMake no mesmo escopo que o script de chamada. É muito parecido com a diretiva #include
em C/C++. É tipicamente usado para definir um conjunto comum de funções ou macros no script de chamada. Ele usa a variável CMAKE_MODULE_PATH
como um caminho de busca.
O comando find_package
procura por scripts do formulário Find*.cmake
e também os executa no mesmo escopo. Tais scripts são frequentemente usados para ajudar a encontrar bibliotecas externas. Por exemplo, se existe um ficheiro chamado FindSDL2.cmake
no caminho de pesquisa, find_package(SDL2)
é equivalente a include(FindSDL2.cmake)
. (Note que existem várias maneiras de usar o comando find_package
– esta é apenas uma delas.)
CMake’s add_subdirectory
, por outro lado, cria um novo escopo, depois executa o script chamado CMakeLists.txt
a partir do diretório especificado nesse novo escopo. Você normalmente o usa para adicionar outro subprojeto baseado em CMake-, como uma biblioteca ou executável, ao projeto de chamada. Os alvos definidos pelo sub-projeto são adicionados ao pipeline de construção, a menos que especificado de outra forma. Nenhuma das variáveis definidas no script do subprojeto irá poluir o escopo do pai a menos que a opção set
comando PARENT_SCOPE
seja usada.
Como exemplo, aqui estão alguns dos scripts envolvidos quando você executa o CMake no projeto Turf:
Configurando e Definindo Propriedades
Um script CMake define alvos usando os comandos add_executable
, add_library
ou add_custom_target
. Uma vez criado um alvo, ele tem propriedades que você pode manipular usando os comandos get_property
e set_property
. Ao contrário das variáveis, os alvos são visíveis em todos os escopos, mesmo que tenham sido definidos em um subdiretório. Todas as propriedades de destino são strings.
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
Outras propriedades de destino incluem LINK_LIBRARIES
, INCLUDE_DIRECTORIES
e COMPILE_DEFINITIONS
. Essas propriedades são modificadas, indiretamente, pelos comandos target_link_libraries
, target_include_directories
e target_compile_definitions
. No final do script, o CMake usa essas propriedades de destino para gerar o pipeline de construção.
Existem propriedades para outras entidades do CMake também. Há um conjunto de propriedades de diretório em cada escopo de arquivo. Há um conjunto de propriedades globais que é acessível a partir de todos os scripts. E há um conjunto de propriedades do arquivo fonte para cada arquivo fonte C/C++.