Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
349ebfa
Add support for parallel builds
jdillard Apr 2, 2026
6dded0a
Add tests
jdillard Apr 2, 2026
b35bdd1
update docs
jdillard Apr 2, 2026
afa688d
update changelog
jdillard Apr 2, 2026
be128e4
revert local fix
jdillard Apr 3, 2026
3324aee
Close parallel simplepdf subprocess log file after build
joby-jareddillard Apr 6, 2026
2a9e93c
Stop parallel PDF subprocess before cleanup when primary build fails
joby-jareddillard Apr 6, 2026
176d653
remove slow marker
joby-jareddillard Apr 6, 2026
cf4eeea
make note about passed args
joby-jareddillard Apr 7, 2026
005900a
Merge branch 'main' into parallel-build
joby-jareddillard Apr 7, 2026
e656499
add comment about listner priority
joby-jareddillard Apr 7, 2026
bb566a9
clean up docs
joby-jareddillard Apr 8, 2026
a861a8f
use sphinx_simplepdf/parallel_build.py
joby-jareddillard Apr 8, 2026
591bd0c
copy specific pdf not all pdfs
joby-jareddillard Apr 8, 2026
6cffe2c
run for all html builder types
joby-jareddillard Apr 10, 2026
259697a
rename to simplepdf_parallel_build
joby-jareddillard Apr 10, 2026
6ce722a
Add versionadded
joby-jareddillard Apr 10, 2026
09ff222
start with stdin=subprocess.DEVNULL so it never waits on the parent TTY
joby-jareddillard Apr 10, 2026
0dd39a6
Always clear parallel PDF temp dir reference in _cleanup
joby-jareddillard Apr 10, 2026
1efb4ed
update comment
joby-jareddillard Apr 10, 2026
19b0f6c
Lazy-import weasyprint so HTML builds skip native deps
joby-jareddillard Apr 11, 2026
ed388a7
Merge branch 'main' into parallel-build
joby-jareddillard Apr 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/building.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ Inside your ``docs`` folder, run::
or for more control::

sphinx-build -M simplepdf . _build

.. note:: To produce a PDF during ``html``, ``dirhtml``, or ``singlehtml`` builds (for example ``make html``), enable
:ref:`simplepdf_parallel_build` in :doc:`configuration`.
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changelog
Unreleased
----------

* **Enhancement**: Add ``simplepdf_parallel_build`` config option to generate PDFs alongside HTML builds via a concurrent subprocess.
* **Bugfix** [#134] Improve support for external theme packages by using a ``get_scss_sources_path()`` convention.

- If needed, theme warnings can be suppressed via ``suppress_warnings = ["simplepdf.theme"]``.
Expand Down
33 changes: 33 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,36 @@ To reduce output noise the output can be filtered by a list of regular expressio
``simplepdf_weasyprint_filter = ["WARNING: Ignored"]``

To suppress all output, the quiet flag `-q` should be used.

.. _simplepdf_parallel_build:

simplepdf_parallel_build
------------------------
.. versionadded:: Unreleased

A boolean value. If set to ``True``, a PDF is generated alongside your primary build when that build is one of Sphinx’s built-in HTML site builders.
Other builders (for example ``latex`` or ``epub``) are unchanged; use a separate ``sphinx-build -b simplepdf`` run if you need a PDF from those workflows.

This means you no longer need a dedicated ``simplepdf`` build step — the PDF is produced as a side effect of your normal build. Because the PDF build runs in parallel with the main builder, overall wall-clock time stays close to whichever of the two builds takes longer, not the sum of both.

**Example** — in ``conf.py``:

.. code-block:: python

simplepdf_parallel_build = True

Then build as usual (for example ``make html`` or ``sphinx-build -M html . _build``).

The PDF copied into the HTML output directory uses the same rules as a normal ``simplepdf`` build:
:ref:`simplepdf_file_name` if set (the file is taken from the same path under the subprocess output directory),
otherwise ``<project>.pdf``. Only the file name is placed in the HTML output directory, not any directory
components from ``simplepdf_file_name``.

The PDF subprocess is started with its own arguments (source directory, separate output and doctree
locations, and ``-q``). **Options you pass only on the parent command line**—for example ``-D``,
``-A``, ``-t``, ``-c``, or ``-n``—**are not forwarded** to that subprocess. The PDF build therefore
reads ``conf.py`` (and the environment) like a normal standalone run. If you need the PDF to match
a one-off CLI invocation, put those settings in ``conf.py`` (or run a separate
``sphinx-build -b simplepdf`` with the same flags).

Default: ``False``
9 changes: 9 additions & 0 deletions sphinx_simplepdf/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
from sphinx_simplepdf.directives.ifbuilder import IfBuilderDirective
from sphinx_simplepdf.directives.ifinclude import IfIncludeDirective
from sphinx_simplepdf.directives.pdfinclude import PdfIncludeDirective
from sphinx_simplepdf.parallel_build import register as register_parallel_build


def setup(app):
# Register simplepdf config and builder on every build: Sphinx loads this extension
# for all builders, but only loads builders/simplepdf.py automatically for -b simplepdf.
from sphinx_simplepdf.builders.simplepdf import setup as _builder_setup

_builder_setup(app)

app.add_directive("if-builder", IfBuilderDirective)
app.add_directive("if-include", IfIncludeDirective)
app.add_directive("pdf-include", PdfIncludeDirective)

register_parallel_build(app)

return {
"parallel_read_safe": True,
"parallel_write_safe": True,
Expand Down
5 changes: 4 additions & 1 deletion sphinx_simplepdf/builders/simplepdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from sphinx.builders.singlehtml import SingleFileHTMLBuilder
from sphinx.errors import ExtensionError
from sphinx.util import logging
import weasyprint

from sphinx_simplepdf.builders.debug import DebugPython
from sphinx_simplepdf.writers.simplepdf import SimplepdfTranslator
Expand Down Expand Up @@ -189,6 +188,10 @@ def finish(self) -> None:
filter_pattern = "(?:{})".format("|".join(filter_list)) if len(filter_list) > 0 else None

if self.config["simplepdf_use_weasyprint_api"]:
# Import only when this code path runs so ``extensions = ["sphinx_simplepdf"]`` on
# ``-b html`` does not load WeasyPrint's native dependencies (e.g. macOS CI).
import weasyprint

doc = weasyprint.HTML(index_path)

doc.write_pdf(
Expand Down
165 changes: 165 additions & 0 deletions sphinx_simplepdf/parallel_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
"""Optional parallel simplepdf subprocess during non-simplepdf builds."""

from pathlib import Path
import shutil
import subprocess
import sys
import tempfile

from sphinx.util import logging

logger = logging.getLogger(__name__)

# Standard HTML-site builders only; simplepdf is omitted so it never nests another simplepdf run.
_PARALLEL_PDF_HTML_BUILDERS = frozenset({"html", "dirhtml", "singlehtml"})


def _parallel_pdf_runs_for_builder(app) -> bool:
return app.builder.name in _PARALLEL_PDF_HTML_BUILDERS


def _configured_pdf_source(app, output_dir: Path) -> Path:
"""Path to the PDF under the simplepdf build output (matches the simplepdf builder)."""
raw = app.config.simplepdf_file_name
if raw:
return output_dir / Path(str(raw))
return output_dir / f"{app.config.project}.pdf"


def _configured_pdf_dest(app, html_outdir: Path) -> Path:
"""Destination path under the HTML output directory (flat basename, like a normal artifact)."""
raw = app.config.simplepdf_file_name
if raw:
return html_outdir / Path(str(raw)).name
return html_outdir / f"{app.config.project}.pdf"


class _PdfGenerator:
"""Manages an optional parallel simplepdf subprocess build."""

def __init__(self, app):
self.app = app
self.process = None
self.build_dir = None
self.log_path = None
self._log_fh = None

def _on_builder_inited(self, app):
if not app.config.simplepdf_parallel_build:
return
if not _parallel_pdf_runs_for_builder(app):
return

self._start_subprocess(app)

def _on_build_finished(self, app, exception):
if not app.config.simplepdf_parallel_build:
return
if not _parallel_pdf_runs_for_builder(app):
return

if exception:
logger.warning("sphinx-simplepdf: skipping PDF due to build error")
self._cleanup()
return

if self.process is None:
return

if self.process.poll() is None:
logger.info("sphinx-simplepdf: waiting for PDF build to finish...")
self.process.wait()

if self.process.returncode != 0:
log_hint = f" see log: {self.log_path}" if self.log_path else ""
logger.warning(f"sphinx-simplepdf: PDF build failed (exit {self.process.returncode}){log_hint}")
self._cleanup()
return

self._copy_pdf(app)
self._cleanup()

def _start_subprocess(self, app):
self.build_dir = Path(tempfile.mkdtemp(prefix="simplepdf_"))
self.log_path = self.build_dir / "build.log"

cmd = [
sys.executable,
"-m",
"sphinx",
"-b",
"simplepdf",
str(app.srcdir),
str(self.build_dir / "output"),
"-d",
str(self.build_dir / "doctrees"),
"-q",
]

logger.info("sphinx-simplepdf: starting PDF build subprocess")
self._log_fh = self.log_path.open("w")
self.process = subprocess.Popen(
cmd,
stdin=subprocess.DEVNULL,
stdout=self._log_fh,
stderr=subprocess.STDOUT,
)

def _copy_pdf(self, app):
if self.build_dir is None:
return

output_dir = self.build_dir / "output"
html_out = Path(app.outdir)
expected_path = _configured_pdf_source(app, output_dir)
target = _configured_pdf_dest(app, html_out)

if expected_path.is_file():
shutil.copy2(expected_path, target)
logger.info(f"sphinx-simplepdf: {expected_path.name} -> {target}")
return

logger.warning("sphinx-simplepdf: expected PDF not found at %s", expected_path)

def _stop_pdf_subprocess(self):
"""If the PDF child is still running, stop it before temp dir removal."""
proc = self.process
if proc is None:
return
if proc.poll() is not None:
self.process = None
return
logger.info("sphinx-simplepdf: terminating PDF build subprocess (primary build failed)")
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
logger.warning("sphinx-simplepdf: PDF subprocess did not exit after terminate; killing")
proc.kill()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
logger.warning("sphinx-simplepdf: PDF subprocess did not respond to kill")
self.process = None

def _cleanup(self):
self._stop_pdf_subprocess()
if self._log_fh is not None:
try:
self._log_fh.close()
finally:
self._log_fh = None
if self.build_dir is not None:
if self.build_dir.exists():
shutil.rmtree(self.build_dir, ignore_errors=True)
self.build_dir = None


def register(app):
"""Register config and Sphinx event hooks for parallel PDF builds."""
app.add_config_value("simplepdf_parallel_build", False, "env", types=[bool])

gen = _PdfGenerator(app)
app.connect("builder-inited", gen._on_builder_inited)
# Sphinx invokes listeners in ascending priority order (default 500); 101 runs before most.
app.connect("build-finished", gen._on_build_finished, priority=101)
Loading
Loading