As explained in my previous post, every CMake-based project must contain a script named CMakeLists.txt. Skrypt ten definiuje cele, ale może też robić wiele innych rzeczy, takich jak znajdowanie bibliotek innych firm lub generowanie plików nagłówkowych C++. Skrypty CMake mają dużą elastyczność.

Za każdym razem, gdy integrujesz zewnętrzną bibliotekę, a często także gdy dodajesz wsparcie dla innej platformy, będziesz musiał edytować skrypt. Spędziłem długi czas edytując skrypty CMake, nie rozumiejąc tak naprawdę tego języka, ponieważ dokumentacja jest dość rozproszona, ale w końcu wszystko się wyjaśniło. Celem tego postu jest doprowadzenie cię do tego samego punktu tak szybko, jak to możliwe.

Ten post nie obejmie wszystkich wbudowanych poleceń CMake’a, ponieważ są ich setki, ale jest to dość kompletny przewodnik po składni i modelu programowania tego języka.

Hello World

Jeśli utworzysz plik o nazwie hello.txt o następującej zawartości:

message("Hello world!") # A message to print

…możesz go uruchomić z wiersza poleceń używając opcji cmake -P hello.txt. (Opcja -P uruchamia podany skrypt, ale nie generuje potoku kompilacji). Zgodnie z oczekiwaniami, wypisuje on „Hello world!”.

$ cmake -P hello.txtHello world!

Wszystkie zmienne są łańcuchami

W CMake, każda zmienna jest łańcuchem. Możesz zastąpić zmienną wewnątrz literału łańcucha przez otoczenie jej ${}. Nazywa się to referencją do zmiennej. Zmodyfikuj hello.txt w następujący sposób:

message("Hello ${NAME}!") # Substitute a variable into the message

Teraz, jeśli zdefiniujemy NAME w wierszu poleceń cmake za pomocą opcji -D, skrypt będzie jej używał:

$ cmake -DNAME=Newman -P hello.txtHello Newman!

Gdy zmienna jest niezdefiniowana, domyślnie przyjmuje pusty łańcuch:

$ cmake -P hello.txtHello !

Aby zdefiniować zmienną wewnątrz skryptu, użyj polecenia set. Pierwszy argument to nazwa zmiennej do przypisania, a drugi to jej wartość:

set(THING "funk")message("We want the ${THING}!")

Cudzysłowy wokół argumentów są opcjonalne, o ile nie ma w nich spacji lub odwołań do zmiennych. Na przykład, mogłem napisać set("THING" funk) w pierwszej linii powyżej – byłoby to równoważne. Dla większości poleceń CMake (z wyjątkiem if i while, opisanych poniżej), wybór czy cytować takie argumenty jest po prostu kwestią stylu. Gdy argumentem jest nazwa zmiennej, raczej nie używam cudzysłowów.

Można symulować strukturę danych za pomocą prefiksów

Make nie ma klas, ale można symulować strukturę danych, definiując grupę zmiennych o nazwach zaczynających się od tego samego prefiksu. Następnie można wyszukiwać zmienne w tej grupie za pomocą zagnieżdżonych ${} odwołań do zmiennych. Na przykład, następujący skrypt wypisze „John Smith mieszka przy 123 Fake St.”:

set(JOHN_NAME "John Smith")set(JOHN_ADDRESS "123 Fake St")set(PERSON "JOHN")message("${${PERSON}_NAME} lives at ${${PERSON}_ADDRESS}.")

Można nawet użyć odwołań do zmiennych w nazwie zmiennej do ustawienia. Na przykład, jeśli wartością PERSON jest nadal „JOHN”, poniższe polecenie ustawi zmienną JOHN_NAME na „John Goodman”:

set(${PERSON}_NAME "John Goodman")

Każde polecenie jest komendą

W CMake każda instrukcja jest poleceniem, które przyjmuje listę argumentów łańcuchowych i nie ma wartości zwrotnej. Argumenty są oddzielone (niecytowanymi) spacjami. Jak już widzieliśmy, polecenie set definiuje zmienną w zakresie pliku.

Jako inny przykład, CMake posiada polecenie math, które wykonuje arytmetykę. Pierwszym argumentem musi być EXPR, drugim argumentem jest nazwa zmiennej do przypisania, a trzecim argumentem jest wyrażenie do oceny – wszystkie łańcuchy. Zauważ, że w trzecim wierszu poniżej CMake zastępuje wartość łańcuchową MY_SUM w dołączonym argumencie przed przekazaniem argumentu do 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}.")

Istnieje polecenie CMake do niemal wszystkiego, co będziesz potrzebował zrobić. Polecenie string pozwala wykonywać zaawansowane operacje na łańcuchach, w tym zamianę wyrażeń regularnych. Polecenie file może odczytywać lub zapisywać pliki, a także manipulować ścieżkami systemu plików.

Komendy kontroli przepływu

Nawet instrukcje kontroli przepływu są komendami. Polecenia if/endif wykonują dołączone polecenia warunkowo. Białe spacje nie mają znaczenia, ale często wcina się dołączone polecenia dla czytelności. Poniższe polecenie sprawdza, czy zmienna wbudowana w CMake WIN32 jest ustawiona:

if(WIN32) message("You're running CMake on Windows.")endif()

CMake posiada również polecenia while/endwhile, które, jak można się spodziewać, powtarzają załączone polecenia tak długo, jak długo warunek jest prawdziwy. Oto pętla, która wypisuje wszystkie liczby Fibonacciego do miliona:

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()

Warunki if i while w Make’u nie są zapisywane tak samo jak w innych językach. Na przykład, aby wykonać porównanie numeryczne, musisz podać LESS jako argument łańcuchowy, jak pokazano powyżej. Dokumentacja wyjaśnia, jak napisać poprawny warunek.

if i while różnią się od innych poleceń CMake tym, że jeśli nazwa zmiennej zostanie podana bez cudzysłowów, polecenie użyje wartości tej zmiennej. W powyższym kodzie skorzystałem z tego zachowania pisząc while(A LESS "1000000") zamiast while("${A}" LESS "1000000") – obie formy są równoważne. Inne polecenia CMake nie robią tego.

Listy są łańcuchami ograniczonymi średnikami

CMake ma specjalną regułę podstawiania dla niecytowanych argumentów. Jeśli cały argument jest referencją do zmiennej bez cudzysłowów, a wartość zmiennej zawiera średniki, CMake podzieli wartość na średniki i przekaże wiele argumentów do dołączonego polecenia. Na przykład, poniższe polecenie przekazuje trzy argumenty do math:

set(ARGS "EXPR;T;1 + 1")math(${ARGS}) # Equivalent to calling math(EXPR T "1 + 1")

Z drugiej strony, cytowane argumenty nigdy nie są dzielone na wiele argumentów, nawet po podstawieniu. CMake zawsze przekazuje cytowany łańcuch jako pojedynczy argument, pozostawiając nienaruszone średniki:

set(ARGS "EXPR;T;1 + 1")message("${ARGS}") # Prints: EXPR;T;1 + 1

Jeśli do polecenia set przekazano więcej niż dwa argumenty, są one łączone średnikami, a następnie przypisywane do określonej zmiennej. To efektywnie tworzy listę z argumentów:

set(MY_LIST These are separate arguments)message("${MY_LIST}") # Prints: These;are;separate;arguments

Takimi listami można manipulować za pomocą polecenia 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

Polecenie foreach/endforeach przyjmuje wiele argumentów. Wykonuje iterację po wszystkich argumentach z wyjątkiem pierwszego, przypisując każdy z nich do nazwanej zmiennej:

foreach(ARG These are separate arguments) message("${ARG}") # Prints each word on a separate lineendforeach()

Możesz wykonać iterację po liście, przekazując niecytowaną referencję do zmiennej do foreach. Jak w przypadku każdego innego polecenia, CMake podzieli wartość zmiennej i przekaże wiele argumentów do polecenia:

foreach(ARG ${MY_LIST}) # Splits the list; passes items as arguments message("${ARG}") # Prints each item on a separate lineendforeach()

Funkcje działają w swoim własnym zakresie; makra nie

W CMake można użyć pary poleceń function/endfunction do zdefiniowania funkcji. Oto jedna z nich, która podwaja wartość liczbową swojego argumentu, a następnie wypisuje wynik:

function(doubleIt VALUE) math(EXPR RESULT "${VALUE} * 2") message("${RESULT}")endfunction()doubleIt("4") # Prints: 8

Funkcje działają w swoim własnym zakresie. Żadna ze zmiennych zdefiniowanych w funkcji nie zanieczyszcza zakresu wywołującego. Jeśli chcesz zwrócić wartość, możesz przekazać nazwę zmiennej do swojej funkcji, a następnie wywołać polecenie set z argumentem specjalnym 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

Podobnie, para poleceń macro/endmacro definiuje makro. W przeciwieństwie do funkcji, makra działają w tym samym zakresie co ich wywołanie. Dlatego wszystkie zmienne zdefiniowane wewnątrz makra są ustawiane w zakresie wywołującego. Poprzednią funkcję możemy zastąpić następującą:

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

Zarówno funkcje jak i makra przyjmują dowolną liczbę argumentów. Nienazwane argumenty są przekazywane do funkcji w postaci listy, poprzez specjalną zmienną o nazwie ARGN. Oto funkcja, która podwaja każdy otrzymany argument, wypisując każdy z nich w osobnym wierszu:

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

Włączanie innych skryptów

Zmienne make definiuje się w zakresie pliku. Polecenie include wykonuje inny skrypt CMake w tym samym zakresie co skrypt wywołujący. Jest to bardzo podobne do dyrektywy #include w C/C++. Zazwyczaj jest używana do definiowania wspólnego zestawu funkcji lub makr w skrypcie wywołującym. Używa zmiennej CMAKE_MODULE_PATH jako ścieżki wyszukiwania.

Polecenie find_package szuka skryptów o postaci Find*.cmake i uruchamia je również w tym samym zakresie. Takie skrypty są często używane do pomocy w znajdowaniu zewnętrznych bibliotek. Na przykład, jeśli w ścieżce wyszukiwania znajduje się plik o nazwie FindSDL2.cmake, to find_package(SDL2) jest równoważne include(FindSDL2.cmake). (Zauważ, że istnieje kilka sposobów użycia polecenia find_package – to tylko jeden z nich.)

Z drugiej strony, polecenie add_subdirectoryMake’a tworzy nowy zakres, a następnie wykonuje skrypt o nazwie CMakeLists.txt z określonego katalogu w tym nowym zakresie. Zazwyczaj używa się go do dodania innego podprojektu opartego na CMake, takiego jak biblioteka lub plik wykonywalny, do projektu wywołującego. Cele zdefiniowane przez podprojekt są dodawane do potoku budowania, chyba że określono inaczej. Żadna ze zmiennych zdefiniowanych w skrypcie podprojektu nie zanieczyści zakresu rodzica, chyba że zostanie użyta opcja PARENT_SCOPE polecenia set.

Jako przykład, oto niektóre ze skryptów zaangażowanych podczas uruchamiania CMake w projekcie Turf:

Uzyskiwanie i ustawianie właściwości

Skrypt CMake definiuje cele za pomocą poleceń add_executable, add_library lub add_custom_target. Po utworzeniu celu ma on właściwości, którymi można manipulować za pomocą poleceń get_property i set_property. W przeciwieństwie do zmiennych, cele są widoczne w każdym zakresie, nawet jeśli zostały zdefiniowane w podkatalogu. Wszystkie właściwości celu są łańcuchami.

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

Inne właściwości celu obejmują LINK_LIBRARIES, INCLUDE_DIRECTORIES i COMPILE_DEFINITIONS. Właściwości te są modyfikowane pośrednio przez polecenia target_link_libraries, target_include_directories i target_compile_definitions. Na końcu skryptu CMake używa tych właściwości docelowych do wygenerowania potoku budowania.

Istnieją również właściwości dla innych elementów CMake. Istnieje zestaw właściwości katalogów dla każdego zakresu pliku. Istnieje zestaw właściwości globalnych, który jest dostępny ze wszystkich skryptów. I istnieje zestaw właściwości pliku źródłowego dla każdego pliku źródłowego C/C++.

.

Articles

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.