from __future__ import annotations
import argparse
import lzma
import sys
import urllib.request
from pathlib import Path
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
PACKAGE_FILE = DATA_DIR / "package_names.txt"
CLASS_FILE = DATA_DIR / "class_names.txt"
TL_YEAR = "2025"
TLPDB_URL = (
"https://ftp.math.utah.edu/pub/tex/historic/systems/texlive/"
f"{TL_YEAR}/tlnet-final/tlpkg/texlive.tlpdb.xz"
)
SEP = "---"
COMMON_PACKAGES = frozenset(
(
"tikz pgf pgfplots inputenc fontenc amssymb amsfonts amsthm amsopn"
" graphicx graphics xcolor color babel microtype fontspec csquotes"
" geometry fancyhdr hyperref url cleveref nameref varioref booktabs"
" array tabularx longtable multirow multicol makecell colortbl enumitem"
" caption subcaption float wrapfig rotating listings minted fancyvrb"
" verbatim tcolorbox mdframed framed siunitx natbib biblatex todonotes"
" soul ulem etoolbox xparse xspace ifthen calc environ setspace parskip"
" titlesec titletoc appendix glossaries acronym nomencl makeidx imakeidx"
" pdfpages standalone import subfiles lipsum blindtext mhchem chemfig"
" datetime2 hyperref bm mathtools esint mathrsfs cancel physics amsmath"
).split()
)
COMMON_CLASSES = frozenset(
(
"article report book letter slides proc minimal beamer memoir standalone"
" extarticle extreport extbook amsart amsbook scrartcl scrreprt scrbook"
" scrlttr2 tufte-handout tufte-book moderncv acmart IEEEtran elsarticle"
" revtex4-2 llncs subfiles"
).split()
)
def parse_tlpdb(text: str) -> tuple[dict[str, str], dict[str, str]]:
packages: dict[str, str] = {}
classes: dict[str, str] = {}
pkg = None
in_runfiles = False
for line in text.splitlines():
if not line:
pkg = None
in_runfiles = False
continue
if line.startswith(" "):
if in_runfiles and pkg is not None:
path = line.strip()
base = path.rsplit("/", 1)[-1]
if base.endswith(".sty"):
packages.setdefault(base[:-4], pkg)
elif base.endswith(".cls"):
classes.setdefault(base[:-4], pkg)
continue
key, _, value = line.partition(" ")
if key == "name":
pkg = value.strip()
in_runfiles = False
elif key == "runfiles":
in_runfiles = True
else:
in_runfiles = False
return packages, classes
def rank(stems: dict[str, str], common: frozenset[str]) -> tuple[list[str], list[str]]:
primary = sorted(s for s, p in stems.items() if s == p or s in common)
primary_set = set(primary)
secondary = sorted(s for s in stems if s not in primary_set)
return primary, secondary
def render(kind: str, primary: list[str], secondary: list[str]) -> str:
header = (
f"# GENERATED by scripts/gen_package_names.py from TeX Live {TL_YEAR} "
f"(tlnet-final texlive.tlpdb) -- do not edit by hand.\n"
f"# {kind} name stems for \\usepackage / \\documentclass completion. Common "
f"and namesake names first, then `{SEP}`, then the long tail.\n"
f"# Run `task pkg-names:sync` to regenerate.\n"
)
body = "\n".join(primary) + f"\n{SEP}\n" + "\n".join(secondary) + "\n"
return header + body
def fetch_tlpdb(source: str | None) -> str:
if source is not None:
raw = Path(source).read_bytes()
else:
try:
with urllib.request.urlopen(TLPDB_URL, timeout=120) as resp:
raw = resp.read()
except OSError as e:
sys.exit(f"error: failed to fetch {TLPDB_URL}: {e}")
if raw[:6] == b"\xfd7zXZ\x00": raw = lzma.decompress(raw)
return raw.decode("utf-8", errors="replace")
def _selftest() -> int:
def eq(got, want, msg):
assert got == want, f"{msg}: got {got!r}, want {want!r}"
sample = (
"name amsmath\n"
"category Package\n"
"runfiles size=42\n"
" texmf-dist/tex/latex/amsmath/amsmath.sty\n"
" texmf-dist/tex/latex/amsmath/amstext.sty\n"
" texmf-dist/doc/latex/amsmath/README\n"
"catalogue-ctan /macros/latex/required/amsmath\n"
"\n"
"name koma-script\n"
"runfiles size=9\n"
" RELOC/tex/latex/koma-script/scrartcl.cls\n"
" RELOC/tex/latex/koma-script/scrbook.cls\n"
"\n"
"name pgf\n"
"runfiles size=1\n"
" texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty\n"
)
pkgs, classes = parse_tlpdb(sample)
eq(pkgs.get("amsmath"), "amsmath", "amsmath stem")
eq(pkgs.get("amstext"), "amsmath", "amstext owned by amsmath")
eq(pkgs.get("tikz"), "pgf", "tikz owned by pgf")
eq("amsmath" in classes, False, "no class from sty")
eq(classes.get("scrartcl"), "koma-script", "scrartcl class stem")
primary, secondary = rank(pkgs, COMMON_PACKAGES)
eq("amsmath" in primary, True, "namesake amsmath primary")
eq("tikz" in primary, True, "common tikz primary")
eq("amstext" in secondary, True, "internal amstext secondary")
cprimary, _ = rank(classes, COMMON_CLASSES)
eq("scrartcl" in cprimary, True, "scrartcl common-class primary")
out = render("Package", primary, secondary)
eq(out.count(f"\n{SEP}\n"), 1, "one separator")
eq(out.startswith("# GENERATED"), True, "header present")
print("selftest: ok")
return 0
def _write_or_check(path: Path, rendered: str, write: bool, label: str) -> int:
n = rendered.count("\n") - rendered.count("\n#") - 1 if write:
path.write_text(rendered)
print(f"wrote {path.relative_to(Path.cwd())} (~{n} {label} names)")
return 0
current = path.read_text() if path.is_file() else ""
if current == rendered:
print(f"{path.name} is in sync (~{n} {label} names)")
return 0
print(f"{path.name} is OUT OF SYNC with TeX Live {TL_YEAR}.", file=sys.stderr)
print("run with --write to regenerate.", file=sys.stderr)
return 1
def main() -> int:
ap = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
ap.add_argument("--write", action="store_true", help="regenerate the lists in place")
ap.add_argument("--source", metavar="FILE", help="local texlive.tlpdb (or .xz)")
ap.add_argument("--selftest", action="store_true", help="run offline unit checks")
args = ap.parse_args()
if args.selftest:
return _selftest()
text = fetch_tlpdb(args.source)
pkgs, classes = parse_tlpdb(text)
if not pkgs or not classes:
sys.exit("error: parsed no package/class stems -- is this a full tlpdb?")
p_primary, p_secondary = rank(pkgs, COMMON_PACKAGES)
c_primary, c_secondary = rank(classes, COMMON_CLASSES)
rc = _write_or_check(
PACKAGE_FILE, render("Package", p_primary, p_secondary), args.write, "package"
)
rc |= _write_or_check(
CLASS_FILE, render("Class", c_primary, c_secondary), args.write, "class"
)
return rc
if __name__ == "__main__":
raise SystemExit(main())