blog.garaż.net

24 kwiecień 2010

CMake i kilka przydatnych rzeczy

CMake jest wieloplatformowym, w miarę wygodnym, narzędziem generującym pliki makefile oraz projekty dla co bardziej znanych IDE programistycznych. Ma prostą składnię, a po raz dobrze napisanym pliku konfiguracyjnym możemy zapomnieć o jego istnieniu i cieszyć się z możliwości łatwego zbudowania projektu na maszynie wyposażonej jedynie w narzędzia konsolowe (kompilator, make) lub wygodą pracy w Eclipsie, Visual Studio lub Xcode.

Jeśli jesteś zaciekawiony samym narzędziem CMake, ale nigdy go nie używałeś bez trudu dotrzesz do masy tekstów ułatwiających rozpoczęcie pracy, także w języku polskim. W tym tekście jednak chciałbym się skupić na sztuczkach, które ostatnio pomogły mi uprościć nieco pracę.

Automatyczne przeszukiwanie katalogów ze źródłami

Kawałek prostego kodu poniżej umożliwia wygenerowanie trzech zmiennych, w których znajdziemy pliki źródłowe (sources), pliki nagłówkowe (headers_files) oraz katalogi z nagłówkami (headers) bez potrzeby jawnego ich deklarowania. Dodatkowo funkcje działają rekurencyjnie co pozwala dowolnie manipulować strukturą projektu i wymaga jedynie późniejszego ponownego wywołania CMake'a do aktualizacji projektu\skryptu budującego. Makr używamy podając najwyższy katalog w hierarchii jaki chcemy przeszukać.

macro (add_sources_dir dir)
    file ( GLOB_RECURSE SOURCES_CPP FOLLOW_SYMLINKS "${dir}/*.cpp" )
    file ( GLOB_RECURSE SOURCES_CXX FOLLOW_SYMLINKS "${dir}/*.cxx" )
    file ( GLOB_RECURSE SOURCES_CC  FOLLOW_SYMLINKS "${dir}/*.cc" )
    file ( GLOB_RECURSE SOURCES_C   FOLLOW_SYMLINKS "${dir}/*.c" )
    set ( sources ${sources} ${SOURCES_CPP} ${SOURCES_CXX} ${SOURCES_CC} ${SOURCES_C} )
endmacro (add_sources_dir)

macro (add_headers_dir dir)
    file ( GLOB_RECURSE HEADERS_HPP FOLLOW_SYMLINKS "${dir}/*.hpp" )
    file ( GLOB_RECURSE HEADERS_HH  FOLLOW_SYMLINKS "${dir}/*.hh" )
    file ( GLOB_RECURSE HEADERS_H   FOLLOW_SYMLINKS "${dir}/*.h" )
    set ( headers_files ${headers_files} ${HEADERS_HPP} ${HEADERS_HH} ${HEADERS_H} )
    foreach (var ${headers_files})
        string ( REGEX MATCH ".*/" PATH ${var} )
        set ( headers ${headers} ${PATH} )
    endforeach (var)
endmacro (add_headers_dir)

Numer rewizji z SVN

W przypadku SVN można zrobić to na dwa sposoby. Pierwszy z nich może sprawiać problemy użytkownikom, którzy korzystają z graficznych narzędzi do kontroli wersji, ponieważ CMake domyślnie obsługuje tylko narzędzia konsolowe (po prostu szuka w systemie komendy svn). Drugi sposób polega na ręcznym wyłuskaniu informacji o rewizji z katalogu .svn i przynajmniej mi pozwolił uwolnić się od problemu niekompatybilnych narzędzi graficznych i konsolowych. Ponieważ nie ma róży bez kolców... ten sposób wymaga obecności narzędzi head i tail w systemie, można przygotować jednak własny program\skrypt, który ominie ten problem i jednocześnie umożliwi pobieranie numeru rewizji także z innych systemów SCM. Wszystko zależy od tego na jakim systemie pracujemy i jakie narzędzia będziemy chcieli wykorzystać.

Sam skrypt aktualizujący numer rewizji najlepiej wydzielić do osobnego pliku CMake'a i może wyglądać on przykładowo tak (sposób numer 1):

cmake_minimum_required ( VERSION 2.8 )
find_package ( subversion REQUIRED)
Subversion_WC_INFO ( . MYPROJECT )
set ( PROJECT_REVISION ${MYPROJECT_WC_REVISION})
configure_file ( include/revision.h.in include/revision.h )

Przykładowy sposób numer 2:

cmake_minimum_required ( VERSION 2.8 )
execute_process(COMMAND head -n4 .svn/entries COMMAND tail -n1 OUTPUT_VARIABLE
PROJECT_REVISION OUTPUT_STRIP_TRAILING_WHITESPACE)
configure_file ( include/revision.h.in include/revision.h )

Oprócz tego (jak widać po treści samych skryptów), potrzebujemy pliku na podstawie, które wygenerujemy nagłówek z kodem źródłowym i zmienną zawierającą nasz numer rewizji (szukaj w skrypcie miejsca z plikiem revision.h.in):

#ifndef REVISION_H_
#define REVISION_H_
#define REVISION "@PROJECT_REVISION@"
#endif

Na sam koniec w sumie najważniejszy moment, który sprawił najwięcej problemów (PS. u mnie nie działał sposób z add_custom_command i parametrem PRE_BUILD -- jedynie POST_BUILD był wykonywany po każdym przebudowaniu), w skrócie dodajemy nowy cel i ustawiamy jako zależność dla naszego głównego celu jakim jest program wynikowy, w którym wyświetlamy informację o rewizji:

add_custom_target ( revisionNumberUpdate COMMAND ${CMAKE_COMMAND} -P CMakeUpdateRevision.txt)
add_dependencies ( main revisionNumberUpdate )

Dodatkowo, jeśli system budujący nie chce przebudować modułu, w którym korzystamy z wygenerowanego pliku możemy dorzucić np. komendę touch:

add_custom_target ( revisionNumberUpdate COMMAND ${CMAKE_COMMAND} -P CMakeUpdateRevision.txt \; touch source/showRevision.c )

Podsumowanie

Z pewnością znajdzie się jeszcze kilka przydatnych sztuczek i wzorców, które ułatwią życie nie jednemu programiście korzystającemu z CMake, jednak te wydały mi się szczególnie przydatne, zwłaszcza po błądzeniu przez kilka godzin szukając eleganckiego rozwiązaniem wspomnianych problemów.

Komentarze

  • Lionix (2010-04-26 22:58:50):

    Ostatnio CMake nawet potrafi generować pliki projektu do Code::Blocksa, co mnie miło zaskoczyło i chętnie przeniosę się na CMake z okazji nowego projektu.

    Bardzo ciekawą rzeczą jest dodawanie daty ostatniej modyfikacji danej części projektu. Jest to o tyle ciekawe że od razu mówi nam kiedy ktoś ostatnio grzebał/popsuł. W projekcie mieliśmy gdzieś za hard-kodowane makro dla automake wyciągające wartość z svn info przy użyciu grepa. Okazało się że kilku różnych systemach i wersjach językowych daje inne wyniki.
    Rozwiązaniem na prędko okazało się

    svn info --xml | xpath -q -e
      '//info/entry/commit/date/text()' 

    Jednak jak wiadomo xpath nie wszędzie musi występować. Ciekawi mnie jak tutaj CMake sobie poradzi.

  • SebaS86 (2010-04-26 23:01:00):

    Aby pozbyć się problemu z lokalizacjami svna wystarczy ustawić zmienne środowiskowe odpowiadające za lokalizację - LC_ALL="C" powinno załatwić sprawę. ;)

Comments !