Skip to content

epub: Honor SOURCE_DATE_EPOCH for dcterms:modified and dc:identifier - #2256

Merged
josevalim merged 2 commits into
elixir-lang:mainfrom
bmwiedemann:reproducibleepub
Jul 27, 2026
Merged

epub: Honor SOURCE_DATE_EPOCH for dcterms:modified and dc:identifier#2256
josevalim merged 2 commits into
elixir-lang:mainfrom
bmwiedemann:reproducibleepub

Conversation

@bmwiedemann

Copy link
Copy Markdown
Contributor

The EPUB content.opf embedded the build time as dcterms:modified and a random UUID as dc:identifier, making the .epub files differ between two builds of the same docs.

When SOURCE_DATE_EPOCH is set
(https://reproducible-builds.org/docs/source-date-epoch/), use it as the modification time, and derive the identifier from the project name and version instead of random data. Behavior without SOURCE_DATE_EPOCH is unchanged.

This PR was done while working on reproducible builds for openSUSE. Together with #2255, this makes our erlang-doc package build reproducible.

Comment thread lib/ex_doc/formatter/epub.ex Outdated
Comment on lines +143 to +152
case System.get_env("SOURCE_DATE_EPOCH") do
nil ->
:calendar.universal_time()

epoch ->
case Integer.parse(epoch) do
{seconds, ""} -> :calendar.system_time_to_universal_time(seconds, :second)
_ -> :calendar.universal_time()
end
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
case System.get_env("SOURCE_DATE_EPOCH") do
nil ->
:calendar.universal_time()
epoch ->
case Integer.parse(epoch) do
{seconds, ""} -> :calendar.system_time_to_universal_time(seconds, :second)
_ -> :calendar.universal_time()
end
end
with epoch when is_binary(epoch) <- System.get_env("SOURCE_DATE_EPOCH"),
{seconds, ""} <- Integer.parse(epoch) do
:calendar.system_time_to_universal_time(seconds, :second)
else
_ -> :calendar.universal_time()
end

Comment thread lib/ex_doc/formatter/epub.ex Outdated
Comment on lines +142 to +150
time =
with epoch when is_binary(epoch) <- System.get_env("SOURCE_DATE_EPOCH"),
{seconds, ""} <- Integer.parse(epoch) do
:calendar.system_time_to_universal_time(seconds, :second)
else
_ -> :calendar.universal_time()
end

{{year, month, day}, {hour, min, sec}} = time

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
time =
with epoch when is_binary(epoch) <- System.get_env("SOURCE_DATE_EPOCH"),
{seconds, ""} <- Integer.parse(epoch) do
:calendar.system_time_to_universal_time(seconds, :second)
else
_ -> :calendar.universal_time()
end
{{year, month, day}, {hour, min, sec}} = time
{{year, month, day}, {hour, min, sec}} =
with epoch when is_binary(epoch) <- System.get_env("SOURCE_DATE_EPOCH"),
{seconds, ""} <- Integer.parse(epoch) do
:calendar.system_time_to_universal_time(seconds, :second)
else
_ -> :calendar.universal_time()
end

Comment thread lib/ex_doc/formatter/epub.ex Outdated
Comment on lines +165 to +166
<<a::binary-size(4), b::binary-size(2), c::binary-size(2), d::binary-size(2),
e::binary-size(6)>> = :erlang.md5("#{config.project} #{config.version}")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should either always default to config.project/config.version, but that is likely not randomic enough OR add the source_date_epoch here. Basically, we always build the datatime first, and use it to generate the datetime and the UUID.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding SOURCE_DATE_EPOCH to the hash would mean we get extra diffs in the output for unrelated changes. That is OK, as it does not block verification with identical build inputs. But is there really an advantage in it?
Are there other relevant inputs to the epub output? Maybe those could be added to the hash as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good questions. I did a quick check and this is actually good. My suggestion is to always apply this new version, instead of conditionally.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

…tifier

The EPUB content.opf embedded the build time as dcterms:modified and a
random UUID as dc:identifier, making the .epub files differ between
two builds of the same docs.

Derive the identifier from the project name and version instead of
random data, and use SOURCE_DATE_EPOCH
(https://reproducible-builds.org/docs/source-date-epoch/) as the
modification time when it is set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@josevalim
josevalim merged commit 23cbd23 into elixir-lang:main Jul 27, 2026
6 checks passed
@josevalim

Copy link
Copy Markdown
Member

💚 💙 💜 💛 ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants