Repository: readthedocs/readthedocs.org

Issue:
Document build failed with the error "Build output directory for format "pdf" contains multiple files and it is not currently supported."
I build my project document successfully before, but recently it failed with the error message "Build output directory for format "pdf" contains multiple files and it is not currently supported. Please, remove all the files but the "pdf" your want to upload. "
<img width="789" alt="image" src="https://user-images.githubusercontent.com/100339350/218298781-a0f0fa28-e508-4bbb-9356-19eec81a5705.png">

it seems like error occurred in this step: 
<img width="762" alt="image" src="https://user-images.githubusercontent.com/100339350/218298834-ee986ae5-a7fa-4e1e-8cff-6dce75f50639.png">

I didn't find any method to solve this problem. Is there anyone who knows this problem? Thank u.  T-T


Discussion:
Hi @TTYee! Can you provide the information asked in the template when you filled the form? (project URL, build URL, etc)
Cython has the same problem.

* Read the Docs project URL: https://readthedocs.org/projects/cython/
* Build URL (the latest successful build): https://readthedocs.org/projects/cython/builds/19395633/
* Build URL (the first failed build): https://readthedocs.org/projects/cython/builds/19423261/
@GalaxySnail hi! Are you able to build the PDF locally using latexmk?
> hi! Are you able to build the PDF locally using latexmk?

I haven't tried it. I'm running diff on these 2 logs:

``` bash
diff -Nu <(curl https://readthedocs.org/api/v2/build/19395633.txt) <(curl https://readthedocs.org/api/v2/build/19423261.txt) | head -n 20
```

``` diff
--- /proc/self/fd/11    2023-02-19 07:42:14.000000000 +0800
+++ /proc/self/fd/14    2023-02-19 07:42:14.000000000 +0800
@@ -1,18 +1,18 @@
 Read the Docs build information
-Build id: 19395633
+Build id: 19423261
 Project: cython
 Version: latest
 Commit: b24286d35aaaf1318cd2bebb10e3e16d72965a5b
-Date: 2023-02-06T17:21:37.353864Z
+Date: 2023-02-08T17:40:15.490385Z
 State: finished
-Success: True
+Success: False


-[rtd-command-info] start-time: 2023-02-06T17:21:38.926762Z, end-time: 2023-02-06T17:21:47.712146Z, duration: 8, exit-code: 0
+[rtd-command-info] start-time: 2023-02-08T17:40:17.041926Z, end-time: 2023-02-08T17:40:26.123477Z, duration: 9, exit-code: 0
 git clone --no-single-branch --depth 50 https://github.com/cython/cython .
 Cloning into '.'...
```

It looks like most of them are the same, latexmk was always failing, the only meaningful different is `Success: True` and `Success: False`. Then I checked previous logs, document build started to succeed since a year ago:

- failed: https://readthedocs.org/projects/cython/builds/15999585/
- successful: https://readthedocs.org/projects/cython/builds/16031152/

It seems that something changed in readthedocs.org, failed latexmk didn't cause the build to fail. However, it seems to be fixed recently, so the build starts failing again.
Can you try building the PDF locally?
I found this error is confusing our users in different ways. The real solution here would be to **fail the build immediately when any of the PDF commands failed** (https://github.com/readthedocs/readthedocs.org/issues/7884). Right now, we are just moving forward and then the users are receiving this unexpected and confusing messages.

These are other related issues as well:
* https://github.com/readthedocs/readthedocs.org/issues/8359
* https://github.com/readthedocs/readthedocs.org/issues/3906
* https://github.com/readthedocs/readthedocs.org/issues/7884
* https://github.com/readthedocs/readthedocs.org/issues/8615

I think it would be good to standardize this behavior to match with the rest of the commands.


@GalaxySnail Hi. I've found the PDF output is not neccessary for my project( I only need HTML output up to now). So I closed the "enable PDF build"(which is set by default) on the "advanced setup" page of our project then the project can be built successfully again. I mean, if PDF output is not neccessary for you either, you can try this method.
![image](https://user-images.githubusercontent.com/100339350/220136481-9a335019-876e-49bb-acac-efc0dba34d94.png)

> Can you try building the PDF locally?

Yes. It can reproduce locally, but I don't know why.

``` shell
$ LC_ALL=C python -m sphinx -T -E -b latex -d _build/doctrees -D language=en . _readthedocs/pdf
```

<details>

<summary>expand output</summary>

```
Running Sphinx v6.1.3
making output directory... done
loading intersphinx inventory from https://docs.python.org/3/objects.inv...
building [mo]: targets for 0 po files that are out of date
writing output...
building [latex]: all documents
updating environment: [new config] 58 added, 0 changed, 0 removed
reading sources... [100%] src/userguide/wrapping_CPlusPlus
looking for now-outdated files... none found
pickling environment... done
checking consistency... D:\Documents\git\cython\docs\src\reference\interfacing_with_other_code.rst: WARNING: document isn't included in any toctree
D:\Documents\git\cython\docs\src\reference\limitations.rst: WARNING: document isn't included in any toctree
D:\Documents\git\cython\docs\src\reference\special_mention.rst: WARNING: document isn't included in any toctree
done
processing reference.tex... src/reference/index src/reference/compilation src/reference/special_methods_table
resolving references...
done
writing... done
processing tutorial.tex... src/tutorial/index src/tutorial/cython_tutorial src/tutorial/external src/tutorial/clibraries src/tutorial/cdef_classes src/tutorial/pxd_files src/tutorial/caveats src/tutorial/profiling_tutorial src/tutorial/strings src/tutorial/memory_allocation src/tutorial/embedding src/tutorial/pure src/tutorial/numpy src/tutorial/array src/tutorial/parallelization src/tutorial/readings src/tutorial/related_work src/tutorial/appendix
resolving references...
D:\Documents\git\cython\docs\src\tutorial\clibraries.rst:765: WARNING: Unparseable C cross-reference: 'void*'
Invalid C declaration: Expected identifier in nested name, got keyword: void [error at 4]
  void*
  ----^
D:\Documents\git\cython\docs\src\tutorial\clibraries.rst:839: WARNING: Unparseable C cross-reference: 'void*'
Invalid C declaration: Expected identifier in nested name, got keyword: void [error at 4]
  void*
  ----^
D:\Documents\git\cython\docs\src\tutorial\clibraries.rst:839: WARNING: Unparseable C cross-reference: 'void*'
Invalid C declaration: Expected identifier in nested name, got keyword: void [error at 4]
  void*
  ----^
D:\Documents\git\cython\docs\src\tutorial\strings.rst:167: WARNING: Unparseable C cross-reference: 'char*'
Invalid C declaration: Expected identifier in nested name, got keyword: char [error at 4]
  char*
  ----^
D:\Documents\git\cython\docs\src\tutorial\strings.rst:172: WARNING: Unparseable C cross-reference: 'char*'
Invalid C declaration: Expected identifier in nested name, got keyword: char [error at 4]
  char*
  ----^
D:\Documents\git\cython\docs\src\tutorial\strings.rst:326: WARNING: Unparseable C cross-reference: 'char*'
Invalid C declaration: Expected identifier in nested name, got keyword: char [error at 4]
  char*
  ----^
D:\Documents\git\cython\docs\src\tutorial\strings.rst:350: WARNING: Unparseable C cross-reference: 'std::string'
Invalid C declaration: Expected end of definition. [error at 3]
  std::string
  ---^
D:\Documents\git\cython\docs\src\tutorial\strings.rst:479: WARNING: Unparseable C cross-reference: 'char'
Invalid C declaration: Expected identifier in nested name, got keyword: char [error at 4]
  char
  ----^
D:\Documents\git\cython\docs\src\tutorial\strings.rst:493: WARNING: Unparseable C cross-reference: 'char'
Invalid C declaration: Expected identifier in nested name, got keyword: char [error at 4]
  char
  ----^
D:\Documents\git\cython\docs\src\tutorial\strings.rst:493: WARNING: Unparseable C cross-reference: 'char'
Invalid C declaration: Expected identifier in nested name, got keyword: char [error at 4]
  char
  ----^
D:\Documents\git\cython\docs\src\tutorial\strings.rst:511: WARNING: Unparseable C cross-reference: 'char'
Invalid C declaration: Expected identifier in nested name, got keyword: char [error at 4]
  char
  ----^
D:\Documents\git\cython\docs\src\tutorial\strings.rst:511: WARNING: Unparseable C cross-reference: 'unsigned char'
Invalid C declaration: Expected identifier in nested name, got keyword: unsigned [error at 8]
  unsigned char
  --------^
D:\Documents\git\cython\docs\src\tutorial\strings.rst:535: WARNING: Unparseable C cross-reference: 'long'
Invalid C declaration: Expected identifier in nested name, got keyword: long [error at 4]
  long
  ----^
D:\Documents\git\cython\docs\src\tutorial\strings.rst:535: WARNING: Unparseable C cross-reference: 'unsigned long'
Invalid C declaration: Expected identifier in nested name, got keyword: unsigned [error at 8]
  unsigned long
  --------^
D:\Documents\git\cython\docs\src\tutorial\strings.rst:535: WARNING: Unparseable C cross-reference: 'int'
Invalid C declaration: Expected identifier in nested name, got keyword: int [error at 3]
  int
  ---^
D:\Documents\git\cython\docs\src\tutorial\strings.rst:616: WARNING: Unparseable C cross-reference: 'char*'
Invalid C declaration: Expected identifier in nested name, got keyword: char [error at 4]
  char*
  ----^
D:\Documents\git\cython\docs\src\tutorial\strings.rst:653: WARNING: Unparseable C cross-reference: 'wchar_t*'
Invalid C declaration: Expected end of definition. [error at 7]
  wchar_t*
  -------^
D:\Documents\git\cython\docs\src\tutorial\strings.rst:663: WARNING: Unparseable C cross-reference: 'Py_UNICODE*'
Invalid C declaration: Expected end of definition. [error at 10]
  Py_UNICODE*
  ----------^
D:\Documents\git\cython\docs\src\tutorial\strings.rst:663: WARNING: Unparseable C cross-reference: 'char*'
Invalid C declaration: Expected identifier in nested name, got keyword: char [error at 4]
  char*
  ----^
D:\Documents\git\cython\docs\src\tutorial\strings.rst:669: WARNING: Unparseable C cross-reference: 'Py_UNICODE*'
Invalid C declaration: Expected end of definition. [error at 10]
  Py_UNICODE*
  ----------^
D:\Documents\git\cython\docs\src\tutorial\strings.rst:689: WARNING: Unparseable C cross-reference: 'Py_UNICODE*'
Invalid C declaration: Expected end of definition. [error at 10]
  Py_UNICODE*
  ----------^
D:\Documents\git\cython\docs\src\tutorial\memory_allocation.rst:25: WARNING: term not in glossary: 'Heap allocation'
done
writing... done
copying images... [100%] src/tutorial/python_division.png
copying TeX support files... copying TeX support files...
done
build succeeded, 25 warnings.

The LaTeX files are in _readthedocs\pdf.
```

</details>

``` shell
$ cd _readthedocs/pdf
$ cat latexmkrc
```

``` perl
$latex = 'latex ' . $ENV{'LATEXOPTS'} . ' %O %S';
$pdflatex = 'pdflatex ' . $ENV{'LATEXOPTS'} . ' %O %S';
$lualatex = 'lualatex ' . $ENV{'LATEXOPTS'} . ' %O %S';
$xelatex = 'xelatex --no-pdf ' . $ENV{'LATEXOPTS'} . ' %O %S';
$makeindex = 'makeindex -s python.ist %O -o %D %S';
add_cus_dep( "glo", "gls", 0, "makeglo" );
sub makeglo {
 return system( "makeindex -s gglo.ist -o '$_[0].gls' '$_[0].glo'" );
}
```

``` shell
$ latexmk -r latexmkrc -pdf -f -dvi- -ps- -jobname=cython -interaction=nonstopmode
```

```
Win CP console initial and current in/out Win: (936, 936), (936, 936)
Coding system for system and terminal: 'CP936'
---
Use of uninitialized value in concatenation (.) or string at (eval 10) line 1.
Use of uninitialized value in concatenation (.) or string at (eval 10) line 2.
Use of uninitialized value in concatenation (.) or string at (eval 10) line 3.
Use of uninitialized value in concatenation (.) or string at (eval 10) line 4.
Latexmk: A user -r option asked me to process an rc file an extra time.
   Name of file = 'latexmkrc'
   Abs. path = 'D:\Documents\git\cython\docs\_readthedocs\pdf\latexmkrc'
  I'll not process it
Rc files read:
  latexmkrc
Latexmk: This is Latexmk, John Collins, 17 Mar. 2022. Version 4.77, version: 4.77.

Latexmk: Need to specify at most one filename if jobname specified without a %A,
    but 2 were found (after defaults and wildcarding).
Use
   latexmk -help
to get usage information
C:\msys2\ucrt64\bin\runscript.tlu:915: command failed with exit code 10:
perl.exe c:\msys2\ucrt64\share\texmf-dist\scripts\latexmk\latexmk.pl -r latexmkrc -pdf -f -dvi- -ps- -jobname=cython -interaction=nonstopmode
```

> I found this error is confusing our users in different ways. The real solution here would be to **fail the build immediately when any of the PDF commands failed** (#7884). Right now, we are just moving forward and then the users are receiving this unexpected and confusing messages.

> Hi. I've found the PDF output is not neccessary for my project( I only need HTML output up to now). So I closed the "enable PDF build"(which is set by default) on the "advanced setup" page of our project then the project can be built successfully again. I mean, if PDF output is not neccessary for you either, you can try this method.

Thanks for the explanation. As a workaround, Cython has decided to stop building PDF docs.

Resolving patch:
diff --git a/readthedocs/doc_builder/backends/sphinx.py b/readthedocs/doc_builder/backends/sphinx.py
--- a/readthedocs/doc_builder/backends/sphinx.py
+++ b/readthedocs/doc_builder/backends/sphinx.py
@@ -26,8 +26,6 @@
 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
 
@@ -488,30 +486,6 @@ 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."""
@@ -589,11 +563,6 @@ 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',
@@ -610,16 +579,18 @@ def _build_latexmk(self, cwd):
             '-interaction=nonstopmode',
         ]
 
-        cmd_ret = self.build_env.run_command_class(
-            cls=latex_class,
-            cmd=cmd,
-            warn_only=True,
-            cwd=self.absolute_host_output_dir,
-        )
-
-        self.pdf_file_name = f'{self.project.slug}.pdf'
+        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
 
-        return cmd_ret.successful
+        # Catch the exception and re-raise it with a specific message
+        except BuildUserError:
+            raise BuildUserError(BuildUserError.PDF_COMMAND_FAILED)
+        return False
 
     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
--- a/readthedocs/doc_builder/constants.py
+++ b/readthedocs/doc_builder/constants.py
@@ -1,15 +1,12 @@
 
 """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
--- a/readthedocs/doc_builder/exceptions.py
+++ b/readthedocs/doc_builder/exceptions.py
@@ -52,6 +52,13 @@ class BuildUserError(BuildBaseException):
         "and it is not currently supported. "
         'Please, remove all the files but the "{artifact_type}" your want to upload.'
     )
+    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):


Test patch:
diff --git a/readthedocs/projects/tests/mockers.py b/readthedocs/projects/tests/mockers.py
--- a/readthedocs/projects/tests/mockers.py
+++ b/readthedocs/projects/tests/mockers.py
@@ -58,13 +58,6 @@ 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
--- a/readthedocs/projects/tests/test_build_tasks.py
+++ b/readthedocs/projects/tests/test_build_tasks.py
@@ -723,6 +723,18 @@ 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(
@@ -794,6 +806,14 @@ def test_build_commands_executed(
                     record=False,
                     demux=True,
                 ),
+                mock.call(
+                    "test",
+                    "-x",
+                    "_build/html",
+                    record=False,
+                    demux=True,
+                    cwd=mock.ANY,
+                ),
             ]
         )