CMake: un esempio piccolissimo - STB1019/SkullOfSummer GitHub Wiki
Introduzione
Scrivere codice C è spesso un compito non troppo semplice. Anche quando è stato fatto però, spesso si incontrano errori di linking dovuti al fatto di non aver linkato la shared library corretta, piuttosto che difficoltà a reperire header file essenziali per la compilazione.
Uno strumento per effettuare la compilazione è il Makefile
; tuttavia, esso usa una sintassi spesso poco comprensibile. Un'alternativa più usata (anche se non del tutto priva di difetta) è cmake.
Cmake è un programma che, dato in ingresso file chiamati "CMakeLists.txt", permette di generare il file "Makefile" automaticamente. Questo file potrà poi essere usato per eseguire i classici comandi:
make
sudo make install
Esempio
Quello che andremo a vedere non è una teoria su come funziona CMake, ma è un piccolissimo esempio che vi permette di poter compilare agilmente programmi C la cui compilazione è abbastanza normale. Dato la cartella "MyAwesomeProject", strutturala nel seguente modo:
MyAwesomeProject
|---- build
|---- src
| |---- c
| | |------ CMakeLists.txt #beta
| |---- include
|---- CMakeLists.txt #alpha
Il "CMakeLists.txt" alpha è il file che devi richiamare con cmake
. E' essenziale essere nella cartella build
quando si chiama cmake
; per esempio:
cd build
cmake ..
make
make clean
make
sudo make install
Se costruisci i file "CMakeLists.txt" correttamente, potrai facilmente estendere il tuo progetto. Nella cartella c
dovrai mettere tutti i codici sorgente (aka "*.c") mentre in include
puoi mettere tutti i file "*.h".
I file CMakeLists.txt invece sono fatti in questo modo:
#CMakeLists.txt alpha
cmake_minimum_required(VERSION 3.5.1) #versione di cmake minima
#quando esegui 'cmake ..' questo messaggio sarà stampato a video
message(INFO "You should call cmake when you are in build or in build. You hould call 'cmake ..'")
set(PROJECT_NAME "MYAWESOMEPROJECTNAME")
project(${PROJECT_NAME} VERSION 1.0)
#se non ho espresso un metodo di build, lo imposto come Debug: in questo modo verrà automaticamente aggiunta la
#flag "-g" di compilazione
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif(NOT CMAKE_BUILD_TYPE)
add_definitions(-Wfatal-errors) #per aggiungere delle flag di compilazione
# aggiunta del comando per disinstallare
add_custom_target(uninstall
COMMAND xargs rm < install_manifest.txt
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
DEPENDS "${CMAKE_BINARY_DIR}/install_manifest.txt"
COMMENT "Removes everything installed by sudo make install"
VERBATIM
)
add_subdirectory(src/c) #per creare il Makefile serve andare a vedere il file CMakeLists.txt contenuto nella cartella src/c
#CMakeLists.txt beta
#Quando il compilatore effettuerà la compilazione, verrà aggiunta la flag "-I../include" (che dice dove stanno gli head file)
include_directories("../include")
#creo una variabile SOURCES e ci metto dentro tutti i file nella CWD (ossia src/c) che terminano con ".c"
file(GLOB SOURCES "*.c")
#faccio la stessa cosa con gli header
file(GLOB HEADERS "../include/*.h")
#ne devi attivare o una o l'altra! ${SOURCES} è la variabile creata poco fa
#questa funzione dice che il make deve creare una shared library (*.so)
add_library(${PROJECT_NAME} SHARED ${SOURCES})
#se attiva questa funzione dice che il make deve creare un'eseguibile
#add_executable(${PROJECT_NAME} ${SOURCES})
#elenco delle librerie da linkare per poter completare la fase di linking correttamente
target_link_libraries(${PROJECT_NAME} "m" "my_awesome_lib" "libgc")
#settiamo qualche informazione su come vogliamo creare il nostro oggetto (o la shared library o l'eseguibile)
set_target_properties(${PROJECT_NAME}
PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" #se è una libreria statica la dobbiamo creare in build/
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" #se è una libreria dinamica la dobbiamo creare in build/
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" # se è un esebuibile lo dobbiamo creare in build/
)
#usato per poter fare "sudo make install" quando stiamo costruendo una libreria dinamica
include(GNUInstallDirs)
install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES ${HEADERS} DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}")
#per poter usare immediatamente la libreria eseguiamo ldconfig
install(CODE "execute_process(COMMAND ldconfig)")
Notare che nel procedimento non abbiamo espresso esplicitamente come è stato creato l'eseguibile: cmake
ce lo crea facilmente da sé. Dobbiamo solo dirgli cosa vogliamo creare e che sorgenti e header considerato. Sotto le coperte, cmake
eseguirà:
- per ogni file in ${SOURCES},
gcc -c <singolo_file.c> -o <singolo_file.o> -I<headers in include_directories>
- linking in un unico oggetto finale (libreria o eseguibile):
gcc <tutti i file .o> -L<path dove prendere le librerie> -l<librerie che hai richiesto>
Un esempio funzionante è disponibile qui.
Una nota molto importante: cmake
analizza i codici sorgenti presenti in "src/c" e in "src/include" solo quando viene eseguito cmake ..
, non quando viene eseguito make
: perciò ogni volta che aggiungete un nuovo file (sia "*.c" che "*.h") dovrete rieseguire anche cmake ..
nella cartella build
!
Riferimenti
- tutorial base di CMake: http://derekmolloy.ie/hello-world-introductions-to-cmake/;
- uninstall in CMake: https://stackoverflow.com/a/41471920/1887602;
- documentazione relativa a ARCHIVE_OUTPUT_DIRECTORY: https://cmake.org/cmake/help/v3.0/prop_tgt/ARCHIVE_OUTPUT_DIRECTORY.html