2

I'm trying to integrate the output of git describe in my binaries:

// version.c.in
#include <stdio.h>

const char version[] __attribute__((used)) = "##BUILDVERSION##:@BUILDVERSION@";

int main() {
    printf("%s\n", version);
    return 0;
}

Make generates a version.c out of it:

// Makefile
SOURCEFILES = $(wildcard *.c) version.c
PROGRAMS = $(addprefix build/,$(SOURCEFILES:.c=.out))

.PHONY: version.c

all: $(PROGRAMS)

clean:
    rm -fR build

build/%.out: build/%.o Makefile
    mkdir -p build
    gcc -g $< -o $@

build/%.o: %.c Makefile
    mkdir -p build
    gcc -Wall -Werror -g -c $< -o $@

version.c: version.c.in Makefile
    cp $< $@_temp
    sed -i "s/@BUILDVERSION@/$$(git describe --always --dirty=-dirty)/g" $@_temp
    @cmp -s $@_temp $@ || mv $@_temp $@
    rm -f $@_temp

I would like to achieve to trigger a rebuild only if the output of git describe changes. With my current approach the linker step is always called, although version.c does not even change. But it is .PHONY, which probably causes the rebuild of all targets depending on it. If I make the target not .PHONY anymore I'm missing a rebuild for instance if I commit or add a tag.

I also could not find a suitable file in .git/ as a possible dependency of version.c.

Is there a way to solve this?

  • 1
    Does this answer your question? [How to set git version in .h file before build?](https://stackoverflow.com/questions/69194603/how-to-set-git-version-in-h-file-before-build) – HardcoreHenry Sep 21 '21 at 14:38

3 Answers3

1

Edit: see also HarcoreHenry's answer to How to set git version in .h file before build? That's my new favorite solution. I'll keep this one here for the record.


But it is .PHONY, which probably causes the rebuild of all targets depending on it.

That's correct: a .PHONY target is always rebuilt, which means anything that depends on it is also always usually rebuilt (see jthill's comment).

Because of make's file-and-time-stamp orientation, what you need is a command that updates the timestamp of some file if and only if the contents of the file will change. You can then have output O depend on input file I, whose time-stamp depends upon the git describe output.

Here is a complete example:

version=$(shell git describe --always --dirty=-dirty)
$(shell echo ${version} > version.tmp && \
  { cmp -s version.tmp version.txt || cp version.tmp version.txt; } && \
  rm -f version.tmp)

all: version.o

version.c: version.c.in version.txt
    sed "s:@BUILDVERSION@:${version}:" < $@.in > $@

version.o: version.c

This should work even in older versions of gmake; newer ones have $(file) functions that can be used to do most of the work. That is, you would read the contents of version.txt with $(file) and then only if those don't match the contents of the git describe output, write to version.txt, all done during the reading of the Makefile (as is the case here: note how the $(shell) invocations happen early on).

(Edit to add note: all should indeed be marked .PHONY here, as Andrey Starodubtsev mentions in his answer. I obviously didn't bother. Whether to segregate all output into a build/ is up to you.)

torek
  • 448,244
  • 59
  • 642
  • 775
  • Thanks, this approach does the trick. – Benedikt Schmidt Sep 21 '21 at 12:54
  • 1
    You can do this with 100% standard make, without any GNU make tricks, by simply creating a rule that builds `version.txt` which only updates that file if its contents have changed, then using the `FORCE` rule trick (see the GNU make manual) to ensure it's always built. – MadScientist Sep 21 '21 at 13:33
  • 1
    Also see [here](https://stackoverflow.com/questions/69194603/how-to-set-git-version-in-h-file-before-build/69195498#69195498), which generates a filename based on the git reversion (and also has the advantage of not requiring the `$(shell)` command) – HardcoreHenry Sep 21 '21 at 14:43
  • Hindbrain finally figured out what was bugging me: "a `.PHONY` target is always rebuilt, which means anything that depends on it is also always rebuilt." is not correct. If the `.PHONY` target is considered, its recipe is always run and its result is always considered to be new, *but this is not true of dependencies*. A dependency's recipe is always run, but its recipe can decide not to rebuild it, and if it's not itself phony make will actually check the target date after running the recipe; if it hasn't actually been rebuilt the rebuild won't cascade. That difference makes my answer work. – jthill Sep 30 '21 at 06:01
  • @jthill: good point, text fixed. – torek Sep 30 '21 at 06:37
1
.PHONY: force
version.c: force
        version=`git describe --always --dirty || echo unknown:non-git`; \
        grep -qsF \"$$version\" version.c \
        || printf >version.c 'const char version[]="%s";\n' $$version

and the version.c recipe will run for any build that depends on the version string, with version.o rebuilt only if the version number actually changed.

Then put extern const char version[]; in a header and you get absolutely minimal rebuild propagation.

jthill
  • 55,082
  • 5
  • 77
  • 137
  • I like this one too; it's short and not gnumake-specific. But I would use `&&` after setting `version` (and use `$(...)` rather than backquotes, as a personal preference thing). – torek Sep 22 '21 at 02:55
  • @torek good point about the condition checking, rather than fail the build for e.g. `.zip` builds I made it use ~unknown~ instead of an empty version string. – jthill Sep 22 '21 at 03:20
0
  1. You store object files in build directory, so generated version.c should probably go there as well.

  2. You can store current commit hash into a temporary file in build directory and make build/version.c depend on it.

  3. Targets all and clean must be marked as .PHONY.

  4. Comments in Makefile start from #.

  5. build/version.c is rebuilt every time cause build/version.o is removed. To avoid this removal one can use solutions from Makefile removes object files for no reason

So, in general possible solution can be something like this:

# Makefile
SOURCEFILES = $(wildcard *.c.in)
OBJS = $(addprefix build/,$(SOURCEFILES:.c.in=.o))
PROGRAMS = $(addprefix build/,$(SOURCEFILES:.c.in=.out))

.PHONY: all
all: $(PROGRAMS)

.PHONY: clean
clean:
    rm -fR build

build/%.out: build/%.o Makefile
    mkdir -p build
    gcc -g $< -o $@

build/%.o: %.c build/%.c Makefile
    mkdir -p build
    gcc -Wall -Werror -g -c $< -o $@

# .SECONDARY: build/version.o
.PRECIOUS: build/version.o

build/current_hash:
    mkdir -p build
    git log --pretty=%h > $@.tmp
    @cmp -s $@.tmp $@ || mv $@.tmp $@

build/version.c: version.c.in Makefile build/current_hash
    cp $< $@_temp
    sed -i "s/@BUILDVERSION@/$$(git describe --always --dirty=-dirty)/g" $@_temp
    @cmp -s $@_temp $@ || mv $@_temp $@
    rm -f $@_temp
Andrey Starodubtsev
  • 5,139
  • 3
  • 32
  • 46