Najprostszy, ale kompletny przykład CMake

117

W jakiś sposób jestem całkowicie zdezorientowany tym, jak działa CMake. Za każdym razem, gdy myślę, że zbliżam się do zrozumienia, jak powinno być napisane CMake, znika to w następnym przeczytanym przykładzie. Chcę tylko wiedzieć, jak powinienem ustrukturyzować mój projekt, aby mój CMake wymagał jak najmniejszej konserwacji w przyszłości. Na przykład nie chcę aktualizować mojego CMakeList.txt podczas dodawania nowego folderu w moim drzewie src, który działa dokładnie tak samo, jak wszystkie inne foldery src.

Tak wyobrażam sobie strukturę mojego projektu, ale proszę, to tylko przykład. Jeśli zalecany sposób różni się, proszę, powiedz mi i powiedz, jak to zrobić.

myProject
    src/
        module1/
            module1.h
            module1.cpp
        module2/
            [...]
        main.cpp
    test/
        test1.cpp
    resources/
        file.png
    bin
        [execute cmake ..]

Nawiasem mówiąc, ważne jest, aby mój program wiedział, gdzie są zasoby. Chciałbym poznać zalecany sposób zarządzania zasobami. Nie chcę uzyskiwać dostępu do moich zasobów za pomocą „../resources/file.png”

Arne
źródło
1
For example I don't want to update my CMakeList.txt when I am adding a new folder in my src treeczy możesz podać przykład IDE, które automatycznie zbiera źródła?
7
żadne idee zwykle nie gromadzą źródeł automatycznie, ponieważ nie muszą. Kiedy dodaję nowy plik lub folder, robię to w ramach ide, a projekt jest aktualizowany. System kompilacji po drugiej stronie nie zauważa, kiedy zmieniam niektóre pliki, więc pożądanym zachowaniem jest automatyczne gromadzenie wszystkich plików źródłowych
Arne
4
Jeśli widzę ten link, mam wrażenie, że CMake zawiódł w najważniejszym zadaniu, które chciał rozwiązać: ułatwiać tworzenie wieloplatformowego systemu budowania.
Arne,

Odpowiedzi:

94

po kilku badaniach mam teraz własną wersję najprostszego, ale kompletnego przykładu cmake. Oto ona i stara się omówić większość podstaw, w tym zasoby i opakowania.

jedną z niestandardowych rzeczy jest obsługa zasobów. Domyślnie cmake chce umieścić je w / usr / share /, / usr / local / share / i czymś równoważnym w systemie Windows. Chciałem mieć prosty plik zip / tar.gz, który można wypakować w dowolnym miejscu i uruchomić. Dlatego zasoby są ładowane względem pliku wykonywalnego.

Podstawową zasadą zrozumienia poleceń cmake jest następująca składnia: <function-name>(<arg1> [<arg2> ...])bez przecinka lub półkoloru. Każdy argument jest łańcuchem. foobar(3.0)i foobar("3.0")jest taki sam. możesz ustawić listy / zmienne za pomocą set(args arg1 arg2). Z tym zestawem zmiennych foobar(${args}) i foobar(arg1 arg2)faktycznie są takie same. Nieistniejąca zmienna jest równoważna pustej liście. Lista jest wewnętrznie tylko ciągiem znaków ze średnikami oddzielającymi elementy. Dlatego lista zawierająca tylko jeden element jest z definicji tylko tym elementem, nie ma miejsca na boksowanie. Zmienne mają charakter globalny. Funkcje wbudowane oferują pewną formę nazwanych argumentów przez fakt, że oczekują pewnych identyfikatorów, takich jak PUBLIClubDESTINATIONna swojej liście argumentów, aby pogrupować argumenty. Ale to nie jest cecha języka, te identyfikatory są również po prostu łańcuchami i analizowane przez implementację funkcji.

możesz sklonować wszystko z github

cmake_minimum_required(VERSION 3.0)
project(example_project)

###############################################################################
## file globbing ##############################################################
###############################################################################

# these instructions search the directory tree when cmake is
# invoked and put all files that match the pattern in the variables 
# `sources` and `data`
file(GLOB_RECURSE sources      src/main/*.cpp src/main/*.h)
file(GLOB_RECURSE sources_test src/test/*.cpp)
file(GLOB_RECURSE data resources/*)
# you can use set(sources src/main.cpp) etc if you don't want to
# use globing to find files automatically

###############################################################################
## target definitions #########################################################
###############################################################################

# add the data to the target, so it becomes visible in some IDE
add_executable(example ${sources} ${data})

# just for example add some compiler flags
target_compile_options(example PUBLIC -std=c++1y -Wall -Wfloat-conversion)

# this lets me include files relative to the root src dir with a <> pair
target_include_directories(example PUBLIC src/main)

# this copies all resource files in the build directory
# we need this, because we want to work with paths relative to the executable
file(COPY ${data} DESTINATION resources)

###############################################################################
## dependencies ###############################################################
###############################################################################

# this defines the variables Boost_LIBRARIES that contain all library names
# that we need to link to
find_package(Boost 1.36.0 COMPONENTS filesystem system REQUIRED)

target_link_libraries(example PUBLIC
  ${Boost_LIBRARIES}
  # here you can add any library dependencies
)

###############################################################################
## testing ####################################################################
###############################################################################

# this is for our testing framework
# we don't add REQUIRED because it's just for testing
find_package(GTest)

if(GTEST_FOUND)
  add_executable(unit_tests ${sources_test} ${sources})

  # we add this define to prevent collision with the main
  # this might be better solved by not adding the source with the main to the
  # testing target
  target_compile_definitions(unit_tests PUBLIC UNIT_TESTS)

  # this allows us to use our executable as a link library
  # therefore we can inherit all compiler options and library dependencies
  set_target_properties(example PROPERTIES ENABLE_EXPORTS on)

  target_link_libraries(unit_tests PUBLIC
    ${GTEST_BOTH_LIBRARIES}
    example
  )

  target_include_directories(unit_tests PUBLIC
    ${GTEST_INCLUDE_DIRS} # doesn't do anything on Linux
  )
endif()

###############################################################################
## packaging ##################################################################
###############################################################################

# all install commands get the same destination. this allows us to use paths
# relative to the executable.
install(TARGETS example DESTINATION example_destination)
# this is basically a repeat of the file copy instruction that copies the
# resources in the build directory, but here we tell cmake that we want it
# in the package
install(DIRECTORY resources DESTINATION example_destination)

# now comes everything we need, to create a package
# there are a lot more variables you can set, and some
# you need to set for some package types, but we want to
# be minimal here
set(CPACK_PACKAGE_NAME "MyExample")
set(CPACK_PACKAGE_VERSION "1.0.0")

# we don't want to split our program up into several things
set(CPACK_MONOLITHIC_INSTALL 1)

# This must be last
include(CPack)
Arne
źródło
8
@SteveLorimer Po prostu nie zgadzam się, że globowanie plików to zły styl, myślę, że ręczne kopiowanie drzewa plików do CMakeLists.txt jest złym stylem, ponieważ jest zbędne. Ale wiem, że ludzie nie zgadzają się w tym temacie, dlatego zostawiłem w kodzie komentarz, w którym można zamienić globbing na listę, która zawiera wszystkie pliki źródłowe. Wyszukaj set(sources src/main.cpp).
Arne,
3
@SteveLorimer tak, często musiałem ponownie wywoływać cmake. Za każdym razem, gdy dodam coś do drzewa katalogów, muszę ręcznie ponownie wywołać cmake, aby globbing get został ponownie oceniony. Jeśli umieścisz pliki w formacie CMakeLists.txt, normalna marka (lub ninja) spowoduje ponowne wywołanie cmake, więc nie możesz o tym zapomnieć. Jest też trochę bardziej przyjazny dla zespołu, ponieważ wtedy członkowie zespołu również nie mogą zapomnieć o wykonaniu cmake. Ale myślę, że nie trzeba dotykać pliku makefile, tylko dlatego, że ktoś dodał plik. Napisz to raz, a nikt nie powinien już o tym myśleć.
Arne
3
@SteveLorimer Ja też nie zgadzam się ze wzorem umieszczania jednego CMakeLists.txt w każdym katalogu projektów, po prostu rozprasza konfigurację projektu wszędzie, myślę, że jeden plik do zrobienia wszystkiego powinien wystarczyć, w przeciwnym razie stracisz przegląd, o czym jest faktycznie wykonywana w procesie kompilacji. Nie oznacza to, że nie mogą istnieć podkatalogi z własnym CMakeLists.txt, po prostu myślę, że powinien to być wyjątek.
Arne
2
Zakładając, że „VCS” jest skrótem od „systemu kontroli wersji” , nie ma to znaczenia. Problem nie polega na tym, że artefakty nie zostaną dodane do kontroli źródła. Problem polega na tym, że CMake nie będzie w stanie ponownie ocenić dodanych plików źródłowych. Nie spowoduje ponownego wygenerowania plików wejściowych systemu kompilacji. System kompilacji z radością trzyma się przestarzałych plików wejściowych, prowadząc do błędów (jeśli masz szczęście) lub niezauważony, jeśli zabraknie Ci szczęścia. GLOBbing tworzy lukę w łańcuchu obliczeń zależności. Jest to znacząca kwestia, a komentarz nie potwierdza tego odpowiednio.
Niespodziewane
2
CMake i VCS działają w całkowitej izolacji. VCS nie zna CMake, a CMake nie zna żadnego VCS. Nie ma między nimi związku. Chyba że zasugerujesz programistom ręczne podjęcie kroków, usunięcie informacji z VCS i oparcie się na heurystycznym czyszczeniu i ponownym uruchomieniu CMake. To oczywiście się nie skaluje i jest podatne na błąd, który jest charakterystyczny dla ludzi. Nie, przepraszam, jak dotąd nie przedstawiłeś przekonującego argumentu na temat plików GLOBbing.
Niespodziewane
39

Najbardziej podstawowy, ale kompletny przykład można znaleźć w samouczku dotyczącym CMake :

cmake_minimum_required (VERSION 2.6)
project (Tutorial)
add_executable(Tutorial tutorial.cxx)

Na przykład swojego projektu możesz mieć:

cmake_minimum_required (VERSION 2.6)
project (MyProject)
add_executable(myexec src/module1/module1.cpp src/module2/module2.cpp src/main.cpp)
add_executable(mytest test1.cpp)

Jeśli chodzi o dodatkowe pytanie, jedną z możliwości jest ponowne skorzystanie z samouczka: utwórz konfigurowalny plik nagłówkowy, który umieścisz w kodzie. W tym celu utwórz plik configuration.h.ino następującej zawartości:

#define RESOURCES_PATH "@RESOURCES_PATH@"

Następnie w Twoim CMakeLists.txtdodatku:

set(RESOURCES_PATH "${PROJECT_SOURCE_DIR}/resources/"
# configure a header file to pass some of the CMake settings
# to the source code
configure_file (
  "${PROJECT_SOURCE_DIR}/configuration.h.in"
  "${PROJECT_BINARY_DIR}/configuration.h"
)

# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
include_directories("${PROJECT_BINARY_DIR}")

Wreszcie, gdy potrzebujesz ścieżki w swoim kodzie, możesz zrobić:

#include "configuration.h"

...

string resourcePath = string(RESOURCE_PATH) + "file.png";
sgvd
źródło
bardzo dziękuję, szczególnie za RESOURCE_PATH, jakoś nie udało mi się zrozumieć, że plik configure_file jest tym, czego szukałem. Ale wszystkie pliki z projektu zostały dodane ręcznie. Czy jest lepszy sposób na proste zdefiniowanie wzorca, w którym wszystkie pliki są dodawane z drzewa src?
Arne,
Zobacz odpowiedź Dietera, ale także moje komentarze na temat tego, dlaczego nie powinieneś jej używać. Jeśli naprawdę chcesz to zautomatyzować, lepszym podejściem może być napisanie skryptu, który możesz uruchomić, aby zregenerować listę plików źródłowych (lub użyj środowiska IDE typu cmake, które robi to za Ciebie; nie znam żadnego).
sgvd
3
@sgvd string resourcePath = string(RESOURCE_PATH) + "file.png"IMHO złym pomysłem jest zakodowanie bezwzględnej ścieżki do katalogu źródłowego. Co jeśli musisz zainstalować swój projekt?
2
Wiem, że automatyczne zbieranie źródeł brzmi fajnie, ale może prowadzić do różnego rodzaju komplikacji. Zobacz to pytanie sprzed jakiegoś czasu, aby zapoznać się z krótką dyskusją: stackoverflow.com/q/10914607/1401351 .
Peter
2
Otrzymasz dokładnie ten sam błąd, jeśli nie uruchomisz cmake; ręczne dodawanie plików zajmuje jedną sekundę, uruchomienie cmake przy każdej kompilacji za każdym razem zajmuje jedną sekundę; faktycznie łamiesz cechę cmake; ktoś, kto pracuje nad tym samym projektem i pobiera twoje zmiany, zrobiłby: uruchamia make -> uzyskuje niezdefiniowane referencje -> miejmy nadzieję, że pamięta o ponownym uruchomieniu cmake, lub plik zawiera błąd -> uruchamia cmake -> uruchamia pomyślnie, natomiast jeśli dodasz plik robi ręcznie: biega skutecznie -> spędza czas z rodziną. Podsumowując, nie bądź leniwy i oszczędź sobie i innym bólu głowy w przyszłości.
sgvd
2

Tutaj piszę najprostszy, ale kompletny przykład plików CMakeLists.txt.

Kod źródłowy

  1. Samouczki z hello world do wieloplatformowych systemów Android / iOS / Web / Desktop.
  2. Na każdej platformie wydałem przykładową aplikację.
  3. Struktura pliku 08-cross_platform jest weryfikowana przez moją pracę
  4. To może nie być idealne, ale przydatne i sprawdzone rozwiązanie dla zespołu na własną rękę

Następnie zaoferowałem dokument ze szczegółami.

Jeśli masz jakieś pytania, skontaktuj się ze mną, a ja wyjaśnię.

MinamiTouma
źródło