SOURCE_DATE_EPOCH
SOURCE_DATE_EPOCH
は 標準化された環境変数 の1つです。
ディストリビューションとして中央集権的に設定できるし、ビルドツールは再現性のあるビルド結果を生成するために使用できます。
自分たちで実装できるか判断するため、Debian としてのチェックリスト を確認してください。
自分たちのユースケースにとって理想的であることが分かっているなら、次は 公開された仕様 に読み進めてください。
提案内容
詳しくは 公開された仕様 を参照してください。
より詳細な理論や仕組みについては 標準化された環境変数 を参照してください。
ここでは、SOURCE_DATE_EPOCH
という環境変数に関する過去の経緯や、代替案について説明します。
環境変数を設定する
Debian
Debian では、debian/changelog
の最新項目と同じ日時を自動的に設定します。
つまり dpkg-parsechangelog -SDate
と同じ日時になります。
- バージョン 9.20151004 (Bug:791823) 以上の debhelper を使用するパッケージでは、ビルド時に環境変数を export するため、何もする必要がありません。
ただし、debhelper を未使用のパッケージについて
debian/rules
で環境変数する場合は、(3) か (4) の方法を試してください。
- バージョン 0.4.131 (Bug:794241) 以上の CDBS を使用するパッケージでは、ビルド時に環境変数を export するため、何もする必要がありません。
- バージョン 1.18.8 (Bug:824572) 以上の dpkg では
include /usr/share/dpkg/pkg-info.mk
かinclude /usr/share/dpkg/default.mk
を使用できます。
- どの方法でも上手くいかない場合(そしてそれをするだけの ‘‘確固とした理由がある’’ 場合)、
debian/rules
にパッケージメンテナとして環境変数を設定してください。
export SOURCE_DATE_EPOCH ?= $(shell dpkg-parsechangelog -STimestamp)
バージョン 1.18.8 より前の dpkg を使わなければならないときは次のように記述します。
export SOURCE_DATE_EPOCH ?= $(shell dpkg-parsechangelog -SDate | date -f- +%s)
バージョン 1.17.0 より前の dpkg を使わなければならないときは次のように記述します。
export SOURCE_DATE_EPOCH ?= $(shell dpkg-parsechangelog | grep -Po '^Date: \K.*' | date -f- +%s)
これらのスニペットは少なくとも 2003 年以降の dpkg で使用できます。
Git
Git リポジトリの最終更新日時を SOURCE_DATE_EPOCH
に設定するときは git log
を使用します。`
SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
環境変数を読み取る
私たちは(Debian は)それぞれのツールの(ソフトウェアの)開発元に、環境変数 SOURCE_DATE_EPOCH
へ対応してもらえるよう働きかけています。
修正パッチの作成など、あなたにも貢献できることがあるはずです。
そうしたら、未来のパッチ作者の参考になるよう、私たちにバグレポートのリンクを教えてください。
Pending:: gettext, qt4-x11 Complete::
- busybox (>1.33.1)
- cmake (>= 3.8.0)
- debhelper (>= 9.20151004)
- docbook-utils (Debian >= 0.6.14-3.1, upstream TODO)
- doxygen (>= 1.8.12, Debian pending)
- epydoc (>= 3.0.1+dfsg-8, upstream pending)
- gcc (>= 7, Debian >= 5.3.1-17, Debian >= 6.1.1-1)
- ghostscript (Debian >= 9.16~dfsg-1, upstream unfortunately rejected due to additional constraints they have)
- groff (Debian >= 1.22.3-2, upstream pending)
- help2man (>= 1.47.1)
- libxslt (>= 1.1.29, Debian >= 1.1.28-3)
- man2html (Debian >= 1.6g-8, needs forwarding)
- mkdocs (>= 0.16, Debian pending)
- ocamldoc (>= 4.03.0, Debian >= 4.02.3-1)
- pydoctor (>= 0.5+git20151204)
- rpm upstream (>4.13 other relevant patches linked in there)
- sphinx (>= 1.4, Debian >= 1.3.1-3)
- texi2html (Debian >= 1.82+dfsg1-4, needs forwarding)
- texlive-bin (>= 2016.20160512.41045)
- txt2man (>= 1.5.7, Debian >= 1.5.6-4)
- rcc (Qt5 >= 5.13.0, Debian TODO)
Or search in all Debian sources: https://codesearch.debian.net/search?perpkg=1&q=SOURCE_DATE_EPOCH
Python
import os
import time
import datetime
build_date = datetime.datetime.utcfromtimestamp(
int(os.environ.get('SOURCE_DATE_EPOCH', time.time()))
)
or with less imports, rendering to a string:
import os
import time
date_str = time.strftime(
"%Y-%m-%d",
time.gmtime(int(os.environ.get('SOURCE_DATE_EPOCH', time.time())))
)
Bash / POSIX shell
For GNU systems:
BUILD_DATE="$(date --utc --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +%Y-%m-%d)"
If you need to support BSD date as well you should fallback to trying ther
-r seconds
timestamp variant:
DATE_FMT="+%Y-%m-%d"
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(date +%s)}"
BUILD_DATE=$(date -u -d "@$SOURCE_DATE_EPOCH" "$DATE_FMT" 2>/dev/null || date -u -r "$SOURCE_DATE_EPOCH" "$DATE_FMT" 2>/dev/null || date -u "$DATE_FMT")
Perl
use POSIX qw(strftime);
my $date = strftime("%Y-%m-%d", gmtime($ENV{SOURCE_DATE_EPOCH} || time));
Makefile
DATE_FMT = +%Y-%m-%d
ifdef SOURCE_DATE_EPOCH
BUILD_DATE ?= $(shell date -u -d "@$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u -r "$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u "$(DATE_FMT)")
else
BUILD_DATE ?= $(shell date "$(DATE_FMT)")
endif
The above will work with either GNU or BSD date, and fallback to ignore
SOURCE_DATE_EPOCH
if both fails.
CMake
STRING(TIMESTAMP BUILD_DATE "%Y-%m-%d" UTC)
… works with CMake versions 2.8.11 and higher, but it only respects
SOURCE_DATE_EPOCH
since version 3.8.0. If you do not have a modern CMake
but need reproducibility you can use the less-preferred variant:
if (DEFINED ENV{SOURCE_DATE_EPOCH})
execute_process(
COMMAND "date" "-u" "-d" "@$ENV{SOURCE_DATE_EPOCH}" "+%Y-%m-%d"
OUTPUT_VARIABLE BUILD_DATE
OUTPUT_STRIP_TRAILING_WHITESPACE)
else ()
execute_process(
COMMAND "date" "+%Y-%m-%d"
OUTPUT_VARIABLE BUILD_DATE
OUTPUT_STRIP_TRAILING_WHITESPACE)
endif ()
The above will work only with GNU date
. See the POSIX shell example on how
to support BSD date.
Meson
By deliberate design, Meson does not provide access to environment
variables in build
files
which makes accessing SOURCE_DATE_EPOCH
troublesome.
date_exe = find_program('date')
cmd = run_command('sh', '-c', 'echo $SOURCE_DATE_EPOCH')
source_date_epoch = cmd.stdout().strip()
if source_date_epoch == ''
source_date_epoch = run_command(date_exe, '+%s').stdout().strip()
endif
formatted_date = run_command(date_exe, '-u', '-d', '@' + source_date_epoch, '+%Y-%m-%d').stdout().strip()
The above will work only with GNU date
. See the POSIX shell example on how
to support BSD date variants.
C
#include <errno.h>
#include <limits.h>
#include <stdlib.h>
struct tm *build_time;
time_t now;
char *source_date_epoch;
unsigned long long epoch;
char *endptr;
source_date_epoch = getenv("SOURCE_DATE_EPOCH");
if (source_date_epoch) {
errno = 0;
epoch = strtoull(source_date_epoch, &endptr, 10);
if ((errno == ERANGE && (epoch == ULLONG_MAX || epoch == 0))
|| (errno != 0 && epoch == 0)) {
fprintf(stderr, "Environment variable $SOURCE_DATE_EPOCH: strtoull: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
if (endptr == source_date_epoch) {
fprintf(stderr, "Environment variable $SOURCE_DATE_EPOCH: No digits were found: %s\n", endptr);
exit(EXIT_FAILURE);
}
if (*endptr != '\0') {
fprintf(stderr, "Environment variable $SOURCE_DATE_EPOCH: Trailing garbage: %s\n", endptr);
exit(EXIT_FAILURE);
}
if (epoch > ULONG_MAX) {
fprintf(stderr, "Environment variable $SOURCE_DATE_EPOCH: value must be smaller than or equal to %lu but was found to be: %llu \n", ULONG_MAX, epoch);
exit(EXIT_FAILURE);
}
now = epoch;
} else {
now = time(NULL);
}
build_time = gmtime(&now);
If you want less verbose code and are happy with the assumptions stated below, you can use
#include <stdlib.h>
time_t now;
char *source_date_epoch;
/* This assumes that the SOURCE_DATE_EPOCH environment variable will contain
a correct, positive integer in the time_t range */
if ((source_date_epoch = getenv("SOURCE_DATE_EPOCH")) == NULL ||
(now = (time_t)strtoll(source_date_epoch, NULL, 10)) <= 0)
time(&now);
C++
#include <sstream>
#include <iostream>
#include <cstdlib>
#include <ctime>
time_t now;
char *source_date_epoch = std::getenv("SOURCE_DATE_EPOCH");
if (source_date_epoch) {
std::istringstream iss(source_date_epoch);
iss >> now;
if (iss.fail() || !iss.eof()) {
std::cerr << "Error: Cannot parse SOURCE_DATE_EPOCH as integer\n";
exit(27);
}
} else {
now = std::time(NULL);
}
Go
import (
"fmt"
"os"
"strconv"
"time"
)
[...]
source_date_epoch := os.Getenv("SOURCE_DATE_EPOCH")
var build_date string
if source_date_epoch == "" {
build_date = time.Now().UTC().Format(http.TimeFormat)
} else {
sde, err := strconv.ParseInt(source_date_epoch, 10, 64)
if err != nil {
panic(fmt.Sprintf("Invalid SOURCE_DATE_EPOCH: %s", err))
}
build_date = time.Unix(sde, 0).UTC().Format(http.TimeFormat)
}
PHP
\date('Y', (int)\getenv('SOURCE_DATE_EPOCH') ?: \time())
Emacs-Lisp
(current-time-string
(when (getenv "SOURCE_DATE_EPOCH")
(seconds-to-time
(string-to-number
(getenv "SOURCE_DATE_EPOCH"))))))
OCaml
let build_date =
try
float_of_string (Sys.getenv "SOURCE_DATE_EPOCH")
with
Not_found -> Unix.time ()
Java / gradle
def buildDate = System.getenv("SOURCE_DATE_EPOCH") == null ?
new java.util.Date() :
new java.util.Date(1000 * Long.parseLong(System.getenv("SOURCE_DATE_EPOCH")))
Javascript / node.js
var timestamp = new Date(process.env.SOURCE_DATE_EPOCH ? (process.env.SOURCE_DATE_EPOCH * 1000) : new Date().getTime());
// Alternatively, to ensure a fixed timezone:
var now = new Date();
if (process.env.SOURCE_DATE_EPOCH) {
now = new Date((process.env.SOURCE_DATE_EPOCH * 1000) + (now.getTimezoneOffset() * 60000));
}
Ruby
if ENV['SOURCE_DATE_EPOCH'].nil?
now = Time.now
else
now = Time.at(ENV['SOURCE_DATE_EPOCH'].to_i).gmtime
end
Note that Ruby’s Datetime.strftime is locale-independent by default.
Scala
To get milliseconds since Epoch:
sys.env.get("SOURCE_DATE_EPOCH").map(_.toLong * 1000)
To get a java.util.Date
:
sys.env.get("SOURCE_DATE_EPOCH")
.map(sde => new java.util.Date(sde.toLong * 1000))
Last-resort using faketime
'’As a last resort to be avoided where possible’’ (e.g. if the upstream tool is too hard to patch, or too time-consuming for you right now to patch, or if they are being uncooperative or unresponsive), package maintainers may try something like the following:
debian/strip-nondeterminism/a2x
:
#!/bin/sh
# Depends: faketime
# Eventually the upstream tool should support SOURCE_DATE_EPOCH internally.
test -n "$SOURCE_DATE_EPOCH" || { echo >&2 "$0: SOURCE_DATE_EPOCH not set"; exit 255; }
exec env NO_FAKE_STAT=1 faketime -f "$(TZ=UTC date -d "@$SOURCE_DATE_EPOCH" +'%Y-%m-%d %H:%M:%S')" /usr/bin/a2x "$@"
debian/rules
:
export PATH := $(CURDIR)/debian/strip-nondeterminism:$(PATH)
debian/control
:
Build-Depends: faketime
But please be aware that this does not work out of the box with pbuilder on
Debian 7 Wheezy, see #778462 against
faketime and #700591 against pbuilder
(fixed in Jessie, but not Wheezy). Adding an according hook to
/etc/pbuilder/hook.d
which mounts /run/shm
inside the chroot should
suffice as local workaround, though.
TODO: document some other nicer options. Generally, all invocations of
date(1)
need to have a fixed TZ
environment set.
NOTE: faketime BREAKS builds on some archs, for example hurd. See #778462 for details.
More detailed discussion
Sometimes developers of build tools do not want to support
SOURCE_DATE_EPOCH
, or they will tweak the suggestion to something related
but different. We really do think the best approach is to use
SOURCE_DATE_EPOCH
exactly as-is described above in our proposal, without
any variation. Here we explain our reasoning versus the arguments we have
encountered.
(See Standard Environment Variables for general arguments.)
“Lying about the time” / “violates language spec”
This argument arises when the tool processes some input which contains a
static instruction to the effect of “get_current_time()”. The input has a
specification that defines what this means. The tool executes this
instruction, then embeds the result in the output. It is argued that
SOURCE_DATE_EPOCH
would break these semantics and violate the
specification.
In most cases, this argument places too much weight on the absoluteness of
time. Regardless of what any specification says, the user can set their own
system clock and achieve an effect similar to SOURCE_DATE_EPOCH
. Note:
Setting the system clock is not enough for ‘‘reliable’’ reproducible builds
- we need
SOURCE_DATE_EPOCH
for long-running build processes that take varying amounts of time. If the tool was run near the end of the process, then merely setting the system clock would not make timestamps here reproducible. It is not up to the tool to judge whether the user is lying with their system clock, and likewise, it is not up to the tool to judge whetherSOURCE_DATE_EPOCH
is a “lie” or not.
For all intents and purposes, if the user has set SOURCE_DATE_EPOCH
then
they are taking a position that “this is the current time; please use
this instead of whatever clock you normally use”. Yes, the project developer
wrote “get_current_time()” but I as the user, by setting
SOURCE_DATE_EPOCH
, am choosing to override this with my own idea of what
time it is. Please execute the build as if the current time was
SOURCE_DATE_EPOCH
. FOSS software should generally prefer to respect
end-users’ wishes rather than developers’ wishes. (And in practise, we
haven’t seen ‘‘any’’ instance where a project developer really really
prefers “time of build” over “modtime of source”.)
In conclusion, the tool may choose to ignore SOURCE_DATE_EPOCH
for other
reasons, but to judge that this is a ‘‘lie’’ is to disrespect the user’s
wishes. Furthermore, choosing to support this is unlikely to ‘‘actually’’
violate any specifications, since they generally don’t define
“current”. This does not take into account, if the specification needs to
interoperate consistently with other programs in a strong cryptographic
ledger protocol where time values ‘‘must’’ be consistent across multiple
entities. However this scenario is unlikely to apply, in the context of
build tools where SOURCE_DATE_EPOCH
would be set.)
Many tools allow the user to override the “current” date -
e.g. -D__TIME__=xxx
, \\year=yyy
, etc. In these cases, it makes even less
sense to ignore SOURCE_DATE_EPOCH
for data integrity reasons - you
‘‘already’’ have a mechanism where the user can “lie” or “break the spec”;
SOURCE_DATE_EPOCH
would just be adding an extra mechanism that makes it
easier to do this globally across all tools.
If for some reason you’re still conflicted on suddenly changing the meaning
of your “now()” function and desire another switch other than
SOURCE_DATE_EPOCH
being set or not, the texlive
project came up with the
variable FORCE_SOURCE_DATE
; when that environment variable is set to 1
cases that wouldn’t normally obey SOURCE_DATE_EPOCH
will do. We
strongly discourage the usage of such variable; SOURCE_DATE_EPOCH
is
meant to be already a flag forcing a particular timestamp to be used.
OTOH, one alternative we can agree with, that also avoids
SOURCE_DATE_EPOCH
, would be to translate the static instruction
“get_current_time()” from the input format to ‘‘an equivalent instruction’’
in the output format, if the output format supports that.
History and alternative proposals
1 and the surrounding messages describe the initial motivation behind this, including an evaluation of how different programming languages handle date formats.
At present, we do not have a proposal that includes anything resembling a “time zone”. Developing such a standard requires consideration of various issues:
Intuitive and naive ways of handling human-readable dates, such as the POSIX date functions, are highly flawed and freely mix implicit not-well-defined calendars with absolute time. For example, they don’t specify they mean the Gregorian calendar, and/or don’t specify what to do with dates before when the Gregorian calendar was introduced, or use named time zones that require an up-to-date timezone database (e.g. with historical DST definitions) to parse properly.
Since this is meant to be a universal standard that all tools and distributions can support, we need to keep things simple and precise, so that different groups of people cannot accidentally interpret it in different ways. So it is probably unwise to try to standardise anything that resembles a named time zone, since that is very very complex.
One likely candidate would be something similar to the git internal
timestamp format, see man git-commit
:
It is
We already have SOURCE_DATE_EPOCH
so the time zone offset could be placed
in SOURCE_DATE_TZOFFSET
or something like that. But all of this needs
further discussion.
Other non-standard variables that we haven’t yet agreed upon, use at your own risk:
export SOURCE_DATE_TZOFFSET = $(shell dpkg-parsechangelog -SDate | tail -c6)
export SOURCE_DATE_RFC2822 = $(shell dpkg-parsechangelog -SDate)
export SOURCE_DATE_ISO8601 = $(shell python -c 'import time,email.utils,sys;t=email.utils.parsedate_tz(sys.argv[1]);\
print(time.strftime("%Y-%m-%dT%H:%M:%S",t[:-1])+"{0:+03d}{1:02d}".format(t[-1]/3600,t[-1]/60%60));' "$(SOURCE_DATE_RFC2822)")
The ISO8601 code snippet is complex, in order to preserve the same timezone
offset as in the RFC2822 form. If one is OK with stripping out this offset,
i.e. forcing to UTC, then one can just use date -u
instead. However, this
then contains the same information as the unix timestamp, but the latter is
generally easier to work with in nearly all programming languages.
Introduction
Achieve deterministic builds
- SOURCE_DATE_EPOCH
- 確実なビルドシステム(Deterministic build systems)
- 揮発性のある入力データは消える場合がある(Volatile inputs can disappear)
- 入力データの順序を固定する(Stable order for inputs)
- 値を初期化する(Value initialization)
- バージョン情報(Version information)
- タイムスタンプ(Timestamps)
- タイムゾーン(Timezones)
- ロケール(Locales)
- アーカイブのメタデータ(Archive metadata)
- 出力データの順序を固定する(Stable order for outputs)
- 無作為性(Randomness)
- ビルド時のファイルシステムパス(Build path)
- システムイメージ(System images)
- JVM
Define a build environment
- ビルド環境に含む要素(What's in a build environment?)
- ビルド環境を記録する(Recording the build environment)
- ビルド環境の定義における戦略(Definition strategies)
- Proprietary operating systems
Distribute the environment
Verification
Specifications
Follow us on Twitter @ReproBuilds, Mastodon @reproducible_builds@fosstodon.org & Reddit and please consider making a donation. • Content licensed under CC BY-SA 4.0, style licensed under MIT. Templates and styles based on the Tor Styleguide. Logos and trademarks belong to their respective owners. • Patches welcome via our Git repository (instructions) or via our mailing list. • Full contact info