Repository: mkdocs/mkdocs

Issue:
Please support SOURCE_DATE_EPOCH specification for build time stamps
Copied from http://bugs.debian.org/824266:

> Hi,
> 
> mkdocs integrates build time stamps into the documentation it is
> generating. This makes at least unburden-home-dir no more reproducibly
> building because it now uses mkdocs to generate HTML documentation at
> build time.
> 
> See https://tests.reproducible-builds.org/rb-pkg/unstable/amd64/unburden-home-dir.html and
> https://tests.reproducible-builds.org/dbd/unstable/amd64/unburden-home-dir_0.4.diffoscope.html
> 
> Other cases of this issue are the source package djangorestframework (see
> https://tests.reproducible-builds.org/rb-pkg/testing/i386/djangorestframework.html and
> https://tests.reproducible-builds.org/dbd/testing/i386/djangorestframework_3.3.2-2.diffoscope.html)
> and of course the source package python-mkdocs itself, see
> https://tests.reproducible-builds.org/rb-pkg/unstable/amd64/python-mkdocs.html and
> https://tests.reproducible-builds.org/dbd/unstable/amd64/python-mkdocs_0.15.3-3.diffoscope.html
> 
> At https://reproducible-builds.org/specs/source-date-epoch/ you can find
> a bound to be widely adopted specification for how to solve such kind of
> issues generally: If the environment variable SOURCE_DATE_EPOCH exists
> use the Unix time stamp (seconds since 1st of January 1970, 00:00:00
> UTC) it contains to preseed all build time stamps with that time stamp
> instead of the current time.
> 
> See https://wiki.debian.org/ReproducibleBuilds/TimestampsProposal#Examples
> for some examples on how to implement support for SOURCE_DATE_EPOCH,
> including an example for Python.



Discussion:
Yup, this change seems reasonable. Thanks for the report.

> See https://wiki.debian.org/ReproducibleBuilds/TimestampsProposal#Examples
> for some examples on how to implement support for SOURCE_DATE_EPOCH,
> including an example for Python.

FYI, the Python example is wrong. If `SOURCE_DATE_EPOCH` is supposed to be a Unix timestamp (number of seconds since epoch), then `time.gmtime()` is not the Python equivalent. However, `calendar.timegm(datetime.datetime.utcnow().utctimetuple())` or `calendar.timegm(time.gmtime())` is. It is annoyingly complicated to get a Unix timestamp in Python. Although Python >= 3.3 makes it easier with `datetime.datetime.utcnow().timestamp()`. Not sure how the `datetime` module went so long without the ability to return a timestamp.

> FYI, the Python example is wrong. If SOURCE_DATE_EPOCH is supposed to be a Unix timestamp (number of seconds since epoch), then `time.gmtime()` is not the Python equivalent. However, `calendar.timegm(datetime.datetime.utcnow().utctimetuple())` or `calendar.timegm(time.gmtime())` is. It is annoyingly complicated to get a Unix timestamp in Python. Although Python >= 3.3 makes it easier with `datetime.datetime.utcnow().timestamp()`. Not sure how the datetime module went so long without the ability to return a timestamp.

meh, yeah `time.gmtime()` returns a `time.struct_time` instance.
umh, and what about `time.time()`?  From https://docs.python.org/3.5/library/time.html :

> Return the time in seconds since the epoch as a floating point number. Note that even though the time is always returned as a floating point number, not all systems provide time with a better precision than 1 second. While this function normally returns non-decreasing values, it can return a lower value than a previous call if the system clock has been set back between the two calls.

should be exactly what is needed (maybe wrapped in `int()`)?

Thanks for catching that. Yes we did originally have `time.time()` which is simplest but somehow apparently I managed to fuck it up [here](https://wiki.debian.org/ReproducibleBuilds/TimestampsProposal?action=diff&rev2=66&rev1=65) possibly by hitting ctrl-z or something at the wrong time. I don't remember adding the `gm` but I do remember doing the rest of that edit.

Hello,

I have been told this this change by itself is not sufficient for reproducible builds.

See https://bugs.debian.org/831648

Looks like a similar change is required for mkdocs/nav.py,in particular the `update_time` variable.

```
--- python-mkdocs-0.15.3.orig/mkdocs/nav.py
+++ python-mkdocs-0.15.3/mkdocs/nav.py
@@ -137,7 +137,11 @@ class Page(object):
         self.abs_url = url
         self.active = False
         self.url_context = url_context
-        self.update_date = datetime.datetime.now().strftime("%Y-%m-%d")
+
+        try:
+            self.update_date = datetime.datetime.utcfromtimestamp(int(os.environ['SOURCE_DATE_EPOCH']))
+        except KeyError:
+            self.update_date = datetime.datetime.now().strftime("%Y-%m-%d")

         # Relative paths to the input markdown file and output html file.
         self.input_path = path
```

Ah, the time stamp for each individual page needs to honor the env variable also. 


Resolving patch:
diff --git a/mkdocs/nav.py b/mkdocs/nav.py
--- a/mkdocs/nav.py
+++ b/mkdocs/nav.py
@@ -137,7 +137,15 @@ def __init__(self, title, url, path, url_context):
         self.abs_url = url
         self.active = False
         self.url_context = url_context
-        self.update_date = datetime.datetime.now().strftime("%Y-%m-%d")
+
+        # Support SOURCE_DATE_EPOCH environment variable for "reproducible" builds.
+        # See https://reproducible-builds.org/specs/source-date-epoch/
+        if 'SOURCE_DATE_EPOCH' in os.environ:
+            self.update_date = datetime.datetime.utcfromtimestamp(
+                int(os.environ['SOURCE_DATE_EPOCH'])
+            ).strftime("%Y-%m-%d")
+        else:
+            self.update_date = datetime.datetime.now().strftime("%Y-%m-%d")
 
         # Relative paths to the input markdown file and output html file.
         self.input_path = path


Test patch: