Come posso passare git SHA1 al compilatore come definizione usando cmake?

In un Makefile questo sarebbe fatto con qualcosa di simile:

g++ -DGIT_SHA1="`git log -1 | head -n 1`" ... 

Questo è molto utile, perché il binario conosce il commit esatto SHA1 in modo che possa scaricarlo in caso di segfault.

Come posso ottenere lo stesso risultato con CMake?

Ho creato alcuni moduli CMake che eseguono peer in un repository git per versioni e scopi simili – sono tutti nel mio repository su https://github.com/rpavlik/cmake-modules

La cosa buona di queste funzioni è che imporranno una riconfigurazione (una replica di cmake) prima di una build ogni volta che l’HEAD commetterà dei cambiamenti. A differenza di fare qualcosa solo una volta con execute_process, non è necessario ricordare di ri-cmake per aggiornare la definizione di hash.

Per questo scopo specifico, è necessario almeno i file GetGitRevisionDescription.cmake e GetGitRevisionDescription.cmake.in . Quindi, nel tuo file CMakeLists.txt principale, avresti qualcosa di simile

 list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/whereYouPutMyModules/") include(GetGitRevisionDescription) get_git_head_revision(GIT_REFSPEC GIT_SHA1) 

Quindi, potresti aggiungerlo come una definizione a livello di sistema (che purtroppo causerebbe un sacco di ricostruzione)

 add_definitions("-DGIT_SHA1=${GIT_SHA1}") 

o, la mia alternativa suggerita: Crea un file sorgente generato. Crea questi due file nella tua fonte:

GitSHA1.cpp.in:

 #define GIT_SHA1 "@[email protected]" const char g_GIT_SHA1[] = GIT_SHA1; 

GitSHA1.h:

 extern const char g_GIT_SHA1[]; 

Aggiungi questo al tuo CMakeLists.txt (assumendo che tu abbia un elenco di file sorgente in SOURCES):

 configure_file("${CMAKE_CURRENT_SOURCE_DIR}/GitSHA1.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/GitSHA1.cpp" @ONLY) list(APPEND SOURCES "${CMAKE_CURRENT_BINARY_DIR}/GitSHA1.cpp" GitSHA1.h) 

Quindi, hai una variabile globale che contiene la tua stringa SHA – l’intestazione con l’extern non cambia quando lo SHA lo fa, quindi puoi semplicemente includere quel luogo in cui vuoi fare riferimento alla stringa, e quindi solo il CPP generato deve essere ricompilato su ogni commit per darti accesso allo SHA ovunque.

L’ho fatto in modo tale da generare:

 const std::string Version::GIT_SHA1 = "e7fb69fb8ee93ac66f006406781138562d0250fb"; const std::string Version::GIT_DATE = "Thu Jan 9 14:17:56 2014"; const std::string Version::GIT_COMMIT_SUBJECT = "Fix all the bugs"; 

Se lo spazio di lavoro che ha eseguito la build aveva modifiche in sospeso, senza commit, la stringa SHA1 di cui sopra sarà suffissa con -dirty .

In CMakeLists.txt :

 # the commit's SHA1, and whether the building workspace was dirty or not execute_process(COMMAND "${GIT_EXECUTABLE}" describe --match=NeVeRmAtCh --always --abbrev=40 --dirty WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" OUTPUT_VARIABLE GIT_SHA1 ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) # the date of the commit execute_process(COMMAND "${GIT_EXECUTABLE}" log -1 --format=%ad --date=local WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" OUTPUT_VARIABLE GIT_DATE ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) # the subject of the commit execute_process(COMMAND "${GIT_EXECUTABLE}" log -1 --format=%s WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" OUTPUT_VARIABLE GIT_COMMIT_SUBJECT ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) # generate version.cc configure_file("${CMAKE_CURRENT_SOURCE_DIR}/version.cc.in" "${CMAKE_CURRENT_BINARY_DIR}/version.cc" @ONLY) list(APPEND SOURCES "${CMAKE_CURRENT_BINARY_DIR}/version.cc" version.hh) 

Ciò richiede version.cc.in :

 #include "version.hh" using namespace my_app; const std::string Version::GIT_SHA1 = "@[email protected]"; const std::string Version::GIT_DATE = "@[email protected]"; const std::string Version::GIT_COMMIT_SUBJECT = "@[email protected]"; 

E version.hh :

 #pragma once #include  namespace my_app { struct Version { static const std::string GIT_SHA1; static const std::string GIT_DATE; static const std::string GIT_COMMIT_SUBJECT; }; } 

Quindi nel codice posso scrivere:

 cout << "Build SHA1: " << Version::GIT_SHA1 << endl; 

Io userei sth. come questo nel mio CMakeLists.txt:

 exec_program( "git" ${CMAKE_CURRENT_SOURCE_DIR} ARGS "describe" OUTPUT_VARIABLE VERSION ) string( REGEX MATCH "-g.*$" VERSION_SHA1 ${VERSION} ) string( REGEX REPLACE "[-g]" "" VERSION_SHA1 ${VERSION_SHA1} ) add_definitions( -DGIT_SHA1="${VERSION_SHA1}" ) 

Sarebbe bello avere una soluzione che cattura le modifiche al repository (da git describe --dirty ), ma triggers la ricompilazione solo se qualcosa sulle informazioni git è cambiato.

Alcune delle soluzioni esistenti:

  1. Usa ‘execute_process’. Questo ottiene solo le informazioni git al momento della configurazione e può perdere le modifiche al repository.
  2. Dipende da .git/logs/HEAD . Questo fa scattare la ricompilazione solo quando qualcosa nel repository cambia, ma manca le modifiche per ottenere lo stato ‘-dirty’.
  3. Utilizzare un comando personalizzato per ribuild le informazioni sulla versione ogni volta che viene eseguita una generazione. Ciò intercetta le modifiche risultanti nello stato -dirty , ma triggers sempre una ricompilazione (in base al timestamp aggiornato del file delle informazioni sulla versione)

Una soluzione alla terza soluzione consiste nell’utilizzare il comando “copy_if_different” di CMake, pertanto la data / ora sul file delle informazioni sulla versione cambia solo se cambia il contenuto.

I passaggi nel comando personalizzato sono:

  1. Raccogli le informazioni git in un file temporaneo
  2. Usa ‘copy_if_different’ per copiare il file temporaneo nel file reale
  3. Elimina il file temporaneo, per triggersre il comando personalizzato da eseguire nuovamente sul prossimo ‘make’

Il codice (prendendo a prestito pesantemente dalla soluzione di kralyk):

 # The 'real' git information file SET(GITREV_BARE_FILE git-rev.h) # The temporary git information file SET(GITREV_BARE_TMP git-rev-tmp.h) SET(GITREV_FILE ${CMAKE_BINARY_DIR}/${GITREV_BARE_FILE}) SET(GITREV_TMP ${CMAKE_BINARY_DIR}/${GITREV_BARE_TMP}) ADD_CUSTOM_COMMAND( OUTPUT ${GITREV_TMP} COMMAND ${CMAKE_COMMAND} -E echo_append "#define GIT_BRANCH_RAW " > ${GITREV_TMP} COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD >> ${GITREV_TMP} COMMAND ${CMAKE_COMMAND} -E echo_append "#define GIT_HASH_RAW " >> ${GITREV_TMP} COMMAND ${GIT_EXECUTABLE} describe --always --dirty --abbrev=40 --match="NoTagWithThisName" >> ${GITREV_TMP} COMMAND ${CMAKE_COMMAND} -E copy_if_different ${GITREV_TMP} ${GITREV_FILE} COMMAND ${CMAKE_COMMAND} -E remove ${GITREV_TMP} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} VERBATIM ) # Finally, the temporary file should be added as a dependency to the target ADD_EXECUTABLE(test source.cpp ${GITREV_TMP}) 

La seguente soluzione si basa sull’osservazione che Git aggiorna il log HEAD ogni volta che si pull o si pull commit qualcosa. Nota che, ad esempio, il suggerimento di Drew sopra aggiornerà le informazioni Git solo se ricostruisci la cache di CMake manualmente dopo ogni commit .

Uso un “comando personalizzato” CMake che genera un file di intestazione a una riga ${SRCDIR}/gitrevision.hh dove ${SRCDIR} è la radice del tuo albero dei sorgenti. Sarà rifatto solo quando viene effettuato un nuovo commit. Ecco la magia CMake necessaria con alcuni commenti:

 # Generate gitrevision.hh if Git is available # and the .git directory is present # this is the case when the software is checked out from a Git repo find_program(GIT_SCM git DOC "Git version control") mark_as_advanced(GIT_SCM) find_file(GITDIR NAMES .git PATHS ${CMAKE_SOURCE_DIR} NO_DEFAULT_PATH) if (GIT_SCM AND GITDIR) # Create gitrevision.hh # that depends on the Git HEAD log add_custom_command(OUTPUT ${SRCDIR}/gitrevision.hh COMMAND ${CMAKE_COMMAND} -E echo_append "#define GITREVISION " > ${SRCDIR}/gitrevision.hh COMMAND ${GIT_SCM} log -1 "--pretty=format:%h %ai" >> ${SRCDIR}/gitrevision.hh DEPENDS ${GITDIR}/logs/HEAD VERBATIM ) else() # No version control # eg when the software is built from a source tarball # and gitrevision.hh is packaged with it but no Git is available message(STATUS "Will not remake ${SRCDIR}/gitrevision.hh") endif() 

Il contenuto di gitrevision.hh sarà simile a questo:

 #define GITREVISION cb93d53 2014-03-13 11:08:15 +0100 

Se vuoi cambiare questo allora modifica il --pretty=format: specificando di conseguenza. Ad esempio, utilizzando %H anziché %h verrà stampato il digest SHA1 completo. Vedi il manuale di Git per i dettagli.

Realizzando gitrevision.hh un gitrevision.hh e proprio file di intestazione C ++ con guardie incluse ecc. È lasciato come esercizio al lettore 🙂

Non posso aiutarti con il lato di CMake, ma per quanto riguarda Git side ti consiglio di dare un’occhiata a come il kernel Linux e il progetto Git stesso lo fa, tramite lo script GIT-VERSION-GEN , o come fa tig nel suo Makefile , usando git describe se è presente git repository, ricadendo su ” version ” / ” VERSION ” / ” GIT-VERSION-FILE ” generati e presenti nei tarball, tornando infine al valore predefinito hardcoded in script (o Makefile).

La prima parte (usando git describe ) richiede che tagga le versioni usando tag annotati (e possibilmente firmati da GPG). Oppure usa git describe --tags per usare anche tag leggeri.

Ecco la mia soluzione, che penso sia ragionevolmente breve ma efficace 😉

Per prima cosa, è necessario un file nell’albero dei sorgenti (io lo chiamo git-rev.h.in ), dovrebbe assomigliare a questo:

 #define STR_EXPAND(x) #x #define STR(x) STR_EXPAND(x) #define GIT_REV STR(GIT_REV_) #define GIT_REV_ \ 

(Per favore non preoccuparti di quelle macro, è un trucco un po ‘folle per creare una stringa da un valore grezzo.) È essenziale che questo file abbia esattamente una newline vuota alla fine in modo che il valore possa essere aggiunto.

E ora questo codice va nel rispettivo file CMakeLists.txt :

 # --- Git revision --- add_dependencies(your_awesome_target gitrev) #put name of your target here include_directories(${CMAKE_CURRENT_BINARY_DIR}) #so that the include file is found set(gitrev_in git-rev.h.in) #just filenames, feel free to change them... set(gitrev git-rev.h) add_custom_target(gitrev ${CMAKE_COMMAND} -E remove -f ${CMAKE_CURRENT_BINARY_DIR}/${gitrev} COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/${gitrev_in} ${CMAKE_CURRENT_BINARY_DIR}/${gitrev} COMMAND git rev-parse HEAD >> ${CMAKE_CURRENT_BINARY_DIR}/${gitrev} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} #very important, otherwise git repo might not be found in shadow build VERBATIM #portability wanted ) 

Questo comando fa in modo che git-rev.h.in venga copiato nell’albero di compilazione come git-rev.h e alla fine git revision viene aggiunto.

Quindi tutto ciò che devi fare è includere git-rev.h in uno dei tuoi file e fare ciò che vuoi con la macro GIT_REV , che fornisce l’hash di revisione git corrente come valore di stringa.

La cosa bella di questa soluzione è che git-rev.h viene ricreato ogni volta che si crea il target associato, quindi non è necessario eseguire cmake più e più volte.

Dovrebbe anche essere abbastanza portatile – non sono stati usati strumenti esterni non portatili e anche la maledetta finestra di cmd supporta gli operatori > e >> 😉

Se CMake non ha una funzionalità integrata per eseguire questa sostituzione, è ansible scrivere uno script di shell wrapper che legge un file modello, sostituisce l’hash SHA1 come sopra nella posizione corretta (utilizzando sed , ad esempio), crea il Realizza il file di build di CMake, quindi chiama CMake per creare il tuo progetto.

Un approccio leggermente diverso potrebbe essere quello di rendere opzionale la sostituzione SHA1. Dovresti creare il file CMake con un valore hash fittizio come "NO_OFFICIAL_SHA1_HASH" . Quando gli sviluppatori costruiscono le proprie build dalle loro directory di lavoro, il codice creato non include un valore hash SHA1 (solo il valore dummy) perché il codice dalla directory di lavoro non ha ancora un valore hash SHA1 corrispondente ancora.

D’altra parte, quando una build ufficiale viene creata dal tuo server di build, da fonti estratte da un repository centrale, allora conosci il valore hash SHA1 per il codice sorgente. A quel punto, è ansible sostituire il valore hash nel file CMake e quindi eseguire CMake.

Soluzione

Semplicemente aggiungendo del codice a soli 2 file: CMakeList.txt e main.cpp .

1. CMakeList.txt

 # git commit hash macro execute_process( COMMAND git log -1 --format=%h WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE GIT_COMMIT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE ) add_definitions("-DGIT_COMMIT_HASH=\"${GIT_COMMIT_HASH}\"") 

2. main.cpp

 inline void LogGitCommitHash() { #ifndef GIT_COMMIT_HASH #define GIT_COMMIT_HASH "0000000" // 0000000 means uninitialized #endif std::cout << "GIT_COMMIT_HASH[" << GIT_COMMIT_HASH << "]"; // 4f34ee8 } 

Spiegazione

In CMakeList.txt , il comando CMake execute_process() viene utilizzato per chiamare il comando git log -1 --format=%h che fornisce l'abbreviazione breve e unica per i valori SHA-1 in stringa come 4f34ee8 . Questa stringa è assegnata alla variabile CMake chiamata GIT_COMMIT_HASH . Il comando CMake add_definitions() definisce la macro GIT_COMMIT_HASH sul valore di 4f34ee8 poco prima della compilazione di gcc. Il valore hash viene utilizzato per sostituire la macro in codice C ++ dal preprocessore e quindi esiste nel file object main.o e nei file binari compilati a.out .

Nota a margine

Un altro modo per ottenere è usare il comando CMake chiamato configure_file() , ma non mi piace usarlo perché il file non esiste prima dell'esecuzione di CMake.

Per un modo rapido e sporco, possibilmente non portatile, di portare il git SHA-1 in un progetto C o C ++ usando CMake, lo uso in CMakeLists.txt:

 add_custom_target(git_revision.h git log -1 "--format=format:#define GIT_REVISION \"%H\"%n" HEAD > git_revision.h WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} VERBATIM) 

Presume che CMAKE_SOURCE_DIR sia parte di un repository git e che git sia disponibile sul sistema e che un reindirizzamento dell’output venga analizzato correttamente dalla shell.

È quindi ansible rendere questo objective una dipendenza di qualsiasi altro target utilizzando

 add_dependencies(your_program git_revision.h) 

Ogni volta che your_program viene creato, il Makefile (o un altro sistema di build, se funziona su altri sistemi di build) ricreare git_revision.h nella directory di origine, con i contenuti

 #define GIT_REVISION "" 

Quindi puoi #include git_revision.h da qualche file di codice sorgente e usarlo in questo modo. Si noti che l’intestazione viene creata letteralmente in ogni build, cioè anche se tutti gli altri file object sono aggiornati, eseguirà comunque questo comando per ricreare git_revision.h. Immagino che non dovrebbe essere un problema enorme perché di solito non si ricostruisce la stessa revisione di git più e più volte, ma è qualcosa di cui essere consapevole, e se è un problema per te, allora non usare questo. (Probabilmente è ansible risolvere una soluzione alternativa usando add_custom_command ma non ne ho avuto bisogno fino ad ora.)