From 5ca8d2c95bafc7f3257d9a1ded4db0824e43e2b4 Mon Sep 17 00:00:00 2001 From: JessicaTegner Date: Sun, 30 Aug 2026 18:23:05 +0200 Subject: [PATCH 1/2] Fix Missing highest variant + new file format upstream --- .gitignore | 2 + pytinytex/cli.py | 13 ++-- pytinytex/tinytex_download.py | 114 ++++++++++++++++++++++----------- readme.md | 13 ++-- tests/test_tinytex_download.py | 114 +++++++++++++++++++++++++++++++++ tests/utils.py | 2 +- 6 files changed, 206 insertions(+), 52 deletions(-) diff --git a/.gitignore b/.gitignore index 4da4b95..c0a7d9d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ *.zip *.tar.gz *.tgz +*.tar.xz +*.exe tinytex/* diff --git a/pytinytex/cli.py b/pytinytex/cli.py index 41bfbf8..3ccfefc 100644 --- a/pytinytex/cli.py +++ b/pytinytex/cli.py @@ -96,13 +96,14 @@ def main(argv=None): p_download = sub.add_parser("download", help="Download TinyTeX") p_download.add_argument( "--variation", - type=int, - default=1, - choices=[0, 1, 2], - help="TinyTeX variation (default: 1)", + default="1", + choices=["0", "1", "2", "none"], + help="TinyTeX variation: 0, 1, 2 or none (default: 1)", ) p_download.add_argument( - "--version", default="latest", help="TinyTeX version (default: latest)" + "--version", + default="latest", + help="TinyTeX version: latest, daily or year.month (default: latest)", ) # uninstall @@ -189,7 +190,7 @@ def main(argv=None): elif args.command == "download": pytinytex.download_tinytex( version=getattr(args, "version", "latest"), - variation=args.variation, + variation=None if args.variation == "none" else int(args.variation), ) print("Done.") diff --git a/pytinytex/tinytex_download.py b/pytinytex/tinytex_download.py index 6925997..0ff14e3 100644 --- a/pytinytex/tinytex_download.py +++ b/pytinytex/tinytex_download.py @@ -2,6 +2,7 @@ import platform import re import shutil +import subprocess import sys import tarfile import tempfile @@ -41,24 +42,30 @@ def download_tinytex( download_folder=None, progress_callback=None, ): - if variation not in [0, 1, 2]: + if variation not in [None, 0, 1, 2]: raise RuntimeError( - "Invalid TinyTeX variation {}. Valid variations are 0, 1, 2.".format( + "Invalid TinyTeX variation {}. Valid variations are None, 0, 1, 2.".format( variation ) ) - if re.match(r"\d{4}\.\d{2}", version) or version == "latest": - if version != "latest": - version = "v" + version - else: + if version == "latest" and variation == 2: + logger.info("TinyTeX-2 is only published in the daily release, using it.") + version = "daily" + if re.match(r"\d{4}\.\d{2}", version): + if variation == 2: + raise RuntimeError( + "TinyTeX variation 2 is only available with version='daily'." + ) + version = "v" + version + elif version not in ("latest", "daily"): raise RuntimeError( "Invalid TinyTeX version {}. TinyTeX version has to be in the format " - "'latest' for the latest available version, or year.month, for example: " - "'2024.12', '2024.09' for a specific version.".format(version) + "'latest' for the latest available version, 'daily' for the daily " + "build, or year.month, for example: '2024.12', '2024.09' for a " + "specific version.".format(version) ) if progress_callback is None and sys.stdout.isatty(): progress_callback = _default_progress - variation = str(variation) pf = sys.platform if pf.startswith("linux"): pf = "linux" @@ -103,18 +110,25 @@ def download_tinytex( logger.info("Extracting %s to a temporary folder...", filename) with tempfile.TemporaryDirectory() as tmpdirname: tmpdirname = Path(tmpdirname) - extracted_dir_name = "TinyTeX" # for Windows and macOS if filename.suffix == ".zip": with zipfile.ZipFile(filename) as zf: zf.extractall(tmpdirname) - elif filename.suffix in (".tgz", ".gz"): - with tarfile.open(filename, "r:gz") as tf: + elif filename.suffix == ".exe": + # 7-Zip self-extracting archive (Windows) + subprocess.run( + [str(filename), "-y", "-o" + str(tmpdirname)], + check=True, + capture_output=True, + ) + elif filename.suffix in (".tgz", ".gz", ".xz"): + with tarfile.open(filename, "r:*") as tf: tf.extractall(tmpdirname) - if filename.suffix == ".gz": - extracted_dir_name = ".TinyTeX" # linux only else: raise RuntimeError("File {0} not supported".format(filename)) - tinytex_extracted = tmpdirname / extracted_dir_name + # archives contain a single "TinyTeX" (or ".TinyTeX" on Linux) folder + tinytex_extracted = next( + p for p in tmpdirname.iterdir() if p.name.lstrip(".") == "TinyTeX" + ) logger.info("Copying TinyTeX to %s...", target_folder) shutil.copytree(tinytex_extracted, target_folder, dirs_exist_ok=True) # Resolve the path and add to PATH so everything is ready to use @@ -142,30 +156,52 @@ def _get_tinytex_urls(version, variation): ) content = response.read() regex = re.compile( - r"/rstudio/tinytex-releases/releases/download/.*TinyTeX\-.*.(?:tar\.gz|tgz|zip)" + r"/rstudio/tinytex-releases/releases/download/[^\"]*TinyTeX[^\"]*" + r"\.(?:tar\.gz|tar\.xz|tgz|zip|exe)" ) tinytex_urls_list = regex.findall(content.decode("utf-8")) - ext2platform = {"zip": "win32", ".gz": "linux", "tgz": "darwin"} - if variation in ("0", "1"): - variation_txt = "TinyTeX-{}-".format(variation) - else: - variation_txt = "TinyTeX-v" - tinytex_urls_list = { - url_frag for url_frag in tinytex_urls_list if variation_txt in url_frag - } - # Filter Linux tar.gz URLs by architecture: TinyTeX releases include - # separate arm64 tarballs (e.g. "TinyTeX-0-arm64-v2026.03.02.tar.gz") - # alongside x86_64 ones. Both end in .tar.gz and would map to the same - # "linux" dict key. Only apply this filter to .tar.gz (Linux) URLs — - # macOS .tgz and Windows .zip are unaffected. - arm64 = _is_arm64() - tinytex_urls_list = { - url_frag - for url_frag in tinytex_urls_list - if not url_frag.endswith(".tar.gz") or arm64 == ("arm64" in url_frag) - } - tinytex_urls = { - ext2platform[url_frag[-3:]]: ("https://github.com" + url_frag) - for url_frag in tinytex_urls_list - } + tinytex_urls = _select_tinytex_urls(tinytex_urls_list, variation, _is_arm64()) return tinytex_urls, version + + +_ASSET_RE = re.compile( + r"^TinyTeX(?:-([012]))?" + r"(?:-(darwin|linux-x86_64|linux-arm64|linuxmusl-x86_64|windows))?" + r"(-arm64)?(?:-v[\d.]+)?\.(tar\.xz|exe|tar\.gz|tgz|zip)$" +) + + +def _select_tinytex_urls(asset_paths, variation, arm64): + """Map release asset paths to {sys.platform: url} for the given variation. + + Handles both the naming used up to v2026.03.02 + (``TinyTeX-1[-arm64]-v2026.03.tar.gz`` / ``.tgz`` / ``.zip``) and the + naming used since (``TinyTeX-1-linux-arm64-v2026.04.tar.xz`` / + ``TinyTeX-1-windows-v2026.04.exe``). Legacy archives win when both exist. + """ + # ponytail: no musl detection, glibc x86_64 asset is always picked on Linux + new_style, old_style = {}, {} + for path in asset_paths: + m = _ASSET_RE.match(path.split("/")[-1]) + if not m: + continue + num, os_name, arm_suffix, ext = m.groups() + if (int(num) if num else None) != variation: + continue + url = "https://github.com" + path + if os_name == "darwin": + new_style["darwin"] = url + elif os_name == "windows": + new_style["win32"] = url + elif os_name == "linux-arm64" and arm64: + new_style["linux"] = url + elif os_name == "linux-x86_64" and not arm64: + new_style["linux"] = url + elif os_name is None: + if ext == "zip": + old_style["win32"] = url + elif ext == "tgz": + old_style["darwin"] = url + elif ext == "tar.gz" and arm64 == bool(arm_suffix): + old_style["linux"] = url + return {**new_style, **old_style} diff --git a/readme.md b/readme.md index a7cb099..9b54002 100644 --- a/readme.md +++ b/readme.md @@ -41,11 +41,12 @@ import pytinytex # Download the default variation (variation 1: ~90 common LaTeX packages) pytinytex.download_tinytex() -# Or pick a variation: -# 0 — infrastructure only, no packages -# 1 — common packages (default) -# 2 — extended package set -pytinytex.download_tinytex(variation=2) +# Or pick a variation (numbers match the TinyTeX release names): +# 0 — TinyTeX-0: infrastructure only, no packages +# 1 — TinyTeX-1: common packages (default) +# None — TinyTeX: community-requested packages +# 2 — TinyTeX-2: full TeX Live (~1.7 GB, daily release only) +pytinytex.download_tinytex(variation=None) # Track download progress pytinytex.download_tinytex(progress_callback=lambda downloaded, total: print(f"{downloaded}/{total} bytes")) @@ -167,7 +168,7 @@ Every feature is also available from the terminal: ```bash # Download TinyTeX pytinytex download -pytinytex download --variation 2 +pytinytex download --variation none # Compile a document pytinytex compile paper.tex diff --git a/tests/test_tinytex_download.py b/tests/test_tinytex_download.py index 10bdd16..fae4cf7 100644 --- a/tests/test_tinytex_download.py +++ b/tests/test_tinytex_download.py @@ -39,3 +39,117 @@ def test_failing_download_invalid_variation(): def test_failing_download_invalid_version(): with pytest.raises(RuntimeError, match="Invalid TinyTeX version invalid."): pytinytex.download_tinytex(version="invalid") + + +# Asset names copied from real releases of rstudio/tinytex-releases. +_ASSETS_2026_08 = [ + "/rstudio/tinytex-releases/releases/download/v2026.08/" + n + for n in ( + "TinyTeX-0-darwin-v2026.08.tar.xz", + "TinyTeX-0-linux-arm64-v2026.08.tar.xz", + "TinyTeX-0-linux-x86_64-v2026.08.tar.xz", + "TinyTeX-0-linuxmusl-x86_64-v2026.08.tar.xz", + "TinyTeX-0-windows-v2026.08.exe", + "TinyTeX-1-darwin-v2026.08.tar.xz", + "TinyTeX-1-linux-arm64-v2026.08.tar.xz", + "TinyTeX-1-linux-x86_64-v2026.08.tar.xz", + "TinyTeX-1-linuxmusl-x86_64-v2026.08.tar.xz", + "TinyTeX-1-tar-v2026.08.gz", + "TinyTeX-1-windows-v2026.08.exe", + "TinyTeX-darwin-v2026.08.tar.xz", + "TinyTeX-linux-arm64-v2026.08.tar.xz", + "TinyTeX-linux-x86_64-v2026.08.tar.xz", + "TinyTeX-linuxmusl-x86_64-v2026.08.tar.xz", + "TinyTeX-v2026.08.tar.gz", + "TinyTeX-v2026.08.tgz", + "TinyTeX-v2026.08.zip", + "TinyTeX-windows-v2026.08.exe", + "installer-unix-v2026.08.tar.gz", + ) +] +_ASSETS_DAILY = [ + "/rstudio/tinytex-releases/releases/download/daily/" + n + for n in ( + "TinyTeX-2-darwin.tar.xz", + "TinyTeX-2-linux-arm64.tar.xz", + "TinyTeX-2-linux-x86_64.tar.xz", + "TinyTeX-2-linuxmusl-x86_64.tar.xz", + "TinyTeX-2-windows.exe", + "TinyTeX-linux-x86_64.tar.xz", + "TinyTeX.tar.gz", + "TinyTeX.tgz", + "TinyTeX.zip", + ) +] +_ASSETS_2026_03 = [ + "/rstudio/tinytex-releases/releases/download/v2026.03.02/" + n + for n in ( + "TinyTeX-0-arm64-v2026.03.02.tar.gz", + "TinyTeX-0-v2026.03.02.tar.gz", + "TinyTeX-0-v2026.03.02.tgz", + "TinyTeX-0-v2026.03.02.zip", + "TinyTeX-arm64-v2026.03.02.tar.gz", + "TinyTeX-v2026.03.02.tar.gz", + "tinitex.zip", + ) +] + + +def _names(urls): + return {k: v.rsplit("/", 1)[1] for k, v in urls.items()} + + +def test_select_urls_new_naming(): + sel = pytinytex.tinytex_download._select_tinytex_urls + assert _names(sel(_ASSETS_2026_08, 0, arm64=False)) == { + "darwin": "TinyTeX-0-darwin-v2026.08.tar.xz", + "linux": "TinyTeX-0-linux-x86_64-v2026.08.tar.xz", + "win32": "TinyTeX-0-windows-v2026.08.exe", + } + assert _names(sel(_ASSETS_2026_08, 1, arm64=True))["linux"] == ( + "TinyTeX-1-linux-arm64-v2026.08.tar.xz" + ) + # community bundle: legacy archives preferred, arm64 only has tar.xz + assert _names(sel(_ASSETS_2026_08, None, arm64=False)) == { + "darwin": "TinyTeX-v2026.08.tgz", + "linux": "TinyTeX-v2026.08.tar.gz", + "win32": "TinyTeX-v2026.08.zip", + } + assert _names(sel(_ASSETS_2026_08, None, arm64=True))["linux"] == ( + "TinyTeX-linux-arm64-v2026.08.tar.xz" + ) + assert sel(_ASSETS_2026_08, 2, arm64=False) == {} + + +def test_select_urls_daily_variation_2(): + sel = pytinytex.tinytex_download._select_tinytex_urls + assert _names(sel(_ASSETS_DAILY, 2, arm64=True)) == { + "darwin": "TinyTeX-2-darwin.tar.xz", + "linux": "TinyTeX-2-linux-arm64.tar.xz", + "win32": "TinyTeX-2-windows.exe", + } + assert _names(sel(_ASSETS_DAILY, None, arm64=False)) == { + "darwin": "TinyTeX.tgz", + "linux": "TinyTeX.tar.gz", + "win32": "TinyTeX.zip", + } + + +def test_select_urls_old_naming(): + sel = pytinytex.tinytex_download._select_tinytex_urls + assert _names(sel(_ASSETS_2026_03, 0, arm64=False)) == { + "darwin": "TinyTeX-0-v2026.03.02.tgz", + "linux": "TinyTeX-0-v2026.03.02.tar.gz", + "win32": "TinyTeX-0-v2026.03.02.zip", + } + assert _names(sel(_ASSETS_2026_03, 0, arm64=True))["linux"] == ( + "TinyTeX-0-arm64-v2026.03.02.tar.gz" + ) + assert _names(sel(_ASSETS_2026_03, None, arm64=True)) == { + "linux": "TinyTeX-arm64-v2026.03.02.tar.gz" + } + + +def test_failing_download_variation_2_with_pinned_version(): + with pytest.raises(RuntimeError, match="only available with version='daily'"): + pytinytex.download_tinytex(variation=2, version="2024.12") diff --git a/tests/utils.py b/tests/utils.py index 89ee575..745b37d 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -24,6 +24,6 @@ def cleanup(): if os.path.isdir(TINYTEX_DISTRIBUTION): shutil.rmtree(TINYTEX_DISTRIBUTION) for item in os.listdir("tests"): - if item.endswith(".zip") or item.endswith(".tar.gz") or item.endswith(".tgz"): + if item.endswith((".zip", ".tar.gz", ".tgz", ".tar.xz", ".exe")): os.remove(os.path.join("tests", item)) pytinytex.clear_path_cache() From cd5a61181edadd3f2b53763429640927fd13f6d1 Mon Sep 17 00:00:00 2001 From: JessicaTegner Date: Tue, 1 Sep 2026 20:33:59 +0200 Subject: [PATCH 2/2] Download musl TinyTeX build on musl-based Linux Detect musl via /lib/ld-musl-*.so.1 and pick the linuxmusl-x86_64 asset when present. Fall back to the glibc build for releases and architectures without a musl build (pre-2026.04, arm64). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WAb58ELQJMZVjuDWQYEyV7 --- pytinytex/tinytex_download.py | 22 +++++++++++++++++----- tests/test_tinytex_download.py | 19 +++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/pytinytex/tinytex_download.py b/pytinytex/tinytex_download.py index 0ff14e3..0c2f2ae 100644 --- a/pytinytex/tinytex_download.py +++ b/pytinytex/tinytex_download.py @@ -1,3 +1,4 @@ +import glob import logging import platform import re @@ -21,6 +22,11 @@ def _is_arm64(): return platform.machine().lower() in ("aarch64", "arm64") +def _is_musl(): + """Return True if running on a musl-based Linux (e.g. Alpine).""" + return bool(glob.glob("/lib/ld-musl-*.so.1")) + + def _default_progress(downloaded, total): """Print download progress on a TTY.""" if total > 0: @@ -160,7 +166,9 @@ def _get_tinytex_urls(version, variation): r"\.(?:tar\.gz|tar\.xz|tgz|zip|exe)" ) tinytex_urls_list = regex.findall(content.decode("utf-8")) - tinytex_urls = _select_tinytex_urls(tinytex_urls_list, variation, _is_arm64()) + tinytex_urls = _select_tinytex_urls( + tinytex_urls_list, variation, _is_arm64(), _is_musl() + ) return tinytex_urls, version @@ -171,15 +179,15 @@ def _get_tinytex_urls(version, variation): ) -def _select_tinytex_urls(asset_paths, variation, arm64): +def _select_tinytex_urls(asset_paths, variation, arm64, musl=False): """Map release asset paths to {sys.platform: url} for the given variation. Handles both the naming used up to v2026.03.02 (``TinyTeX-1[-arm64]-v2026.03.tar.gz`` / ``.tgz`` / ``.zip``) and the naming used since (``TinyTeX-1-linux-arm64-v2026.04.tar.xz`` / - ``TinyTeX-1-windows-v2026.04.exe``). Legacy archives win when both exist. + ``TinyTeX-1-windows-v2026.04.exe``). Legacy archives win when both exist, + except on musl where the dedicated musl build (new naming only) is used. """ - # ponytail: no musl detection, glibc x86_64 asset is always picked on Linux new_style, old_style = {}, {} for path in asset_paths: m = _ASSET_RE.match(path.split("/")[-1]) @@ -195,7 +203,9 @@ def _select_tinytex_urls(asset_paths, variation, arm64): new_style["win32"] = url elif os_name == "linux-arm64" and arm64: new_style["linux"] = url - elif os_name == "linux-x86_64" and not arm64: + elif os_name == "linux-x86_64" and not arm64 and not musl: + new_style["linux"] = url + elif os_name == "linuxmusl-x86_64" and not arm64 and musl: new_style["linux"] = url elif os_name is None: if ext == "zip": @@ -204,4 +214,6 @@ def _select_tinytex_urls(asset_paths, variation, arm64): old_style["darwin"] = url elif ext == "tar.gz" and arm64 == bool(arm_suffix): old_style["linux"] = url + if musl and "linux" in new_style: + old_style.pop("linux", None) # legacy tarballs are glibc-only return {**new_style, **old_style} diff --git a/tests/test_tinytex_download.py b/tests/test_tinytex_download.py index fae4cf7..86165e9 100644 --- a/tests/test_tinytex_download.py +++ b/tests/test_tinytex_download.py @@ -121,6 +121,25 @@ def test_select_urls_new_naming(): assert sel(_ASSETS_2026_08, 2, arm64=False) == {} +def test_select_urls_musl(): + sel = pytinytex.tinytex_download._select_tinytex_urls + assert _names(sel(_ASSETS_2026_08, 1, arm64=False, musl=True))["linux"] == ( + "TinyTeX-1-linuxmusl-x86_64-v2026.08.tar.xz" + ) + # legacy glibc tarball must not override the musl build + assert _names(sel(_ASSETS_2026_08, None, arm64=False, musl=True))["linux"] == ( + "TinyTeX-linuxmusl-x86_64-v2026.08.tar.xz" + ) + # no musl arm64 build upstream: fall back to glibc arm64 + assert _names(sel(_ASSETS_2026_08, 1, arm64=True, musl=True))["linux"] == ( + "TinyTeX-1-linux-arm64-v2026.08.tar.xz" + ) + # old releases have no musl build: fall back to glibc + assert _names(sel(_ASSETS_2026_03, 0, arm64=False, musl=True))["linux"] == ( + "TinyTeX-0-v2026.03.02.tar.gz" + ) + + def test_select_urls_daily_variation_2(): sel = pytinytex.tinytex_download._select_tinytex_urls assert _names(sel(_ASSETS_DAILY, 2, arm64=True)) == {