Som forklaret i mit tidligere indlæg, skal alle CMake-baserede projekter indeholde et script med navnet CMakeLists.txt
. Dette script definerer mål, men det kan også gøre en masse andre ting, f.eks. finde biblioteker fra tredjeparter eller generere C++-headerfiler. CMake-scripts har stor fleksibilitet.
Hver gang du integrerer et eksternt bibliotek, og ofte når du tilføjer understøttelse for en anden platform, skal du redigere scriptet. Jeg brugte lang tid på at redigere CMake-scripts uden rigtig at forstå sproget, da dokumentationen er ret spredt, men til sidst klikkede det hele. Målet med dette indlæg er at få dig til det samme punkt så hurtigt som muligt.
Dette indlæg vil ikke dække alle CMake’s indbyggede kommandoer, da der er hundredvis, men det er en ret komplet guide til sprogets syntaks og programmeringsmodel.
Hello World
Hvis du opretter en fil med navnet hello.txt
med følgende indhold:
message("Hello world!") # A message to print
…kan du køre den fra kommandolinjen ved hjælp af cmake -P hello.txt
. (Indstillingen -P
kører det givne script, men genererer ikke en build pipeline.) Som forventet udskriver den “Hello world!”.
$ cmake -P hello.txtHello world!
Alle variabler er strenge
I CMake er alle variabler en streng. Du kan erstatte en variabel inde i en strenglitteral ved at omgive den med ${}
. Dette kaldes en variabelreference. Ændr hello.txt
som følger:
message("Hello ${NAME}!") # Substitute a variable into the message
Nu, hvis vi definerer NAME
på cmake
-kommandolinjen ved hjælp af -D
-indstillingen, vil scriptet bruge den:
$ cmake -DNAME=Newman -P hello.txtHello Newman!
Når en variabel er udefineret, er den som standard en tom streng:
$ cmake -P hello.txtHello !
For at definere en variabel inde i et script skal du bruge set
-kommandoen. Det første argument er navnet på den variabel, der skal tildeles, og det andet argument er dens værdi:
set(THING "funk")message("We want the ${THING}!")
Anførselstegn omkring argumenter er valgfrie, så længe der ikke er mellemrum eller variabelreferencer i argumentet. Jeg kunne f.eks. have skrevet set("THING" funk)
i den første linje ovenfor – det ville have været tilsvarende. For de fleste CMake-kommandoer (undtagen if
og while
, som er beskrevet nedenfor) er valget af, om sådanne argumenter skal anføres i citationstegn, blot et spørgsmål om stil. Når argumentet er navnet på en variabel, har jeg en tendens til ikke at bruge anførselstegn.
Du kan simulere en datastruktur ved hjælp af præfikser
CMake har ikke klasser, men du kan simulere en datastruktur ved at definere en gruppe af variabler med navne, der begynder med samme præfiks. Du kan derefter slå op i variabler i denne gruppe ved hjælp af indlejrede ${}
variabelreferencer. Følgende script vil f.eks. udskrive “John Smith bor på 123 Fake St.”:
set(JOHN_NAME "John Smith")set(JOHN_ADDRESS "123 Fake St")set(PERSON "JOHN")message("${${PERSON}_NAME} lives at ${${PERSON}_ADDRESS}.")
Du kan endda bruge variabelreferencer i navnet på den variabel, der skal indstilles. Hvis værdien af PERSON
f.eks. stadig er “JOHN”, vil følgende eksempel sætte variablen JOHN_NAME
til “John Goodman”:
set(${PERSON}_NAME "John Goodman")
Hvert statement er en kommando
I CMake er ethvert statement en kommando, som tager en liste af strengargumenter og ikke har nogen returværdi. Argumenter er adskilt af (ikke anførselstegn) mellemrum. Som vi allerede har set, definerer set
-kommandoen en variabel i filomfanget.
Som et andet eksempel har CMake en math
-kommando, der udfører aritmetik. Det første argument skal være EXPR
, det andet argument er navnet på den variabel, der skal tildeles, og det tredje argument er det udtryk, der skal evalueres – alle strings. Bemærk, at CMake på den tredje linje nedenfor erstatter strengværdien af MY_SUM
i det omsluttende argument, før argumentet overføres til 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}.")
Der er en CMake-kommando til stort set alt, hvad du har brug for at gøre. Med kommandoen string
kan du udføre avanceret strengmanipulation, herunder udskiftning af regulære udtryk. Kommandoen file
kan læse eller skrive filer eller manipulere filsystemstier.
Flow Control Commands
Selv flow control statements er kommandoer. if
/endif
-kommandoerne udfører de omsluttede kommandoer betinget. Whitespace er ligegyldigt, men det er almindeligt at indrykke de omsluttede kommandoer af hensyn til læsbarheden. Følgende kontrollerer, om CMakes indbyggede variabel WIN32
er sat:
if(WIN32) message("You're running CMake on Windows.")endif()
CMake har også while
/endwhile
-kommandoer, der, som man kan forvente, gentager de omsluttede kommandoer, så længe betingelsen er sand. Her er en løkke, der udskriver alle Fibonacci-tallene op til en million:
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
og while
betingelser er ikke skrevet på samme måde som i andre sprog. For at udføre en numerisk sammenligning skal du f.eks. angive LESS
som et strengargument, som vist ovenfor. Dokumentationen forklarer, hvordan man skriver en gyldig betingelse.
if
og while
adskiller sig fra andre CMake-kommandoer ved, at hvis navnet på en variabel angives uden anførselstegn, vil kommandoen bruge variablens værdi. I ovenstående kode benyttede jeg mig af denne adfærd ved at skrive while(A LESS "1000000")
i stedet for while("${A}" LESS "1000000")
– begge former er ækvivalente. Andre CMake-kommandoer gør ikke dette.
Lister er bare semikolon-afgrænsede strenge
CMake har en særlig substitutionsregel for argumenter uden anførselstegn. Hvis hele argumentet er en variabelhenvisning uden anførselstegn, og variablens værdi indeholder semikoloner, vil CMake opdele værdien ved semikolonerne og sende flere argumenter til den omsluttende kommando. Eksempelvis videregiver følgende tre argumenter til math
:
set(ARGS "EXPR;T;1 + 1")math(${ARGS}) # Equivalent to calling math(EXPR T "1 + 1")
På den anden side bliver anførselstegn aldrig opdelt i flere argumenter, selv efter substitution. CMake overdrager altid en citeret streng som et enkelt argument, idet semikolonerne forbliver intakte:
set(ARGS "EXPR;T;1 + 1")message("${ARGS}") # Prints: EXPR;T;1 + 1
Hvis der overdrages mere end to argumenter til kommandoen set
, bliver de forenet med semikolonerne og derefter tildelt den angivne variabel. Dette skaber effektivt en liste ud fra argumenterne:
set(MY_LIST These are separate arguments)message("${MY_LIST}") # Prints: These;are;separate;arguments
Du kan manipulere sådanne lister ved hjælp af list
-kommandoen:
set(MY_LIST These are separate arguments)list(REMOVE_ITEM MY_LIST "separate") # Removes "separate" from the listmessage("${MY_LIST}") # Prints: These;are;arguments
Kommandoen foreach
/endforeach
accepterer flere argumenter. Den iterer over alle argumenter undtagen det første og tildeler hvert enkelt argument til den navngivne variabel:
foreach(ARG These are separate arguments) message("${ARG}") # Prints each word on a separate lineendforeach()
Du kan iterere over en liste ved at sende en ikke-angivne variabelreference til foreach
. Som med enhver anden kommando vil CMake opdele variabelens værdi og sende flere argumenter til kommandoen:
foreach(ARG ${MY_LIST}) # Splits the list; passes items as arguments message("${ARG}") # Prints each item on a separate lineendforeach()
Funktioner kører i deres eget område; det gør makroer ikke
I CMake kan du bruge et par function
/endfunction
-kommandoer til at definere en funktion. Her er en, der fordobler den numeriske værdi af sit argument og derefter udskriver resultatet:
function(doubleIt VALUE) math(EXPR RESULT "${VALUE} * 2") message("${RESULT}")endfunction()doubleIt("4") # Prints: 8
Funktioner kører i deres eget scope. Ingen af de variabler, der er defineret i en funktion, forurener opkalderens scope. Hvis du vil returnere en værdi, kan du give navnet på en variabel til din funktion og derefter kalde kommandoen set
med det særlige argument 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
Sådan definerer et par af macro
/endmacro
-kommandoer en makro. I modsætning til funktioner kører makroer i det samme omfang som deres opkalder. Derfor bliver alle variabler, der er defineret inde i en makro, sat i opkalderens scope. Vi kan erstatte den tidligere funktion med følgende:
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
Både funktioner og makroer accepterer et vilkårligt antal argumenter. Unavngivne argumenter eksponeres for funktionen som en liste gennem en særlig variabel med navnet ARGN
. Her er en funktion, der fordobler alle de argumenter, den modtager, og udskriver hvert argument på en separat linje:
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
Inddragelse af andre scripts
CMake-variabler er defineret på filområdet. Kommandoen include
udfører et andet CMake-script i samme scope som det kaldende script. Det er meget lig #include
-direktivet i C/C++. Den bruges typisk til at definere et fælles sæt af funktioner eller makroer i det kaldende script. Den bruger variablen CMAKE_MODULE_PATH
som en søgesti.
Kommandoen find_package
leder efter scripts af formen Find*.cmake
og kører dem også i samme område. Sådanne scripts bruges ofte til at hjælpe med at finde eksterne biblioteker. Hvis der f.eks. findes en fil med navnet FindSDL2.cmake
i søgestien, svarer find_package(SDL2)
til include(FindSDL2.cmake)
. (Bemærk, at der er flere måder at bruge kommandoen find_package
på – dette er blot en af dem.)
CMakes add_subdirectory
-kommando opretter på den anden side et nyt scope og udfører derefter scriptet CMakeLists.txt
fra den angivne mappe i det nye scope. Du bruger den typisk til at tilføje et andet CMake-baseret underprojekt, f.eks. et bibliotek eller en eksekverbar fil, til det kaldende projekt. De mål, der er defineret af underprojektet, tilføjes til build pipelinen, medmindre andet er angivet. Ingen af de variabler, der er defineret i underprojektets script, forurener det overordnede scope, medmindre set
-kommandoens PARENT_SCOPE
-indstilling anvendes.
Som eksempel er her nogle af de scripts, der er involveret, når du kører CMake på Turf-projektet:
Oprettelse og indstilling af egenskaber
Et CMake-script definerer mål ved hjælp af add_executable
-, add_library
– eller add_custom_target
-kommandoerne. Når et mål er oprettet, har det egenskaber, som du kan manipulere ved hjælp af get_property
– og set_property
-kommandoerne. I modsætning til variabler er targets synlige i alle scope, selv om de er defineret i en undermappe. Alle målegenskaber er 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
Andre målegenskaber omfatter LINK_LIBRARIES
, INCLUDE_DIRECTORIES
og COMPILE_DEFINITIONS
. Disse egenskaber ændres indirekte af kommandoerne target_link_libraries
, target_include_directories
og target_compile_definitions
. I slutningen af scriptet bruger CMake disse målegenskaber til at generere build-pipelinen.
Der er også egenskaber for andre CMake-enheder. Der er et sæt mappeegenskaber ved hvert filomfang. Der er et sæt af globale egenskaber, som er tilgængelige fra alle scripts. Og der er et sæt kildefilegenskaber for hver C/C++-kildefil.