Come spiegato nel mio post precedente, ogni progetto basato su CMake deve contenere uno script chiamato CMakeLists.txt
. Questo script definisce i target, ma può anche fare molte altre cose, come trovare librerie di terze parti o generare file header C++. Gli script di CMake hanno molta flessibilità.
Ogni volta che si integra una libreria esterna, e spesso quando si aggiunge il supporto per un’altra piattaforma, è necessario modificare lo script. Ho passato molto tempo a modificare gli script di CMake senza capire veramente il linguaggio, dato che la documentazione è piuttosto dispersiva, ma alla fine, le cose sono scattate. L’obiettivo di questo post è di portarvi allo stesso punto il più velocemente possibile.
Questo post non coprirà tutti i comandi incorporati in CMake, poiché ce ne sono centinaia, ma è una guida abbastanza completa alla sintassi e al modello di programmazione del linguaggio.
Hello World
Se crei un file chiamato hello.txt
con il seguente contenuto:
message("Hello world!") # A message to print
…puoi eseguirlo dalla linea di comando usando cmake -P hello.txt
. (L’opzione -P
esegue lo script dato, ma non genera una pipeline di compilazione). Come previsto, stampa “Hello world!”.
$ cmake -P hello.txtHello world!
All Variables Are Strings
In CMake, ogni variabile è una stringa. Puoi sostituire una variabile dentro un letterale di stringa circondandola con ${}
. Questo è chiamato un riferimento di variabile. Modifica hello.txt
come segue:
message("Hello ${NAME}!") # Substitute a variable into the message
Ora, se definiamo NAME
sulla linea di comando cmake
usando l’opzione -D
, lo script la userà:
$ cmake -DNAME=Newman -P hello.txtHello Newman!
Quando una variabile è indefinita, è di default una stringa vuota:
$ cmake -P hello.txtHello !
Per definire una variabile dentro uno script, usa il comando set
. Il primo argomento è il nome della variabile da assegnare, e il secondo argomento è il suo valore:
set(THING "funk")message("We want the ${THING}!")
Le virgolette intorno agli argomenti sono opzionali, finché non ci sono spazi o riferimenti a variabili nell’argomento. Per esempio, avrei potuto scrivere set("THING" funk)
nella prima riga sopra – sarebbe stato equivalente. Per la maggior parte dei comandi CMake (eccetto if
e while
, descritti più avanti), la scelta di citare o meno tali argomenti è semplicemente una questione di stile. Quando l’argomento è il nome di una variabile, tendo a non usare le virgolette.
Puoi simulare una struttura dati usando i prefissi
CMake non ha classi, ma puoi simulare una struttura dati definendo un gruppo di variabili con nomi che iniziano con lo stesso prefisso. Puoi poi cercare le variabili in quel gruppo usando riferimenti annidati a variabili ${}
. Per esempio, il seguente script stamperà “John Smith vive al 123 Fake St.”:
set(JOHN_NAME "John Smith")set(JOHN_ADDRESS "123 Fake St")set(PERSON "JOHN")message("${${PERSON}_NAME} lives at ${${PERSON}_ADDRESS}.")
Si possono anche usare riferimenti di variabili nel nome della variabile da impostare. Per esempio, se il valore di PERSON
è ancora “JOHN”, il seguente imposterà la variabile JOHN_NAME
a “John Goodman”:
set(${PERSON}_NAME "John Goodman")
Every Statement is a Command
In CMake, ogni statement è un comando che prende una lista di argomenti stringa e non ha valore di ritorno. Gli argomenti sono separati da spazi (non quotati). Come abbiamo già visto, il comando set
definisce una variabile allo scopo del file.
Come altro esempio, CMake ha un comando math
che esegue l’aritmetica. Il primo argomento deve essere EXPR
, il secondo argomento è il nome della variabile da assegnare, e il terzo argomento è l’espressione da valutare – tutte stringhe. Nota che nella terza linea sotto, CMake sostituisce il valore della stringa MY_SUM
nell’argomento che lo racchiude prima di passare l’argomento 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}.")
C’è un comando CMake per qualsiasi cosa tu debba fare. Il comando string
ti permette di eseguire una manipolazione avanzata delle stringhe, inclusa la sostituzione delle espressioni regolari. Il comando file
può leggere o scrivere file, o manipolare i percorsi del filesystem.
Comandi di controllo del flusso
Anche le istruzioni di controllo del flusso sono comandi. I comandi if
/endif
eseguono i comandi allegati in modo condizionale. Gli spazi vuoti non hanno importanza, ma è comune indentare i comandi allegati per la leggibilità. Il seguente controlla se la variabile incorporata di CMake WIN32
è impostata:
if(WIN32) message("You're running CMake on Windows.")endif()
CMake ha anche i comandi while
/endwhile
che, come ci si potrebbe aspettare, ripetono i comandi allegati finché la condizione è vera. Ecco un ciclo che stampa tutti i numeri di Fibonacci fino a un milione:
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()
Le condizioni if
e while
di MCake non sono scritte come in altri linguaggi. Per esempio, per eseguire un confronto numerico, devi specificare LESS
come argomento di stringa, come mostrato sopra. La documentazione spiega come scrivere una condizione valida.
if
e while
sono diversi dagli altri comandi di CMake in quanto se il nome di una variabile è specificato senza virgolette, il comando userà il valore della variabile. Nel codice sopra, ho approfittato di questo comportamento scrivendo while(A LESS "1000000")
invece di while("${A}" LESS "1000000")
– entrambe le forme sono equivalenti. Altri comandi di CMake non lo fanno.
Le liste sono solo stringhe delimitate da un punto e virgola
CMake ha una speciale regola di sostituzione per gli argomenti senza apici. Se l’intero argomento è un riferimento a una variabile senza virgolette, e il valore della variabile contiene punti e virgola, CMake dividerà il valore in corrispondenza dei punti e virgola e passerà più argomenti al comando che lo racchiude. Per esempio, il seguente passa tre argomenti a math
:
set(ARGS "EXPR;T;1 + 1")math(${ARGS}) # Equivalent to calling math(EXPR T "1 + 1")
D’altra parte, gli argomenti quotati non sono mai divisi in argomenti multipli, anche dopo la sostituzione. CMake passa sempre una stringa quotata come un singolo argomento, lasciando intatto il punto e virgola:
set(ARGS "EXPR;T;1 + 1")message("${ARGS}") # Prints: EXPR;T;1 + 1
Se più di due argomenti sono passati al comando set
, sono uniti da punto e virgola, quindi assegnati alla variabile specificata. Questo crea effettivamente una lista dagli argomenti:
set(MY_LIST These are separate arguments)message("${MY_LIST}") # Prints: These;are;separate;arguments
È possibile manipolare tali liste usando il 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
Il comando foreach
/endforeach
accetta più argomenti. Itera su tutti gli argomenti tranne il primo, assegnando ciascuno alla variabile nominata:
foreach(ARG These are separate arguments) message("${ARG}") # Prints each word on a separate lineendforeach()
Puoi iterare su una lista passando un riferimento di variabile non quotato a foreach
. Come con qualsiasi altro comando, CMake dividerà il valore della variabile e passerà più argomenti al 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
In CMake, puoi usare una coppia di comandi function
/endfunction
per definire una funzione. Eccone una che raddoppia il valore numerico del suo argomento, poi stampa il risultato:
function(doubleIt VALUE) math(EXPR RESULT "${VALUE} * 2") message("${RESULT}")endfunction()doubleIt("4") # Prints: 8
Le funzioni funzionano nel loro ambito. Nessuna delle variabili definite in una funzione inquina l’ambito del chiamante. Se volete restituire un valore, potete passare il nome di una variabile alla vostra funzione, poi chiamate il comando set
con l’argomento speciale 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
Similmente, una coppia di comandi macro
/endmacro
definisce una macro. A differenza delle funzioni, le macro funzionano nello stesso ambito del loro chiamante. Pertanto, tutte le variabili definite all’interno di una macro sono impostate nell’ambito del chiamante. Possiamo sostituire la funzione precedente con la seguente:
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
Sia le funzioni che le macro accettano un numero arbitrario di argomenti. Gli argomenti senza nome sono esposti alla funzione come una lista, attraverso una variabile speciale chiamata ARGN
. Ecco una funzione che raddoppia ogni argomento che riceve, stampando ognuno su una linea separata:
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
Includendo altri script
Le variabili di CMake sono definite allo scopo del file. Il comando include
esegue un altro script CMake nello stesso scope dello script chiamante. E’ molto simile alla direttiva #include
in C/C++. E’ tipicamente usata per definire un insieme comune di funzioni o macro nello script chiamante. Usa la variabile CMAKE_MODULE_PATH
come percorso di ricerca.
Il comando find_package
cerca script della forma Find*.cmake
e li esegue nello stesso ambito. Tali script sono spesso usati per aiutare a trovare librerie esterne. Per esempio, se c’è un file chiamato FindSDL2.cmake
nel percorso di ricerca, find_package(SDL2)
è equivalente a include(FindSDL2.cmake)
. (Nota che ci sono diversi modi di usare il comando find_package
– questo è solo uno di essi.)
Il comando add_subdirectory
di CMake, d’altra parte, crea un nuovo scope, poi esegue lo script chiamato CMakeLists.txt
dalla directory specificata in quel nuovo scope. Tipicamente lo usi per aggiungere un altro sottoprogetto basato su CMake, come una libreria o un eseguibile, al progetto chiamante. Gli obiettivi definiti dal sottoprogetto sono aggiunti alla pipeline di compilazione a meno che non sia specificato diversamente. Nessuna delle variabili definite nello script del sottoprogetto inquinerà l’ambito del genitore a meno che non venga usata l’opzione PARENT_SCOPE
del comando set
.
Come esempio, ecco alcuni degli script coinvolti quando esegui CMake sul progetto Turf:
Immissione e impostazione delle proprietà
Uno script CMake definisce i target usando i comandi add_executable
, add_library
o add_custom_target
. Una volta che un target è creato, ha delle proprietà che puoi manipolare usando i comandi get_property
e set_property
. A differenza delle variabili, i target sono visibili in ogni ambito, anche se sono stati definiti in una sottodirectory. Tutte le proprietà dei target sono stringhe.
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
Altre proprietà dei target includono LINK_LIBRARIES
, INCLUDE_DIRECTORIES
e COMPILE_DEFINITIONS
. Queste proprietà sono modificate, indirettamente, dai comandi target_link_libraries
, target_include_directories
e target_compile_definitions
. Alla fine dello script, CMake usa queste proprietà di destinazione per generare la pipeline di compilazione.
Ci sono anche proprietà per altre entità CMake. C’è un insieme di proprietà di directory in ogni ambito di file. C’è un insieme di proprietà globali che è accessibile da tutti gli script. E c’è un insieme di proprietà di file sorgente per ogni file sorgente C/C++.
.