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à:

  1. per ogni file in ${SOURCES}, gcc -c <singolo_file.c> -o <singolo_file.o> -I<headers in include_directories>
  2. 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