După cum am explicat în postul meu anterior, fiecare proiect bazat pe CMake trebuie să conțină un script numit CMakeLists.txt
. Acest script definește țintele, dar poate face și o mulțime de alte lucruri, cum ar fi găsirea de biblioteci terțe sau generarea de fișiere de antet C++. Scripturile CMake au o mare flexibilitate.
De fiecare dată când integrați o bibliotecă externă, și adesea când adăugați suport pentru o altă platformă, va trebui să editați scriptul. Am petrecut mult timp editând scripturi CMake fără să înțeleg cu adevărat limbajul, deoarece documentația este destul de împrăștiată, dar, în cele din urmă, lucrurile au prins contur. Scopul acestei postări este de a vă duce în același punct cât mai repede posibil.
Această postare nu va acoperi toate comenzile încorporate în CMake, deoarece există sute, dar este un ghid destul de complet al sintaxei și al modelului de programare al limbajului.
Hello World
Dacă creați un fișier numit hello.txt
cu următorul conținut:
message("Hello world!") # A message to print
…îl puteți rula din linia de comandă folosind cmake -P hello.txt
. (Opțiunea -P
execută scriptul dat, dar nu generează un build pipeline). Așa cum era de așteptat, acesta tipărește „Hello world!”.
$ cmake -P hello.txtHello world!
Toate variabilele sunt șiruri de caractere
În CMake, fiecare variabilă este un șir de caractere. Puteți înlocui o variabilă în interiorul unui literal de șir de caractere, înconjurând-o cu ${}
. Acest lucru se numește o referință de variabilă. Modificați hello.txt
după cum urmează:
message("Hello ${NAME}!") # Substitute a variable into the message
Acum, dacă definim NAME
pe linia de comandă cmake
folosind opțiunea -D
, scriptul o va folosi:
$ cmake -DNAME=Newman -P hello.txtHello Newman!
Când o variabilă este nedefinită, aceasta este implicită la un șir gol:
$ cmake -P hello.txtHello !
Pentru a defini o variabilă în interiorul unui script, folosiți comanda set
. Primul argument este numele variabilei de atribuit, iar al doilea argument este valoarea acesteia:
set(THING "funk")message("We want the ${THING}!")
Cuvintele din jurul argumentelor sunt opționale, atâta timp cât nu există spații sau referințe la variabile în argument. De exemplu, aș fi putut scrie set("THING" funk)
în prima linie de mai sus – ar fi fost echivalent. Pentru majoritatea comenzilor CMake (cu excepția if
și while
, descrise mai jos), alegerea de a cita sau nu astfel de argumente este pur și simplu o chestiune de stil. Atunci când argumentul este numele unei variabile, tind să nu folosesc ghilimele.
Puteți simula o structură de date folosind prefixe
CMake nu are clase, dar puteți simula o structură de date prin definirea unui grup de variabile cu nume care încep cu același prefix. Puteți apoi să căutați variabilele din acel grup folosind referințe de variabile ${}
imbricate. De exemplu, următorul script va imprima „John Smith locuiește la 123 Fake St.”:
set(JOHN_NAME "John Smith")set(JOHN_ADDRESS "123 Fake St")set(PERSON "JOHN")message("${${PERSON}_NAME} lives at ${${PERSON}_ADDRESS}.")
Puteți utiliza chiar și referințe de variabile în numele variabilei de setat. De exemplu, dacă valoarea lui PERSON
este încă „JOHN”, următorul script va seta variabila JOHN_NAME
la „John Goodman”:
set(${PERSON}_NAME "John Goodman")
Care instrucțiune este o comandă
În CMake, fiecare instrucțiune este o comandă care ia o listă de argumente de tip șir de caractere și nu are valoare de întoarcere. Argumentele sunt separate prin spații (fără ghilimele). După cum am văzut deja, comanda set
definește o variabilă în domeniul de aplicare al fișierului.
Ca un alt exemplu, CMake are o comandă math
care efectuează aritmetică. Primul argument trebuie să fie EXPR
, al doilea argument este numele variabilei de atribuit, iar al treilea argument este expresia de evaluat – toate șiruri de caractere. Observați că pe a treia linie de mai jos, CMake înlocuiește valoarea șirului de caractere MY_SUM
în argumentul care îl înconjoară înainte de a trece argumentul la 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}.")
Există o comandă CMake pentru aproape orice trebuie să faceți. Comanda string
vă permite să efectuați manipularea avansată a șirurilor de caractere, inclusiv înlocuirea expresiilor regulate. Comanda file
poate citi sau scrie fișiere, sau manipula căile sistemului de fișiere.
Comande de control al fluxului
Chiar și instrucțiunile de control al fluxului sunt comenzi. Comenzile if
/endif
execută în mod condiționat comenzile anexate. Spațiul alb nu contează, dar este obișnuită indentarea comenzilor închise pentru lizibilitate. Următoarea comandă verifică dacă variabila încorporată de CMake WIN32
este setată:
if(WIN32) message("You're running CMake on Windows.")endif()
CMake are, de asemenea, comenzile while
/endwhile
care, așa cum v-ați putea aștepta, repetă comenzile închise atâta timp cât condiția este adevărată. Iată o buclă care tipărește toate numerele Fibonacci până la un milion:
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()
Condițiile if
și while
de la CMake nu sunt scrise la fel ca în alte limbaje. De exemplu, pentru a efectua o comparație numerică, trebuie să specificați LESS
ca argument de șir de caractere, așa cum se arată mai sus. Documentația explică cum să scrieți o condiție validă.
if
și while
sunt diferite de alte comenzi CMake prin faptul că, dacă numele unei variabile este specificat fără ghilimele, comanda va folosi valoarea variabilei. În codul de mai sus, am profitat de acest comportament scriind while(A LESS "1000000")
în loc de while("${A}" LESS "1000000")
– ambele forme sunt echivalente. Alte comenzi CMake nu fac acest lucru.
Listele sunt doar șiruri delimitate de punct și virgulă
CMake are o regulă specială de substituție pentru argumentele fără ghilimele. Dacă întregul argument este o referință la o variabilă fără ghilimele, iar valoarea variabilei conține punct și virgulă, CMake va împărți valoarea la punctul și virgulă și va transmite mai multe argumente comenzii care o înconjoară. De exemplu, următoarele transmit trei argumente către math
:
set(ARGS "EXPR;T;1 + 1")math(${ARGS}) # Equivalent to calling math(EXPR T "1 + 1")
Pe de altă parte, argumentele cotate nu sunt niciodată împărțite în mai multe argumente, chiar și după substituire. CMake trece întotdeauna un șir citat ca un singur argument, lăsând punct și virgulă intact:
set(ARGS "EXPR;T;1 + 1")message("${ARGS}") # Prints: EXPR;T;1 + 1
Dacă mai mult de două argumente sunt trecute la comanda set
, acestea sunt unite prin punct și virgulă, apoi sunt atribuite variabilei specificate. Acest lucru creează efectiv o listă din argumente:
set(MY_LIST These are separate arguments)message("${MY_LIST}") # Prints: These;are;separate;arguments
Puteți manipula astfel de liste cu ajutorul comenzii 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
Comanda foreach
/endforeach
acceptă mai multe argumente. Ea trece peste toate argumentele, cu excepția primului, atribuindu-le pe fiecare dintre ele variabilei numite:
foreach(ARG These are separate arguments) message("${ARG}") # Prints each word on a separate lineendforeach()
Puteți trece peste o listă prin transmiterea unei referințe de variabilă necifrată către foreach
. Ca și în cazul oricărei alte comenzi, CMake va diviza valoarea variabilei și va trece mai multe argumente la comandă:
foreach(ARG ${MY_LIST}) # Splits the list; passes items as arguments message("${ARG}") # Prints each item on a separate lineendforeach()
Funcțiile se execută în propriul domeniu; macrourile nu
În CMake, puteți folosi o pereche de comenzi function
/endfunction
pentru a defini o funcție. Iată una care dublează valoarea numerică a argumentului său, apoi imprimă rezultatul:
function(doubleIt VALUE) math(EXPR RESULT "${VALUE} * 2") message("${RESULT}")endfunction()doubleIt("4") # Prints: 8
function(doubleIt VALUE) math(EXPR RESULT "${VALUE} * 2") message("${RESULT}")endfunction()doubleIt("4") # Prints: 8
Funcțiile se execută în propriul domeniu de aplicare. Niciuna dintre variabilele definite într-o funcție nu poluează domeniul de acțiune al apelantului. Dacă doriți să returnați o valoare, puteți trece numele unei variabile în funcție, apoi apelați comanda set
cu argumentul special 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
În mod similar, o pereche de comenzi macro
/endmacro
definește o macro. Spre deosebire de funcții, macrocomenzile se execută în același domeniu ca și apelantul lor. Prin urmare, toate variabilele definite în interiorul unei macro sunt setate în domeniul de aplicare al apelantului. Putem înlocui funcția anterioară cu următoarea:
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
Atât funcțiile, cât și macrourile acceptă un număr arbitrar de argumente. Argumentele fără nume sunt expuse funcției sub forma unei liste, prin intermediul unei variabile speciale numite ARGN
. Iată o funcție care dublează fiecare argument pe care îl primește, tipărindu-l pe fiecare pe o linie separată:
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
Includerea altor scripturi
Variabilele MacCake sunt definite la nivelul fișierului. Comanda include
execută un alt script CMake în același domeniu ca și scriptul apelant. Seamănă foarte mult cu directiva #include
din C/C++. Este utilizată de obicei pentru a defini un set comun de funcții sau macro-uri în scriptul de apelare. Folosește variabila CMAKE_MODULE_PATH
ca o cale de căutare.
Comanda find_package
caută scripturi de forma Find*.cmake
și, de asemenea, le execută în același domeniu. Astfel de scripturi sunt adesea folosite pentru a ajuta la găsirea bibliotecilor externe. De exemplu, dacă există un fișier numit FindSDL2.cmake
în calea de căutare, find_package(SDL2)
este echivalent cu include(FindSDL2.cmake)
. (Rețineți că există mai multe moduri de utilizare a comenzii find_package
– acesta este doar unul dintre ele.)
Comanda add_subdirectory
a lui CMake, pe de altă parte, creează un nou domeniu de aplicare, apoi execută scriptul numit CMakeLists.txt
din directorul specificat în acel nou domeniu de aplicare. De obicei, o utilizați pentru a adăuga un alt subproiect bazat pe CMake, cum ar fi o bibliotecă sau un executabil, la proiectul apelant. Țintele definite de subproiect sunt adăugate la conducta de construcție, cu excepția cazului în care se specifică altfel. Niciuna dintre variabilele definite în scriptul subproiectului nu va polua domeniul de aplicare al părintelui, cu excepția cazului în care se folosește opțiunea PARENT_SCOPE
a comenzii set
.
Ca exemplu, iată câteva dintre scripturile implicate atunci când rulați CMake pe proiectul Turf:
Getting and Setting Properties
Un script CMake definește țintele folosind comenzile add_executable
, add_library
sau add_custom_target
. Odată ce o țintă este creată, aceasta are proprietăți pe care le puteți manipula folosind comenzile get_property
și set_property
. Spre deosebire de variabile, țintele sunt vizibile în orice domeniu, chiar dacă au fost definite într-un subdirector. Toate proprietățile țintelor sunt șiruri de caractere.
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
Alte proprietăți ale țintelor includ LINK_LIBRARIES
, INCLUDE_DIRECTORIES
și COMPILE_DEFINITIONS
. Aceste proprietăți sunt modificate, indirect, de comenzile target_link_libraries
, target_include_directories
și target_compile_definitions
. La sfârșitul scriptului, CMake folosește aceste proprietăți țintă pentru a genera conducta de construcție.
Există proprietăți și pentru alte entități CMake. Există un set de proprietăți de directoare la fiecare domeniu de cuprindere a fișierelor. Există un set de proprietăți globale care este accesibil din toate scripturile. Și există un set de proprietăți ale fișierului sursă pentru fiecare fișier sursă C/C++.
.