2# +==== BEGIN CatFeeder =================+
4# ..............(..../\\
5# ...............)..(.')
7# ...............\\(__)|
8# Inspired by Joan Stark
9# source https://www.asciiart.eu/
14# CREATION DATE: 16-01-2026
15# LAST Modified: 23:55:24 16-01-2026
17# This is the backend server in charge of making the actual website work.
19# COPYRIGHT: (c) Cat Feeder
20# PURPOSE: The code in charge of downloading the tyni tex binaries and exposing them to the PATH.
22# +==== END CatFeeder =================+
28from pathlib
import Path
29from typing
import Dict, Tuple, Optional, Union
33from display_tty
import Disp, initialise_logger
35from ..core
import FinalClass
36from ..utils
import CONST
38DOWNLOAD_PATH: Union[str, Path] = CONST.ASSETS_DIRECTORY /
"tinytex"
43 Downloads and installs TinyTeX locally in the current directory.
44 Handles cross-platform archive selection, SHA256 verification,
45 extraction, and exposes the bin folder to Python via PATH.
48 installer = TinyTeXInstaller(flavour="TinyTeX-1")
49 bin_path = installer.install()
52 disp: Disp = initialise_logger(__qualname__, CONST.DEBUG)
54 _instance: Optional[
"TinyTeXInstaller"] =
None
57 RELEASES: Dict[str, Dict[str, Tuple[str, str]]] = {
60 "https://github.com/rstudio/tinytex-releases/releases/download/v2026.01/TinyTeX-0-v2026.01.zip",
61 "2ecc48c2e25387b4736f63fdcbe7fddeb8027e8da3bcbf4f3149e3affe926722"
64 "https://github.com/rstudio/tinytex-releases/releases/download/v2026.01/TinyTeX-1-v2026.01.zip",
65 "807d2600e9bf7171a3785416ad2e9d2cf18aeabc6cb56f533ad43d2819d37b8d"
70 "https://github.com/rstudio/tinytex-releases/releases/download/v2026.01/TinyTeX-0-v2026.01.tar.gz",
71 "5b1cc012e4c033ef7748023d67ae6709b1db65b738edd2c69e82bedf297ba4cd"
74 "https://github.com/rstudio/tinytex-releases/releases/download/v2026.01/TinyTeX-1-v2026.01.tar.gz",
75 "91c7e636c900d70f3731149c1b54ede26a5c10212b0bd1b7694c1b0d898ec37a"
80 "https://github.com/rstudio/tinytex-releases/releases/download/v2026.01/TinyTeX-0-v2026.01.tgz",
81 "638a817a448f896b81de8219896a20c306fd484067136f3febdf12c03bb49605"
84 "https://github.com/rstudio/tinytex-releases/releases/download/v2026.01/TinyTeX-1-v2026.01.tgz",
85 "1eec5125d45b73c3112399afa7c2c3595803b17c762bfd97180c86656d183706"
90 def __new__(cls, *args, **kwargs) -> "TinyTeXInstaller":
95 def __init__(self, flavour: str =
"TinyTeX-1", install_dir_name: Union[str, Path] = DOWNLOAD_PATH, *, timeout_seconds: int = 300) ->
None:
96 self.
disp.log_debug(
"Initialising...")
104 f
"flavour '{flavour}' not available for OS '{self.os_key}'"
108 self.
disp.log_debug(
"Initialised.")
115 """Detect the current operating system."""
116 if sys.platform.startswith(
"win"):
118 if sys.platform.startswith(
"linux"):
120 if sys.platform.startswith(
"darwin"):
122 raise RuntimeError(f
"Unsupported OS: {sys.platform}")
125 """Download a file from a URL to a local destination.
128 url (str): The URL to download from.
129 dest_path (str): The local file path to save the downloaded file.
131 self.
disp.log_info(f
"Downloading {url} ...")
132 with requests.get(url, stream=
True, timeout=self.
timeout)
as r:
134 with open(dest_path,
"wb")
as f:
135 for chunk
in r.iter_content(chunk_size=8192):
137 self.
disp.log_info(f
"Downloaded to {dest_path}")
140 """Verify the SHA256 hash of a file.
143 file_path (str): Path to the file to verify.
144 expected_hash (str): Expected SHA256 hash string.
147 ValueError: If the hash does not match.
149 self.
disp.log_info(f
"Verifying SHA256 for {file_path} ...")
151 with open(file_path,
"rb")
as f:
152 for chunk
in iter(
lambda: f.read(8192), b
""):
154 actual_hash = h.hexdigest()
155 if actual_hash != expected_hash:
157 f
"SHA256 mismatch: {actual_hash} != {expected_hash}"
160 f
"SHA256 mismatch: {actual_hash} != {expected_hash}"
162 self.
disp.log_info(
"SHA256 verified")
165 self.
disp.log_info(f
"Extracting {archive_path} → {dest_dir}")
166 if archive_path.endswith(
".zip"):
167 self.
disp.log_debug(
"Detected ZIP archive.")
168 with zipfile.ZipFile(archive_path,
"r")
as zf:
169 zf.extractall(dest_dir)
170 elif archive_path.endswith((
".tar.gz",
".tgz")):
171 self.
disp.log_debug(
"Detected TAR.GZ archive.")
172 with tarfile.open(archive_path,
"r:gz")
as tf:
173 tf.extractall(dest_dir)
175 self.
disp.log_error(
"Unknown archive format.")
176 raise ValueError(f
"Unknown archive format: {archive_path}")
179 """Install TinyTeX locally and expose its bin folder. Returns the bin path."""
180 self.
disp.log_info(
"Starting TinyTeX installation...")
183 self.
disp.log_debug(f
"Installation directory: {install_dir}")
184 os.makedirs(install_dir, exist_ok=
True)
186 archive_path = os.path.join(install_dir, os.path.basename(self.
url))
187 self.
disp.log_debug(f
"Archive path: {archive_path}")
189 if not os.path.exists(archive_path):
191 f
"{archive_path} does not exist, downloading...")
193 self.
disp.log_debug(
"Download complete, verifying...")
197 f
"{archive_path} already exists, skipping download.")
203 bin_path: Optional[str] =
None
204 for root, dirs, _
in os.walk(install_dir):
206 bin_path = os.path.join(root,
"bin")
208 self.
disp.log_debug(f
"Bin path: {bin_path}")
212 "Could not find TinyTeX 'bin' directory after extraction")
215 os.environ[
"PATH"] = f
"{bin_path}{os.pathsep}{os.environ.get('PATH', '')}"
216 sys.path.insert(0, bin_path)
217 self.
disp.log_info(f
"TinyTeX bin available at: {bin_path}")
223if __name__ ==
"__main__":
225 bin_dir = installer.install()
226 print(
"You can now run pdflatex, xelatex, lualatex from Python or Pandoc.")
"TinyTeXInstaller" __new__(cls, *args, **kwargs)
None _extract_archive(self, str archive_path, str dest_dir)
Union[str, Path] install_dir_name
None _verify_sha256(self, str file_path, str expected_hash)
None __init__(self, str flavour="TinyTeX-1", Union[str, Path] install_dir_name=DOWNLOAD_PATH, *, int timeout_seconds=300)
None _download_file(self, str url, str dest_path)