Som jag förklarade i mitt tidigare inlägg måste varje CMake-baserat projekt innehålla ett skript som heter CMakeLists.txt
. Det här skriptet definierar mål, men det kan också göra många andra saker, till exempel hitta bibliotek från tredje part eller generera C++ headerfiler. CMake-skript har stor flexibilitet.
Varje gång du integrerar ett externt bibliotek, och ofta när du lägger till stöd för en annan plattform, måste du redigera skriptet. Jag tillbringade lång tid med att redigera CMake-skript utan att riktigt förstå språket, eftersom dokumentationen är ganska utspridd, men så småningom klickade det. Målet med det här inlägget är att få dig till samma punkt så snabbt som möjligt.
Det här inlägget kommer inte att täcka alla CMakes inbyggda kommandon, eftersom det finns hundratals, men det är en ganska komplett guide till språkets syntax och programmeringsmodell.
Hello World
Om du skapar en fil som heter hello.txt
med följande innehåll:
message("Hello world!") # A message to print
…kan du köra den från kommandoraden med cmake -P hello.txt
. (Alternativet -P
kör det givna skriptet, men genererar ingen byggpipeline.) Som väntat skriver det ut ”Hello world!”.
$ cmake -P hello.txtHello world!
Alla variabler är strängar
I CMake är varje variabel en sträng. Du kan ersätta en variabel inuti en stränglitteral genom att omge den med ${}
. Detta kallas för en variabelreferens. Ändra hello.txt
på följande sätt:
message("Hello ${NAME}!") # Substitute a variable into the message
Om vi nu definierar NAME
på kommandoraden cmake
med hjälp av alternativet -D
kommer skriptet att använda den:
$ cmake -DNAME=Newman -P hello.txtHello Newman!
När en variabel är odefinierad, är den som standard en tom sträng:
$ cmake -P hello.txtHello !
För att definiera en variabel inuti ett skript använder du kommandot set
. Det första argumentet är namnet på variabeln som ska tilldelas och det andra argumentet är dess värde:
set(THING "funk")message("We want the ${THING}!")
Kvoter runt argumenten är valfria, så länge det inte finns några mellanslag eller variabelreferenser i argumentet. Jag kunde till exempel ha skrivit set("THING" funk)
i den första raden ovan – det skulle ha varit likvärdigt. För de flesta CMake-kommandon (utom if
och while
, som beskrivs nedan) är valet att citera sådana argument helt enkelt en stilfråga. När argumentet är namnet på en variabel tenderar jag att inte använda citationstecken.
Du kan simulera en datastruktur med hjälp av prefix
CMake har inga klasser, men du kan simulera en datastruktur genom att definiera en grupp variabler med namn som börjar med samma prefix. Du kan sedan söka upp variabler i den gruppen med hjälp av inbäddade ${}
variabelreferenser. Följande skript kommer till exempel att skriva ut ”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 till och med använda variabelreferenser i namnet på den variabel som ska ställas in. Om värdet på PERSON
till exempel fortfarande är ”JOHN”, kommer följande att sätta variabeln JOHN_NAME
till ”John Goodman”:
set(${PERSON}_NAME "John Goodman")
Varje Statement är ett kommando
I CMake är varje Statement ett kommando som tar en lista med strängargument och som inte har något returvärde. Argumenten är åtskilda med (okvotsade) mellanslag. Som vi redan har sett definierar kommandot set
en variabel i filomfång.
Som ett annat exempel har CMake ett math
-kommando som utför aritmetik. Det första argumentet måste vara EXPR
, det andra argumentet är namnet på variabeln som ska tilldelas och det tredje argumentet är uttrycket som ska utvärderas – alla strängar. Observera att på den tredje raden nedan ersätter CMake strängvärdet MY_SUM
i det omslutande argumentet innan argumentet överförs till 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}.")
Det finns ett CMake-kommando för nästan allt du behöver göra. Med kommandot string
kan du utföra avancerad strängmanipulation, inklusive ersättning av reguljära uttryck. Med kommandot file
kan du läsa eller skriva filer, eller manipulera sökvägar i filsystemet.
Flödeskontrollkommandon
Även flödeskontrollsatser är kommandon. Med if
/endif
-kommandona utförs de bifogade kommandona villkorligt. Vitt utrymme spelar ingen roll, men det är vanligt att man drar in de omslutna kommandona för att öka läsbarheten. Följande kontrollerar om CMakes inbyggda variabel WIN32
är inställd:
if(WIN32) message("You're running CMake on Windows.")endif()
CMake har också while
/endwhile
-kommandon som, som man kan förvänta sig, upprepar de omslutna kommandona så länge villkoret är sant. Här är en slinga som skriver ut alla Fibonacci-tal upp till en miljon:
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
och while
villkor är inte skrivna på samma sätt som i andra språk. För att utföra en numerisk jämförelse måste du till exempel ange LESS
som ett strängargument, som visas ovan. Dokumentationen förklarar hur man skriver ett giltigt villkor.
if
och while
skiljer sig från andra CMake-kommandon på så sätt att om namnet på en variabel anges utan citationstecken kommer kommandot att använda variabelns värde. I koden ovan utnyttjade jag detta beteende genom att skriva while(A LESS "1000000")
i stället för while("${A}" LESS "1000000")
– båda formerna är likvärdiga. Andra CMake-kommandon gör inte det.
Listor är bara semikolonavgränsade strängar
CMake har en speciell ersättningsregel för argument utan citattecken. Om hela argumentet är en variabelreferens utan citationstecken och variabelns värde innehåller semikolon kommer CMake att dela upp värdet vid semikolonerna och skicka flera argument till det omslutande kommandot. Till exempel, följande passerar tre argument till math
:
set(ARGS "EXPR;T;1 + 1")math(${ARGS}) # Equivalent to calling math(EXPR T "1 + 1")
Å andra sidan delas aldrig citerade argument upp i flera argument, inte ens efter substitution. CMake skickar alltid en citerad sträng som ett enda argument och lämnar semikolonerna intakta:
set(ARGS "EXPR;T;1 + 1")message("${ARGS}") # Prints: EXPR;T;1 + 1
Om mer än två argument skickas till kommandot set
förenas de med semikolon och tilldelas sedan den angivna variabeln. Detta skapar effektivt en lista från argumenten:
set(MY_LIST These are separate arguments)message("${MY_LIST}") # Prints: These;are;separate;arguments
Du kan manipulera sådana listor med hjälp av kommandot 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
Kommandot foreach
/endforeach
accepterar flera argument. Det itererar över alla argument utom det första och tilldelar vart och ett till den namngivna variabeln:
foreach(ARG These are separate arguments) message("${ARG}") # Prints each word on a separate lineendforeach()
Du kan iterera över en lista genom att skicka en icke angiven variabelreferens till foreach
. Precis som med alla andra kommandon kommer CMake att dela upp variabelns värde och skicka flera argument till kommandot:
foreach(ARG ${MY_LIST}) # Splits the list; passes items as arguments message("${ARG}") # Prints each item on a separate lineendforeach()
Funktioner körs i sitt eget område; makron gör inte det
I CMake kan du använda ett par function
/endfunction
-kommandon för att definiera en funktion. Här är en funktion som fördubblar det numeriska värdet för sitt argument och sedan skriver ut resultatet:
function(doubleIt VALUE) math(EXPR RESULT "${VALUE} * 2") message("${RESULT}")endfunction()doubleIt("4") # Prints: 8
Funktioner körs i sitt eget scope. Ingen av de variabler som definieras i en funktion förorenar anroparens scope. Om du vill returnera ett värde kan du skicka namnet på en variabel till din funktion och sedan anropa kommandot set
med specialargumentet 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
På samma sätt definierar ett par macro
/endmacro
-kommandon ett makro. Till skillnad från funktioner körs makron i samma omfattning som deras anropare. Därför sätts alla variabler som definieras inuti ett makro i anroparens scope. Vi kan ersätta den tidigare funktionen med följande:
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 och makron accepterar ett godtyckligt antal argument. Onominerade argument exponeras för funktionen som en lista, genom en särskild variabel med namnet ARGN
. Här är en funktion som fördubblar varje argument den tar emot och skriver ut varje argument på en separat rad:
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
Inkluderar andra skript
CMake-variabler definieras i filomfång. Kommandot include
utför ett annat CMake-skript i samma scope som det anropande skriptet. Det är ungefär som #include
-direktivet i C/C++. Det används vanligtvis för att definiera en gemensam uppsättning funktioner eller makron i det anropande skriptet. Det använder variabeln CMAKE_MODULE_PATH
som sökväg.
Kommandot find_package
letar efter skript av formen Find*.cmake
och kör även dem i samma scope. Sådana skript används ofta för att hjälpa till att hitta externa bibliotek. Om det till exempel finns en fil som heter FindSDL2.cmake
i sökvägen motsvarar find_package(SDL2)
include(FindSDL2.cmake)
. (Observera att det finns flera sätt att använda kommandot find_package
– detta är bara ett av dem.)
CMakes kommando add_subdirectory
skapar å andra sidan ett nytt scope och kör sedan skriptet CMakeLists.txt
från den angivna katalogen i det nya scope. Du använder det vanligtvis för att lägga till ett annat CMake-baserat underprojekt, t.ex. ett bibliotek eller en körbar fil, till det anropande projektet. De mål som definieras av underprojektet läggs till i byggpipelinen om inget annat anges. Ingen av de variabler som definieras i underprojektets skript kommer att förorena det överordnade scopeet om inte set
-kommandots PARENT_SCOPE
-alternativ används.
Som exempel kan du se några av de skript som är involverade när du kör CMake på Turf-projektet:
Hämtning och inställning av egenskaper
Ett CMake-skript definierar mål med hjälp av add_executable
-, add_library
– eller add_custom_target
-kommandona. När ett mål har skapats har det egenskaper som du kan manipulera med hjälp av kommandona get_property
och set_property
. Till skillnad från variabler är mål synliga i alla scope, även om de har definierats i en underkatalog. Alla målegenskaper är strängar.
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
Andra målegenskaper inkluderar LINK_LIBRARIES
, INCLUDE_DIRECTORIES
och COMPILE_DEFINITIONS
. Dessa egenskaper ändras indirekt av kommandona target_link_libraries
, target_include_directories
och target_compile_definitions
. I slutet av skriptet använder CMake dessa målegenskaper för att generera byggpipelinen.
Det finns också egenskaper för andra CMake-enheter. Det finns en uppsättning katalogegenskaper vid varje filomfång. Det finns en uppsättning globala egenskaper som är tillgängliga från alla skript. Och det finns en uppsättning egenskaper för källfilen för varje C/C++-källfil.