This commit is contained in:
rushjais 2026-10-13 12:22:37 -07:00 • committed by GitHub
commit 509726881a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 52 additions and 7 deletions

3
RELEASE.md Normal file
View file

@ -0,0 +1,3 @@
Release type: patch
Process content and static files in a stable, sorted order so that build output no longer depends on the interpreter's hash seed

View file

@ -259,8 +259,10 @@ class Generator:
def _check_disabled_readers(self, paths, exclude: list[str] | None) -> None:
"""Log warnings for files that would have been processed by disabled readers."""
for fil in self.get_files(
paths, exclude=exclude, extensions=self.readers.disabled_extensions
for fil in sorted(
self.get_files(
paths, exclude=exclude, extensions=self.readers.disabled_extensions
)
):
self.readers.check_file(fil)
@ -705,8 +707,11 @@ class ArticlesGenerator(CachingGenerator):
all_articles = []
all_drafts = []
hidden_articles = []
for f in self.get_files(
self.settings["ARTICLE_PATHS"], exclude=self.settings["ARTICLE_EXCLUDES"]
for f in sorted(
self.get_files(
self.settings["ARTICLE_PATHS"],
exclude=self.settings["ARTICLE_EXCLUDES"],
)
):
article = self.get_cached_data(f, None)
if article is None:
@ -920,8 +925,10 @@ class PagesGenerator(CachingGenerator):
all_pages = []
hidden_pages = []
draft_pages = []
for f in self.get_files(
self.settings["PAGE_PATHS"], exclude=self.settings["PAGE_EXCLUDES"]
for f in sorted(
self.get_files(
self.settings["PAGE_PATHS"], exclude=self.settings["PAGE_EXCLUDES"]
)
):
page = self.get_cached_data(f, None)
if page is None:
@ -1039,7 +1046,7 @@ class StaticGenerator(Generator):
exclude=self.settings["STATIC_EXCLUDES"],
extensions=False,
)
for f in linked_files | found_files:
for f in sorted(linked_files | found_files):
# skip content source files unless the user explicitly wants them
if self.settings["STATIC_EXCLUDE_SOURCES"]:
if self._is_potential_source_path(f):

View file

@ -1279,6 +1279,41 @@ class TestStaticGenerator(unittest.TestCase):
def set_ancient_mtime(self, path, timestamp=1):
os.utime(path, (timestamp, timestamp))
def test_static_files_processed_in_stable_order(self):
"""Static files are processed in sorted order, not in the iteration
order of the sets returned by get_files(), which depends on the
interpreter's hash seed."""
class ReversedSet(set):
# Iterates in reverse sorted order, standing in for an unlucky
# PYTHONHASHSEED; set unions keep the behavior.
def __iter__(self):
return iter(sorted(super().__iter__(), reverse=True))
def __or__(self, other):
return ReversedSet(set(self) | set(other))
__ror__ = __or__
settings = get_settings(PATH=self.content_path, STATIC_PATHS=[""])
context = get_context(settings)
generator = StaticGenerator(
context=context,
settings=settings,
path=settings["PATH"],
output_path=self.temp_output,
theme=settings["THEME"],
)
get_files = generator.get_files
generator.get_files = lambda *args, **kwargs: ReversedSet(
get_files(*args, **kwargs)
)
generator.generate_context()
source_paths = [sf.source_path for sf in context["staticfiles"]]
self.assertGreater(len(source_paths), 1)
self.assertEqual(source_paths, sorted(source_paths))
def test_theme_static_paths_dirs(self):
"""Test that StaticGenerator properly copies also files mentioned in
TEMPLATE_STATIC_PATHS, not just directories."""