web-dev-qa-db-fra.com

Comment puis-je passer git SHA1 au compilateur en tant que définition en utilisant cmake?

Dans un Makefile, cela se ferait avec quelque chose comme:

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

Ceci est très utile, car le binaire connaît la validation exacte SHA1, ainsi il peut le vider en cas de segfault.

Comment puis-je obtenir le même résultat avec CMake?

40
Łukasz Lew

J'ai créé des modules CMake qui s'apparentent à un référentiel git pour la gestion des versions et des applications similaires. Ils sont tous dans mon référentiel à l'adresse suivante: https://github.com/rpavlik/cmake-modules

La bonne chose à propos de ces fonctions est qu’elles forceront une reconfiguration (une réexécution de cmake) avant une construction à chaque fois que la HEAD commit modifie. Contrairement à faire quelque chose une seule fois avec execute_process, vous n'avez pas besoin de vous rappeler de recommencer pour mettre à jour la définition de hachage.

Pour ce but spécifique, vous aurez besoin au moins des fichiers GetGitRevisionDescription.cmake et GetGitRevisionDescription.cmake.in. Ensuite, dans votre fichier CMakeLists.txt principal, vous auriez quelque chose comme ceci

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

Ensuite, vous pouvez soit l'ajouter en tant que définition à l'échelle du système (ce qui provoquerait malheureusement beaucoup de reconstruction)

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

ou, mon alternative suggérée: Créer un fichier source généré. Créez ces deux fichiers dans votre source:

GitSHA1.cpp.in:

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

GitSHA1.h:

extern const char g_GIT_SHA1[];

Ajoutez ceci à votre CMakeLists.txt (en supposant que vous ayez une liste de fichiers source dans 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)

Ensuite, vous avez une variable globale contenant votre chaîne SHA - l'en-tête avec l'extern ne change pas lorsque le SHA le fait, vous pouvez donc simplement l'inclure à tout endroit que vous souhaitez référencer. à la chaîne, puis seul le CPP généré doit être recompilé à chaque commit pour vous donner accès au SHA partout.

88
Ryan Pavlik

Je l'ai fait de manière à générer:

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";

Si l'espace de travail qui a généré la construction comportait des modifications en attente et non validées, la chaîne SHA1 ci-dessus sera suffixée de -dirty.

Dans 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)

Cela nécessite version.cc.in:

#include "version.hh"

using namespace my_app;

const std::string Version::GIT_SHA1 = "@GIT_SHA1@";
const std::string Version::GIT_DATE = "@GIT_DATE@";
const std::string Version::GIT_COMMIT_SUBJECT = "@GIT_COMMIT_SUBJECT@";

Et version.hh:

#pragma once

#include <string>

namespace my_app
{
  struct Version
  {
    static const std::string GIT_SHA1;
    static const std::string GIT_DATE;
    static const std::string GIT_COMMIT_SUBJECT;
  };
}

Alors dans le code je peux écrire:

cout << "Build SHA1: " << Version::GIT_SHA1 << endl;
14
Drew Noakes

J'utiliserais qc. comme ceci dans mon 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}" )
9
the Ritz

Ce serait bien d'avoir une solution qui capte les modifications dans le référentiel (depuis git describe --dirty), mais ne déclenche la recompilation que si quelque chose à propos des informations git a changé.

Certaines des solutions existantes:

  1. Utilisez 'execute_process'. Cela ne récupère que les informations git au moment de la configuration et peut manquer des modifications dans le référentiel.
  2. Dépendre de .git/logs/HEAD. Cela ne déclenche la recompilation que lorsque quelque chose dans le référentiel change, mais manque les modifications pour obtenir l'état '-dirty'.
  3. Utilisez une commande personnalisée pour reconstruire les informations de version chaque fois qu'une génération est exécutée. Cela intercepte les modifications entraînant l'état -dirty, mais déclenche une recompilation tout le temps (en fonction de l'horodatage mis à jour du fichier d'informations sur la version).

Une solution à la troisième solution consiste à utiliser la commande CMake 'copy_if_different', de sorte que l'horodatage sur le fichier d'informations sur la version ne change que si le contenu change.

Les étapes de la commande personnalisée sont les suivantes:

  1. Recueillir les informations git dans un fichier temporaire
  2. Utilisez 'copy_if_different' pour copier le fichier temporaire dans le fichier réel
  3. Supprimez le fichier temporaire pour que la commande personnalisée soit réexécutée à la prochaine étape.

Le code (empruntant lourdement à la solution de 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})
6
Mark Dewing

La solution suivante est basée sur l'observation que Git met à jour le journal HEAD à chaque fois que vous pull ou commit quelque chose. Notez que par exemple La suggestion de Drew ci-dessus mettra à jour les informations Git uniquement si vous reconstruisez manuellement le cache CMake après chaque commit.

J'utilise une "commande personnalisée" CMake qui génère un fichier d'en-tête d'une ligne ${SRCDIR}/gitrevision.hh, où ${SRCDIR} est la racine de votre arborescence source. Il sera refait seulement quand un nouveau commit sera fait. Voici la magie nécessaire CMake avec quelques commentaires:

# 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
    # e.g. 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()

Le contenu de gitrevision.hh ressemblera à ceci:

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

Si vous voulez changer cela, éditez la spécification --pretty=format: en conséquence. Par exemple. utiliser %H au lieu de %h imprimera l'intégralité du résumé SHA1. Voir le manuel Git pour plus de détails.

Faire de gitrevision.hh un fichier d’en-tête C++ à part entière avec include guards, etc. est laissé au lecteur comme un exercice :-)

5
Laryx Decidua

Je ne peux pas vous aider du côté de CMake, mais en ce qui concerne Côté Git , je vous conseillerais de regarder comment le noyau Linux et le projet Git le font, via GIT-VERSION-GEN script , ou comment tig le fait dans son Makefile , en utilisantgit describesi un dépôt git est présent, retombant sur "version"/"VERSION"/"GIT-VERSION-FILE" généré et présent dans les archives, retomber enfin à la valeur par défaut codée en dur dans le script (ou le Makefile).

La première partie (à l'aide de git describe) requiert que vous tagguiez les versions à l'aide de balises annotées (et éventuellement signées par GPG). Ou utilisez git describe --tags pour utiliser également des balises légères.

3
Jakub Narębski

Voici ma solution qui, à mon avis, est assez courte et efficace ;-)

Tout d'abord, un fichier est nécessaire dans l'arborescence des sources (je l'appelle git-rev.h.in), elle devrait ressembler à ceci:

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

(Ne faites jamais attention à ces macros, c’est un truc un peu fou pour créer une chaîne à partir d’une valeur brute.) Il est essentiel que ce fichier ait exactement une nouvelle ligne vide à la fin pour que cette valeur puisse être ajoutée.

Et maintenant, ce code va dans le fichier CMakeLists.txt respectif:

# --- 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
)

Cette commande indique que le git-rev.h.in est copié dans l'arborescence de construction en tant que git-rev.h et que la révision git est ajoutée à la fin.

Il ne vous reste donc plus qu'à inclure git-rev.h dans l'un de vos fichiers et à faire ce que vous voulez avec la macro GIT_REV, qui génère le hachage de révision git actuel en tant que valeur de chaîne.

La bonne chose à propos de cette solution est que le git-rev.h est recréé chaque fois que vous construisez la cible associée, de sorte que vous n'avez pas à exécuter cmake encore et encore.

Il devrait également être assez portable - aucun outil externe non portable n'a été utilisé et même la stupide stupide fenêtre cmd supporte les opérateurs > et >> ;-)

3
kralyk

Solution

Ajoutez simplement du code à seulement 2 fichiers: CMakeList.txt et 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
}

Explication

Dans CMakeList.txt, CMake commandexecute_process() est utilisé pour appeler la commande git log -1 --format=%h qui vous donne l'abréviation courte et unique de vos valeurs SHA-1 dans une chaîne telle que 4f34ee8. Cette chaîne est assignée à la variable CMake appelée GIT_COMMIT_HASH. La commande CMake add_definitions() définit la macro GIT_COMMIT_HASH sur la valeur de 4f34ee8 juste avant la compilation gcc. La valeur de hachage est utilisée pour remplacer la macro dans le code C++ par le préprocesseur; elle existe donc dans le fichier objet main.o et dans les fichiers binaires compilés a.out.

Note latérale

Une autre méthode consiste à utiliser la commande CMake appelée configure_file(), mais je n'aime pas l'utiliser parce que le fichier n'existe pas avant l'exécution de CMake.

1
etoricky

Si CMake n’a pas la capacité intégrée d’effectuer cette substitution, vous pouvez écrire un script shell wrapper qui lit un fichier modèle, substitue le hachage SHA1 comme indiqué ci-dessus à l’emplacement correct (à l’aide de sed, par exemple), crée le fichier de construction CMake réel, puis appelle CMake pour générer votre projet.

Une approche légèrement différente pourrait consister à rendre la substitution SHA1 optionnel . Vous créeriez le fichier CMake avec une valeur de hachage factice telle que "NO_OFFICIAL_SHA1_HASH". Lorsque les développeurs construisent leurs propres versions à partir de leurs répertoires de travail, le code généré n'inclut pas de valeur de hachage SHA1 (uniquement la valeur fictive) car le code du répertoire de travail n'a même pas encore de valeur de hachage SHA1 correspondante.

D'autre part, quand une compilation officielle est faite par votre serveur de build, à partir de sources extraites d'un référentiel central, vous connaissez la valeur de hachage SHA1 du code source. À ce stade, vous pouvez remplacer la valeur de hachage dans le fichier CMake, puis lancer CMake.

1
Greg Hewgill

Pour un moyen rapide et sale, peut-être pas portable, de faire entrer git SHA-1 dans un projet C ou C++ en utilisant CMake, je l’utilise dans 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)

Cela suppose que CMAKE_SOURCE_DIR fait partie d'un référentiel git, et que git est disponible sur le système, et qu'une redirection de sortie sera correctement analysée par le shell.

Vous pouvez ensuite transformer cette cible en dépendance de toute autre cible à l'aide de

add_dependencies(your_program git_revision.h)

Chaque fois que your_program est construit, le Makefile (ou un autre système de construction, si cela fonctionne sur d’autres systèmes de construction) va recréer git_revision.h dans le répertoire source, avec le contenu.

#define GIT_REVISION "<SHA-1 of the current git revision>"

Vous pouvez donc utiliser #include git_revision.h à partir d’un fichier de code source et l’utiliser de cette manière. Notez que l’en-tête est créé littéralement every build, c’est-à-dire que même si tous les autres fichiers objet sont à jour, la commande sera toujours exécutée pour recréer git_revision.h. Je suppose que cela ne devrait pas être un gros problème car en général, vous ne reconstruisez pas la même révision git encore et encore, mais il faut en être conscient, et si est un problème pour vous, alors n'utilise pas ça. (Il est probablement possible de modifier une solution de contournement à l'aide de add_custom_command mais je n'en ai pas eu besoin jusqu'à présent.)

0
David Z