diff --git a/readthedocs/doc_builder/backends/sphinx.py b/readthedocs/doc_builder/backends/sphinx.py index 0c1a409c520..43778e31441 100644 --- a/readthedocs/doc_builder/backends/sphinx.py +++ b/readthedocs/doc_builder/backends/sphinx.py @@ -26,6 +26,8 @@ from readthedocs.projects.utils import safe_write from ..base import BaseBuilder +from ..constants import PDF_RE +from ..environments import BuildCommand, DockerBuildCommand from ..exceptions import BuildUserError from ..signals import finalize_sphinx_context_data @@ -486,6 +488,30 @@ def _post_build(self): ) +class LatexBuildCommand(BuildCommand): + + """Ignore LaTeX exit code if there was file output.""" + + def run(self): + super().run() + # Force LaTeX exit code to be a little more optimistic. If LaTeX + # reports an output file, let's just assume we're fine. + if PDF_RE.search(self.output): + self.exit_code = 0 + + +class DockerLatexBuildCommand(DockerBuildCommand): + + """Ignore LaTeX exit code if there was file output.""" + + def run(self): + super().run() + # Force LaTeX exit code to be a little more optimistic. If LaTeX + # reports an output file, let's just assume we're fine. + if PDF_RE.search(self.output): + self.exit_code = 0 + + class PdfBuilder(BaseSphinx): """Builder to generate PDF documentation.""" @@ -563,6 +589,11 @@ def _build_latexmk(self, cwd): cwd=self.absolute_host_output_dir, ) + if self.build_env.command_class == DockerBuildCommand: + latex_class = DockerLatexBuildCommand + else: + latex_class = LatexBuildCommand + cmd = [ 'latexmk', '-r', @@ -579,18 +610,16 @@ def _build_latexmk(self, cwd): '-interaction=nonstopmode', ] - try: - cmd_ret = self.run( - *cmd, - cwd=self.absolute_host_output_dir, - ) - self.pdf_file_name = f"{self.project.slug}.pdf" - return cmd_ret.successful + cmd_ret = self.build_env.run_command_class( + cls=latex_class, + cmd=cmd, + warn_only=True, + cwd=self.absolute_host_output_dir, + ) - # Catch the exception and re-raise it with a specific message - except BuildUserError: - raise BuildUserError(BuildUserError.PDF_COMMAND_FAILED) - return False + self.pdf_file_name = f"{self.project.slug}.pdf" + + return cmd_ret.successful def _post_build(self): """Internal post build to cleanup PDF output directory and leave only one .pdf file.""" diff --git a/readthedocs/doc_builder/constants.py b/readthedocs/doc_builder/constants.py index d1400615bc8..94220e6511e 100644 --- a/readthedocs/doc_builder/constants.py +++ b/readthedocs/doc_builder/constants.py @@ -1,12 +1,15 @@ """Doc build constants.""" +import re import structlog from django.conf import settings log = structlog.get_logger(__name__) +PDF_RE = re.compile("Output written on (.*?)") + # Docker DOCKER_SOCKET = settings.DOCKER_SOCKET DOCKER_VERSION = settings.DOCKER_VERSION diff --git a/readthedocs/doc_builder/exceptions.py b/readthedocs/doc_builder/exceptions.py index be874b5073d..02ac48b3aa0 100644 --- a/readthedocs/doc_builder/exceptions.py +++ b/readthedocs/doc_builder/exceptions.py @@ -57,13 +57,6 @@ class BuildUserError(BuildBaseException): "Ensure your project is configured to use the output path " "'$READTHEDOCS_OUTPUT/html' instead." ) - PDF_COMMAND_FAILED = gettext_noop( - "PDF generation failed. " - "The build log below contains information on what errors caused the failure. " - "Our code has recently changed to fail the entire build on PDF errors, " - "where we used to pass the build when a PDF was created. " - "Please contact us if you need help understanding this error." - ) class BuildUserSkip(BuildUserError): diff --git a/readthedocs/projects/tests/mockers.py b/readthedocs/projects/tests/mockers.py index 8371f1b4143..471fc00008a 100644 --- a/readthedocs/projects/tests/mockers.py +++ b/readthedocs/projects/tests/mockers.py @@ -58,6 +58,13 @@ def _mock_artifact_builders(self): "project-slug.pdf", ) + self.patches["builder.pdf.LatexBuildCommand.run"] = mock.patch( + "readthedocs.doc_builder.backends.sphinx.LatexBuildCommand.run", + return_value=mock.MagicMock(output="stdout", successful=True), + ) + # self.patches['builder.pdf.LatexBuildCommand.output'] = mock.patch( + # 'readthedocs.doc_builder.backends.sphinx.LatexBuildCommand.output', + # ) self.patches['builder.pdf.glob'] = mock.patch( 'readthedocs.doc_builder.backends.sphinx.glob', return_value=['output.file'], diff --git a/readthedocs/projects/tests/test_build_tasks.py b/readthedocs/projects/tests/test_build_tasks.py index 2b69d66cc53..16cff2d3170 100644 --- a/readthedocs/projects/tests/test_build_tasks.py +++ b/readthedocs/projects/tests/test_build_tasks.py @@ -723,18 +723,6 @@ def test_build_commands_executed( bin_path=mock.ANY, ), mock.call("cat", "latexmkrc", cwd=mock.ANY), - mock.call( - "latexmk", - "-r", - "latexmkrc", - "-pdf", - "-f", - "-dvi-", - "-ps-", - "-jobname=project", - "-interaction=nonstopmode", - cwd=mock.ANY, - ), # NOTE: pdf `mv` commands and others are not here because the # PDF resulting file is not found in the process (`_post_build`) mock.call(