Tomás
Un requisito bastante común, creo: quiero myapp --version
para mostrar la versión y el hash de confirmación de Git (incluido si el repositorio estaba sucio). La aplicación se está construyendo a través de un Makefile
(realmente generado por qmake
, pero hagámoslo “simple” por ahora). Estoy bastante versado en Makefiles, pero este me tiene perplejo.
Puedo obtener fácilmente el resultado deseado como este:
$ git describe --always --dirty --match 'NOT A TAG'
e0e8556-dirty
El código C++ espera que el hash de confirmación esté disponible como una macro de preprocesador llamada GIT_COMMIT
p.ej:
#define GIT_COMMIT "e0e8556-dirty" // in an include file
-DGIT_COMMIT=e0e8556-dirty // on the g++ command line
A continuación se muestran las diferentes formas en que he tratado de sondear el git describe
salida a través de C++. Ninguno de ellos funciona perfectamente.
Enfoque El Primero: el $(shell)
función.
Usamos make’s $(shell)
función para ejecutar el comando de shell y pegar el resultado en una variable make:
GIT_COMMIT := $(shell git describe --always --dirty --match 'NOT A TAG')
main.o: main.cpp
g++ -c -DGIT_COMMIT=$(GIT_COMMIT) -o$@ $<
Esto funciona para una compilación limpia, pero tiene un problema: si cambio el hash de Git (por ejemplo, confirmando o modificando algunos archivos en una copia de trabajo limpia), make no ve estos cambios y el binario no se reconstruye.
Enfoque El Segundo: generar version.h
Aquí, usamos una receta make para generar un version.h
archivo que contiene las definiciones de preprocesador necesarias. El objetivo es falso, por lo que siempre se reconstruye (de lo contrario, siempre se vería actualizado después de la primera compilación).
.PHONY: version.h
version.h:
echo "#define GIT_COMMIT \"$(git describe --always --dirty --match 'NOT A TAG')\"" > $@
main.o: main.cpp version.h
g++ -c -o$@ $<
Esto funciona de manera confiable y no pierde ningún cambio en el hash de confirmación de Git, pero el problema aquí es que siempre se reconstruye version.h
y todo lo que depende de él (incluida una etapa de enlace bastante larga).
Enfoque El Tercero: solo generando version.h
si ha cambiado
La idea: si escribo la salida en version.h.tmp
y luego compare esto con el existente version.h
y solo sobrescriba este último si es diferente, no siempre necesitaríamos reconstruir.
Sin embargo, haga que averigüe lo que necesita reconstruir antes de comenzar a ejecutar cualquier receta. Así que esto tendría que venir antes de esa etapa, es decir, también correr desde un $(shell)
función.
Aquí está mi intento de eso:
$(shell echo "#define GIT_COMMIT \"$$(git describe --always --dirty --match 'NOT A TAG')\"" > version.h.tmp; if diff -q version.h.tmp version.h >/dev/null 2>&1; then rm version.h.tmp; else mv version.h.tmp version.h; fi)
main.o: main.cpp version.h
g++ -c -o$@ $<
Este casi funciona: cada vez que cambia el hash de Git, la primera compilación se regenera version.h
y vuelve a compilar, pero también lo hace la segunda compilación. A partir de entonces, make decide que todo está al día.
Entonces parecería que make decide qué reconstruir incluso antes de ejecutar el $(shell)
función, lo que hace que este enfoque también se rompa.
Esto parece algo tan común y, dado que Make es una herramienta tan flexible, me cuesta creer que no haya forma de hacerlo 100 % correcto. ¿Existe tal enfoque?
tom de geus
Encontré una buena solución. aquí:
En tus CMakeLists.txt
poner:
# Get the current working branch
execute_process(
COMMAND git rev-parse --abbrev-ref HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_BRANCH
OUTPUT_STRIP_TRAILING_WHITESPACE)
# Get the latest commit hash
execute_process(
COMMAND git rev-parse HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_COMMIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE)
y luego definir en tu fuente:
target_compile_definitions(${PROJECT_NAME} PRIVATE
"-DGIT_COMMIT_HASH=\"${GIT_COMMIT_HASH}\"")
En la fuente ahora estará disponible como #define
. Uno podría querer asegurarse de que la fuente todavía compila correctamente por incluido:
#ifndef GIT_COMMIT_HASH
#define GIT_COMMIT_HASH "?"
#endif
Entonces ya está listo para usar, con por ejemplo:
std::string hash = GIT_COMMIT_HASH;
-
Gracias por tu respuesta. Mi pregunta era sobre simple
make
aunque (oqmake
) pero no CMake.– Tomás
16 sep 2020 a las 16:25
-
@Thomas En caso de que alguna vez cambies a CMake;) . Pensé que sería bueno documentarlo para los usuarios de CMake.
–Tom de Geus
16 sep 2020 a las 16:45
-
stackoverflow.com/questions/1435953/…
– Tomás
17 de septiembre de 2020 a las 9:25
-
La cuestión es que este comando no se activará automáticamente si no se realizaron cambios en los archivos relacionados con cmake. Por lo tanto, pronto perderá el rastro del hash de confirmación.
– Hzh
30 de agosto de 2021 a las 16:47
-
Todavía funciona bastante bien para versiones de lanzamiento que se hacen desde cero de todos modos.
– Audrius Meškauskas
2 de febrero a las 10:54
Resulta que mi tercer enfoque estuvo bien después de todo: $(shell)
hace ejecutar antes de hacer averigua qué reconstruir. El problema fue que, durante mis pruebas aisladas, accidentalmente cometí version.h
al repositorio, lo que provocó la doble reconstrucción.
Pero aún hay espacio para mejorar, gracias a @BasileStarynkevitch y @RenaudPacalet: si version.h
se usa desde varios archivos, es mejor almacenar el hash en un version.cpp
en su lugar, por lo que solo necesitamos volver a compilar un archivo pequeño y volver a vincularlo.
Así que aquí está la solución final:
versión.h
#ifndef VERSION_H
#define VERSION_H
extern char const *const GIT_COMMIT;
#endif
Makefile
$(shell echo -e "#include \"version.h\"\n\nchar const *const GIT_COMMIT = \"$$(git describe --always --dirty --match 'NOT A TAG')\";" > version.cpp.tmp; if diff -q version.cpp.tmp version.cpp >/dev/null 2>&1; then rm version.cpp.tmp; else mv version.cpp.tmp version.cpp; fi)
# Normally generated by CMake, qmake, ...
main: main.o version.o
g++ -o$< $?
main.o: main.cpp version.h
g++ -c -o$@ $<
version.o: version.cpp version.h
g++ -c -o$@ $<
¡Gracias a todos por participar con alternativas!
Renaud Pacalet
En primer lugar, podría generar un falso version.h
pero úsalo solo en version.cpp
que define el print_version
función utilizada en cualquier otro lugar. Cada invocación de make mientras nada cambió le costaría solo una compilación ultrarrápida de version.cpp
más el etapa de enlace bastante larga. No hay otras re-compilaciones.
A continuación, probablemente puedas resolver tu problema con un poco de make recursivo:
TARGETS := $(patsubst %.cpp,%.o,$(wildcard *.cpp)) ...
ifeq ($(MODE),)
$(TARGETS): version
$(MAKE) MODE=1 $@
.PHONY: version
version:
VERSION=$$(git describe --always --dirty) && \
printf '#define GIT_COMMIT "%s"\n' "$$VERSION" > version.tmp && \
if [ ! -f version.h ] || ! diff --brief version.tmp version.h &> /dev/null; then \
cp version.tmp version.h; \
fi
else
main.o: main.cpp version.h
g++ -c -o$@ $<
...
endif
El $(MAKE) MODE=1 $@
la invocación hará algo si y sólo si version.h
ha sido modificado por la primera invocación de make (o si el objetivo tuvo que ser reconstruido de todos modos). Y la primera invocación make modificará version.h
si y solo si el hash de confirmación cambió.
-
¡Idea interesante! Un poco aterrador, también. Necesitaría algo de trabajo extra para preservar los argumentos de la línea de comando (por ejemplo, el objetivo que se construirá).
– Tomás
8 de agosto de 2018 a las 7:11
-
@Thomas: sí, hay algo de trabajo extra. Agregar una regla predeterminada de último recurso a la primera parte es tentador, pero también tiene efectos secundarios indeseables. Creo que la mejor opción, si corresponde, es crear una lista de todos sus objetivos y asignarla a una variable antes del
ifeq-endif
. Luego, simplemente agregue una regla para estos objetivos en elifeq-else
. Actualicé mi respuesta para ilustrar esto.– Renaud Pacalet
8 de agosto de 2018 a las 8:15
jthill
Usando .PHONY
directamente significa que se supone que el archivo de destino no existe, lo que no desea para los archivos reales. Para forzar una receta que podría reconstruir un archivo, hacerlo depender de un objetivo falso. Al igual que:
.PHONY: force
version.c: force
printf '"%s"' `git describe --always --dirty` | grep -qsf - version.c \
|| printf >version.c 'const char version[]="%s";\n' `git describe --always --dirty`
(excepto que Markdown no entiende las pestañas, debe corregirlo al pegar)
y el version.c
la receta se ejecutará cada vez, ya que se supone que su dependencia falsa no existe, pero las cosas que dependen de version.c verificarán el archivo real, que solo se actualiza realmente si su contenido no tiene la versión actual.
O podría generar la cadena de versión en version.h
al igual que con la configuración “Acercarse al segundo” en su pregunta, lo importante es no decir make
los archivos reales son falsos.
¿Por qué no tener version.h
depende de tu .git/index
¿archivo? Eso se toca cada vez que comprometes o cambias algo en tu área de preparación (lo que no sucede a menudo, por lo general).
version.h: .git/index
echo "#define GIT_COMMIT \"$(git describe --always --dirty)\"" > $@
Si planea construir sin Git en algún momento, tendrá que cambiar esto, por supuesto…
-
Esto no detectará una transición de no sucio a sucio.
– jthill
8 de agosto de 2018 a las 2:56
-
Supongo que podrías agregar
$(shell git ls-files --others)
a la lista de dependencias para resolver esto. Pero creo que la simplicidad de esta solución vale la pena.– Botje
8 de agosto de 2018 a las 5:40
Sugiero generar un pequeño archivo C autosuficiente version.c
definir algunas variables globales y garantizar que se regenere en cada enlace exitoso de myapp
ejecutable.
Así que en tu archivo MAKE
version.c:
echo "const char version_git_commit[]=" > $@
echo " \"$(git describe --always --dirty)\";" >> $@
Luego tenga un encabezado de C++ que lo declare:
extern "C" const char version_git_commit[];
Por cierto, mira en mi bismón repositorio (compromiso c032c37be992a29a1e), es Makefile
archivo de destino __timestamp.c
por inspiración. Tenga en cuenta que para el ejecutable binario bismonio objetivo, make
es quitando __timestamp.c
después de cada enlace exitoso.
Podrías mejorar tu Makefile
para eliminar version.c
y version.o
después de cada enlace ejecutable exitoso (por ejemplo, después de algunos $(LINK.cc)
linea para tu myapp
ejecutable). Por lo tanto, tendrías en tu makefile:
myapp: #list of dependencies, with version.o ....
$(LINK.cc) .... version.o ... -o $@
$(RM) version.o version.c
Entonces podrías tener solo tu version.c
y version.o
reconstruido cada vez, y eso es muy rápido.
-
Esto no detectará una transición de no sucio a sucio.
– jthill
8 de agosto de 2018 a las 2:56
-
Supongo que podrías agregar
$(shell git ls-files --others)
a la lista de dependencias para resolver esto. Pero creo que la simplicidad de esta solución vale la pena.– Botje
8 de agosto de 2018 a las 5:40
mdemirst
Puedes conseguirlo llamando git rev-parse --short HEAD
comando directamente desde su ejecutable
Aquí esta lo que hice:
en CMakeLists.txt
add_definitions("-DPROJECT_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}\"")
y en su archivo fuente:
#include <array>
#include <cstdio>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
inline std::string execCommand(const char* cmd) {
std::array<char, 128> buffer;
std::string result;
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd, "r"), pclose);
if (!pipe) {
throw std::runtime_error("popen() failed!");
}
while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
result += buffer.data();
}
return result;
}
int main()
{
std::string git_command = "cd ";
git_command += PROJECT_DIR; // get your project directory from cmake variable
git_command += " && git rev-parse --short HEAD"; // get the git commit id in your project directory
std::cout << "Git commit id is :" << execCommand(git_command.c_str()) << std::endl;
return 0;
}
-
Gracias, pero es necesario en tiempo de compilación, no en tiempo de ejecución.
--version
normalmente no se invoca desde el repositorio de origen.– Tomás
13 oct 2019 a las 15:12
Es algo común, pero la gente resuelve el problema al no preocuparse por la recompilación innecesaria. 🙂 stackoverflow.com/a/44038455/7976758
– Doctor
7 de agosto de 2018 a las 13:21
Alternativamente, podrías usar ganchos de git
– marco a.
7 de agosto de 2018 a las 13:24
@MarcoA. Interesante, no lo había considerado. Pero los git hooks no pueden detectar si una copia de trabajo pasa de un estado limpio a uno sucio, ¿verdad?
– Tomás
7 de agosto de 2018 a las 13:26
Realmente no puedo reproducir su enfoque 3. con ese Makefile, main.o se construye solo una vez después de un cambio en el hash de git, la segunda vez que invoco make, no lo vuelve a construir. Otro enfoque similar es una combinación de su enfoque 2 y 3, de modo que indique version.h como objetivo en lugar de usar $(shell.. ) pero no cambie el archivo si no se cambia el hash de git.
– nos
7 de agosto de 2018 a las 13:52
@nos Creo que cometí accidentalmente
version.h
en mi prueba anterior, lo que provocaría una reconstrucción doble después de confirmar todo: una vez porque el hash de confirmación cambió, pero luego una vez más porque la actualizaciónversion.h
pasó de limpio a sucio. ¡Gracias por señalarlo! Significa que el tercer enfoque, aunque peludo, hace el trabajo.– Tomás
8 de agosto de 2018 a las 7:18