Getting a UNIX epoch timestamp for a reproducible build’s
SOURCE_DATE_EPOCH environment variable for a file from the last git commit inside a Makefile.
define source_epoch SOURCE_DATE_EPOCH=$(shell (git log -1 --pretty=%ct -- $(1); date +%s) | head -1) endef %.out: %.in $(call source_epoch, $<) command $< $@
This post is about getting the “Last updated: -----” timestamp on a rendered page with Asciidoctor to be the latest Git commit that touched the file.
My solution before this was almost good, except it would fail when the file hadn’t had a commit yet. This is annoying when writing the first version of a file, and I frequently forgot about this edge case until finally deciding to fix it.
I have been using Asciidoctor for processing AsciiDoc-formatted documents for a few years now. It splits the difference for me between the ubiquitous but forrest of variations that is Markdown and more rigorous formats like reStructuredText. I wanted the ability to export to HTML with no extra work to look good on a mobile device, and to render to PDF on occasion.
Whatever the tool, it must have solid support for LaTeX math (AMSmath), which has always been in my experience not as deep in Markdown tools as the layout looks like in my head — I frequently need more than a just a choice between inline and displaymath.
Perhaps I’ll (re)learn reStructuredText and see if Sphinx is a better match for more recent wants from my doc writing.
Search around for how to get the UNIX epoch timestamp from
git and you get:
git log -1 --pretty=%ct
Modify ensure that the
SOURCE_DATE_EPOCH environment variable is set when Asciidoctor runs and it will use that time instead of the current time.
This is because the tool makes it relatively easy to do a reproducible build.
Automate this in a Makefile like so:
%.html: %.adoc SOURCE_DATE_EPOCH=$(shell git log -1 --pretty=%ct) asciidoctor ...
Perfect! Now we get a reader-visible indication of the last time the post was changed with no extra work to forget to do.
Do you see the bug?
It took me at least a year to notice that firing off a
make all -B would result in updating all the timestamps.
Re-rendering all of the pages is fine with me, if the particular
.adoc didn’t change, the resulting HTML would be the same also with the properly set
I didn’t notice because I was mostly interested in the most recent file I’ve been editing and checking in a browser to ensure it renders as intended.
The bug is that the
git log -1 --pretty=%ct returns the latest git commit date of the repository and not the last commit date of the particular file.
Figuring out the nature of a problem is a long way down the road to fixing the issue. So, give the Git command the name of the current file that Make is considering at that moment.
SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct %.adoc)
But that last
% doesn’t get replaced properly.
Actually, I’ve forgotten exactly what the problem with this was, but it had to do with how Make interprets a Makefile.
The solution is to use a Makefile macro, which was a new thing for me.
Call the macro and give it a positional argument list. Pass that argument to the git command and done!
# Make macro/function define source_epoch SOURCE_DATE_EPOCH=$(shell git log -1 --pretty=%ct -- $(1)) endef # ... $(OUT)/%.html: $(SRC/%.adoc) $(call source_epoch, $<) \ $(ASCIIDOCTOR) \ $(ASCIIDOCTOR_OPTS) \ --out-file=$@ \ $<
--in the git command stops option processing and ensures the following argument is always interpreted as a file name. This was a gotcha that I’ve been gotcha’d before, so just good practice now.
$(call source_epoch, $<)uses the
callfunction to give the first prerequisite (
$<) as the first argument to the
source_epochmacro, and leaves the result in its place.
Problem solved, plus learned more about functions in (GNU) Makefiles!
but sometimes Asciidoctor would fail with a message like:
asciidoctor: FAILED: /home/dan/agnd-blog/src/adoc-last-updated/foo.adoc: Failed to load AsciiDoc document - invalid value for Integer(): "" Use --trace to show backtrace make: *** [Makefile:87: build/adoc-last-updated/foo.html] Error 1
Let. me. tell. you. Pretty sure I know about integers, that I wasn’t using any, and that this error message was not helpful.
Not even adding the
--trace option was useful.
A few experiments to get the offending
.adoc to render and I would eventually remember that the error came from trying to use the value of the environment variable
SOURCE_DATE_EPOCH but was set to an empty value.
This because the
git log would output nothing, BUT STILL RETURN a code of 0, which usually means “the program ran successfully.”
In some way, the program was successful, but I rely on a Make job failing whenever something internal returns nonzero to block continuing in the rendering or whatever else chain of commands; it’s kind of a feature.
So, now (re)recalling that the issue is with `git log’s behavior: add the file and make a commit, and everything works fine again.
Until it happens the next time and I repeat nearly the same troubleshooting again.
Time to fix this for good.
What we know so far:
This only happens because the
.adocfile in question hasn’t been introduced to the git repository.
I don’t want to need to remember to make a commit before rendering.
Sometimes I don’t even intend to commit this file, but still want to render it.
git log -1 — file.adocwill have an empty response if not in the repository.
If there is no commit for the file, then revert back to the behavior where Asciidoctor uses the current datetime.
SOURCE_DATE_EPOCHdisables that default behavior.
I’ve forgotten the five minutes of experimentation that yielded what’s below, so no more story.
# Make macro/function define source_epoch SOURCE_DATE_EPOCH=$(shell (git log -1 --pretty=%ct -- $(1); date +%s) | head -1) endef # ... $(OUT)/%.html: $(SRC/%.adoc) $(call source_epoch, $<) \ $(ASCIIDOCTOR) \ $(ASCIIDOCTOR_OPTS) \ --out-file=$@ \ $<
Run two commands in a subshell.
Case 1: file has a commit
git logoutputs the desired timestamp
date +%soutputs the current timestamp
Piping the ordered concatenation to
head -leaves only the first
Case 2: no commit
git logoutputs nothing
date +%soutputs the current UNIX time
head -1passes the first (and only) line
Perhaps there is another edge case here, but it sure feels like the final solution.
I’m not rendering a bajillion source files, so the “inefficiency” of three commands, an extra subshell, and a pipe is negligible — my time is worth more than that saved in this application.