"""Record the CI toolchain and verify the requested TeX Live year. Copyright (C) 2026 Junhao Wu. LPPL 1.3c or later; see LICENSE. """ import argparse import hashlib from importlib import metadata import json import os from pathlib import Path import platform import re import shutil import subprocess ROOT = Path(__file__).resolve().parents[1] def verify_year(versions, expected): years = set() for value in versions: match = re.search(r'TeX Live (\d{4})', value) if not match: raise ValueError(f'Missing TeX Live year in engine banner: {value}') years.add(match.group(1)) if len(years) != 1 or (expected != 'latest' and years != {expected}): raise ValueError(f'TeX Live year mismatch: requested {expected}, found {sorted(years)}') return years.pop() def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--expected-year', required=True) parser.add_argument('--output', type=Path, required=True) args = parser.parse_args() engines = {name: subprocess.check_output([name, '--version'], text=True).splitlines()[0] for name in ('lualatex', 'xelatex', 'pdflatex', 'xetex')} year = verify_year(engines.values(), args.expected_year) probe = args.output.parent / 'ci-toolchain-probe' probe.mkdir(parents=True, exist_ok=True) probe_source = r'''\documentclass{article} \begin{document} \typeout{CI-KERNEL:\fmtversion} \typeout{CI-PROPERTY-API:\ifdefined\RecordProperties true\else false\fi} Toolchain probe. \end{document} ''' formats = {} # Warm each format serially before parallel test jobs. Historic images may # build missing formats on demand; concurrent first runs can race. for engine in ('lualatex', 'xelatex', 'pdflatex'): folder = probe / engine folder.mkdir(exist_ok=True) (folder / 'probe.tex').write_text(probe_source, encoding='utf-8') with (folder / 'compile.log').open('w', encoding='utf-8') as stream: subprocess.run([engine, '-no-shell-escape', '-interaction=nonstopmode', '-halt-on-error', 'probe.tex'], cwd=folder, stdout=stream, stderr=subprocess.STDOUT, check=True, timeout=120) log = (folder / 'probe.log').read_text(errors='replace') formats[engine] = dict(latex_kernel=re.search(r'CI-KERNEL:(\S+)', log).group(1), expl3_date=re.search(r'L3 programming layer <([^>]+)>', log).group(1), record_properties=bool(re.search(r'CI-PROPERTY-API:\s*true', log))) if len({item['latex_kernel'] for item in formats.values()}) != 1: raise ValueError('The three engines use different LaTeX kernel dates') kernel, expl3, property_api = (formats['pdflatex'][key] for key in ('latex_kernel', 'expl3_date', 'record_properties')) tools = {} for name in ('latexmk', 'biber', 'latexminted'): executable = shutil.which(name) if executable: result = subprocess.run([executable, '--version'], capture_output=True, text=True, timeout=30) tools[name] = {'path': executable, 'exit_code': result.returncode, 'version': (result.stdout + result.stderr).strip()} else: tools[name] = {'path': None} record = dict(texlive_year=year, requested_year=args.expected_year, latex_kernel=kernel, expl3_date=expl3, record_properties=property_api, formats=formats, image=os.environ.get('CI_TEXLIVE_IMAGE'), revision=os.environ.get('GITHUB_SHA'), platform=platform.platform(), python=platform.python_version(), engines=engines, tools=tools, libraries={name: metadata.version(name) for name in ('pypdf', 'pypdfium2', 'PyYAML')}, source_sha256=hashlib.sha256((ROOT / 'wiseref.dtx').read_bytes()).hexdigest()) args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(record, indent=2) + '\n', encoding='utf-8') print(f'TeX Live {year}: toolchain recorded in {args.output}') if __name__ == '__main__': main()