quizcomp.external.latex

  1import argparse
  2import logging
  3import os
  4import shutil
  5import subprocess
  6import typing
  7
  8_logger = logging.getLogger(__name__)
  9
 10_pdflatex_bin_path: typing.Union[str, None] = None  # pylint: disable=invalid-name
 11_pdflatex_use_docker: bool = False  # pylint: disable=invalid-name
 12
 13DOCKER_IMAGE: str = "ghcr.io/edulinq/pdflatex-docker:1.0.0"
 14
 15def set_pdflatex_bin_path(path: str) -> None:
 16    """ Set the binary path to use for NodeJS (node). """
 17
 18    global _pdflatex_bin_path  # pylint: disable=global-statement
 19    _pdflatex_bin_path = path
 20
 21def set_pdflatex_use_docker(pdflatex_use_docker: bool) -> None:
 22    """ Set whether or not to use the pdflatex Docker container. """
 23
 24    global _pdflatex_use_docker  # pylint: disable=global-statement
 25    _pdflatex_use_docker = pdflatex_use_docker
 26
 27def is_available() -> bool:
 28    """ Check if LaTeX is available on this system. """
 29
 30    if (_pdflatex_use_docker):
 31        if (not _is_docker_available()):
 32            _logger.warning("Docker is not available, cannot compile PDFs.")
 33            return False
 34
 35        return True
 36
 37    if (_pdflatex_bin_path is not None):
 38        return True
 39
 40    if (shutil.which('pdflatex') is None):
 41        _logger.warning("Could not find `pdflatex`, cannot compile PDFs.")
 42        return False
 43
 44    return True
 45
 46def _is_docker_available() -> bool:
 47    """ Check if Docker is available on this system. """
 48
 49    if (shutil.which('docker') is None):
 50        return False
 51
 52    result = subprocess.run(["docker", "info"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL, check = False)
 53    return (result.returncode == 0)
 54
 55def compile(path: str) -> None:
 56    """
 57    Compile a LaTeX file to PDF in its containing directory.
 58
 59    The caller must provide a path to a TeX file within a prepared output directory.
 60    This directory should contain all necessary resources (e.g., images) and no non-relevant files.
 61    Compilation may generate additional files (e.g., .aux, .log) in this directory,
 62    and permissions may be modified as needed.
 63    """
 64
 65    if (_pdflatex_use_docker is True):
 66        _compile_docker(path)
 67    else:
 68        _compile_local(path)
 69
 70def _compile_local(path: str) -> None:
 71    """ Compile without using Docker. """
 72
 73    bin_path = "pdflatex"
 74    if (_pdflatex_bin_path is not None):
 75        bin_path = _pdflatex_bin_path
 76
 77    tex_filename = os.path.basename(path)
 78    out_dir = os.path.dirname(path)
 79
 80    # Need to compile twice to get positioning information.
 81    for _ in range(2):
 82        result = subprocess.run([bin_path, '-interaction=nonstopmode', tex_filename],
 83                                cwd = out_dir, text = True, capture_output = True, check = False)
 84        if (result.returncode != 0):
 85            raise ValueError(f"pdflatex did not exit cleanly. Stdout: '{result.stdout}', Stderr: '{result.stderr}'")
 86
 87def _compile_docker(path: str) -> None:
 88    """ Compile using Docker. """
 89
 90    tex_filename = os.path.basename(path)
 91    out_dir_path = os.path.abspath(os.path.dirname(path))
 92
 93    docker_cmd = [
 94        "docker", "run", "--rm",
 95        "-v", f"{out_dir_path}:/work",
 96        DOCKER_IMAGE,
 97        tex_filename
 98    ]
 99
100    result = subprocess.run(docker_cmd, capture_output = True, text = True, check = False)
101    if (result.returncode != 0):
102        raise ValueError(f"Docker compilation failed with exit code '{result.returncode}'. Stdout: '{result.stdout}', Stderr: '{result.stderr}'")
103
104def set_cli_args(parser: argparse.ArgumentParser, extra_state: typing.Dict[str, typing.Any]) -> None:
105    """ Set LaTeX-related CLI options. """
106
107    parser.add_argument('--pdflatex-bin-path', dest = 'pdflatex_bin_path',
108        action = 'store', type = str, default = None,
109        help = ('The path to the pdflatex binary to use.'
110                + ' If not specified, $PATH will be searched.'
111                + ' Used to compile PDFs.'))
112
113    parser.add_argument('--pdflatex-use-docker', dest = 'pdflatex_use_docker',
114        action = 'store_true', default = False,
115        help = ('Use Docker to compile PDFs with pdflatex.'
116                + f" The Docker image '{DOCKER_IMAGE}' will be used."))
117
118def init_from_args(parser: argparse.ArgumentParser, args: argparse.Namespace, extra_state: typing.Dict[str, typing.Any]) -> None:
119    """ Initialize this module from the CLI options set in set_cli_args(). """
120
121    if (args.pdflatex_use_docker):
122        set_pdflatex_use_docker(args.pdflatex_use_docker)
123
124    if (args.pdflatex_bin_path is not None):
125        set_pdflatex_bin_path(args.pdflatex_bin_path)
DOCKER_IMAGE: str = 'ghcr.io/edulinq/pdflatex-docker:1.0.0'
def set_pdflatex_bin_path(path: str) -> None:
16def set_pdflatex_bin_path(path: str) -> None:
17    """ Set the binary path to use for NodeJS (node). """
18
19    global _pdflatex_bin_path  # pylint: disable=global-statement
20    _pdflatex_bin_path = path

Set the binary path to use for NodeJS (node).

def set_pdflatex_use_docker(pdflatex_use_docker: bool) -> None:
22def set_pdflatex_use_docker(pdflatex_use_docker: bool) -> None:
23    """ Set whether or not to use the pdflatex Docker container. """
24
25    global _pdflatex_use_docker  # pylint: disable=global-statement
26    _pdflatex_use_docker = pdflatex_use_docker

Set whether or not to use the pdflatex Docker container.

def is_available() -> bool:
28def is_available() -> bool:
29    """ Check if LaTeX is available on this system. """
30
31    if (_pdflatex_use_docker):
32        if (not _is_docker_available()):
33            _logger.warning("Docker is not available, cannot compile PDFs.")
34            return False
35
36        return True
37
38    if (_pdflatex_bin_path is not None):
39        return True
40
41    if (shutil.which('pdflatex') is None):
42        _logger.warning("Could not find `pdflatex`, cannot compile PDFs.")
43        return False
44
45    return True

Check if LaTeX is available on this system.

def compile(path: str) -> None:
56def compile(path: str) -> None:
57    """
58    Compile a LaTeX file to PDF in its containing directory.
59
60    The caller must provide a path to a TeX file within a prepared output directory.
61    This directory should contain all necessary resources (e.g., images) and no non-relevant files.
62    Compilation may generate additional files (e.g., .aux, .log) in this directory,
63    and permissions may be modified as needed.
64    """
65
66    if (_pdflatex_use_docker is True):
67        _compile_docker(path)
68    else:
69        _compile_local(path)

Compile a LaTeX file to PDF in its containing directory.

The caller must provide a path to a TeX file within a prepared output directory. This directory should contain all necessary resources (e.g., images) and no non-relevant files. Compilation may generate additional files (e.g., .aux, .log) in this directory, and permissions may be modified as needed.

def set_cli_args(parser: argparse.ArgumentParser, extra_state: Dict[str, Any]) -> None:
105def set_cli_args(parser: argparse.ArgumentParser, extra_state: typing.Dict[str, typing.Any]) -> None:
106    """ Set LaTeX-related CLI options. """
107
108    parser.add_argument('--pdflatex-bin-path', dest = 'pdflatex_bin_path',
109        action = 'store', type = str, default = None,
110        help = ('The path to the pdflatex binary to use.'
111                + ' If not specified, $PATH will be searched.'
112                + ' Used to compile PDFs.'))
113
114    parser.add_argument('--pdflatex-use-docker', dest = 'pdflatex_use_docker',
115        action = 'store_true', default = False,
116        help = ('Use Docker to compile PDFs with pdflatex.'
117                + f" The Docker image '{DOCKER_IMAGE}' will be used."))

Set LaTeX-related CLI options.

def init_from_args( parser: argparse.ArgumentParser, args: argparse.Namespace, extra_state: Dict[str, Any]) -> None:
119def init_from_args(parser: argparse.ArgumentParser, args: argparse.Namespace, extra_state: typing.Dict[str, typing.Any]) -> None:
120    """ Initialize this module from the CLI options set in set_cli_args(). """
121
122    if (args.pdflatex_use_docker):
123        set_pdflatex_use_docker(args.pdflatex_use_docker)
124
125    if (args.pdflatex_bin_path is not None):
126        set_pdflatex_bin_path(args.pdflatex_bin_path)

Initialize this module from the CLI options set in set_cli_args().