diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index bb02058f7bf..4871f4603c8 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -15,7 +15,7 @@ from sphinx.config import Config from sphinx.deprecation import RemovedInSphinx70Warning -from sphinx.environment import CONFIG_CHANGED_REASON, CONFIG_OK, BuildEnvironment +from sphinx.environment import CONFIG_CHANGED_REASON, CONFIG_OK, BuildEnvironment, _safe_mtime from sphinx.environment.adapters.asset import ImageAdapter from sphinx.errors import SphinxError from sphinx.events import EventManager @@ -512,11 +512,7 @@ def read_doc(self, docname: str) -> None: doctree = publisher.document # store time of reading, for outdated files detection - # (Some filesystems have coarse timestamp resolution; - # therefore time.time() can be older than filesystem's timestamp. - # For example, FAT32 has 2sec timestamp resolution.) - self.env.all_docs[docname] = max(time.time(), - path.getmtime(self.env.doc2path(docname))) + self.env.all_docs[docname] = _safe_mtime(self.env.doc2path(docname)) # cleanup self.env.temp_data.clear() diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index e6f1e162179..979f13935ff 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -9,6 +9,7 @@ from copy import copy from datetime import datetime from os import path +import time from typing import TYPE_CHECKING, Any, Callable, Generator, Iterator from docutils import nodes @@ -172,6 +173,9 @@ def __init__(self, app: Sphinx): # docname -> set of dependent file # names, relative to documentation root self.dependencies: dict[str, set[str]] = defaultdict(set) + # dependent file -> mtime at the time of reading + # contains all dependent files + self.all_dependencies: dict[str, float] = {} # docname -> set of included file # docnames included from other documents self.included: dict[str, set[str]] = defaultdict(set) @@ -447,6 +451,7 @@ def find_files(self, config: Config, builder: Builder) -> None: domain = docname_to_domain(docname, self.config.gettext_compact) if domain in mo_paths: self.dependencies[docname].add(mo_paths[domain]) + self.all_dependencies[mo_paths[domain]] = _safe_mtime(mo_paths[domain]) except OSError as exc: raise DocumentError(__('Failed to scan documents in %s: %r') % (self.srcdir, exc)) from exc @@ -502,19 +507,24 @@ def get_outdated_files(self, config_changed: bool) -> tuple[set[str], set[str], deppath) changed.add(docname) break - depmtime = path.getmtime(deppath) - if depmtime > mtime: + depmtime = self.all_dependencies[dep] + newdepmtime = path.getmtime(deppath) + if newdepmtime > depmtime: try: - depmtime_dt = datetime.utcfromtimestamp(depmtime) + newdepmtime_dt = datetime.utcfromtimestamp(newdepmtime) except ValueError: # e.g., year 53606865 is out of range + newdepmtime_dt = depmtime + try: + depmtime_dt = datetime.utcfromtimestamp(depmtime) + except ValueError: depmtime_dt = depmtime logger.debug( '[build target] outdated %r ' 'from dependency %r: %s -> %s', docname, deppath, - datetime.utcfromtimestamp(mtime), - depmtime_dt) + depmtime_dt, + newdepmtime_dt) changed.add(docname) break except OSError: @@ -567,6 +577,7 @@ def note_dependency(self, filename: str) -> None: *filename* should be absolute or relative to the source directory. """ self.dependencies[self.docname].add(filename) + self.all_dependencies[filename] = path.getmtime(filename) def note_included(self, filename: str) -> None: """Add *filename* as a included from other document. @@ -744,3 +755,10 @@ def check_consistency(self) -> None: for domain in self.domains.values(): domain.check_consistency() self.events.emit('env-check-consistency', self) + + +def _safe_mtime(fname): + # (Some filesystems have coarse timestamp resolution; + # therefore time.time() can be older than filesystem's timestamp. + # For example, FAT32 has 2sec timestamp resolution.) + return max(time.time(), path.getmtime(fname))