pytex-preprocessor 1.0.3__tar.gz → 1.0.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/PKG-INFO +1 -1
  2. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/pyproject.toml +1 -1
  3. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_api/_compile.py +48 -1
  4. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_builder/tectonic.py +78 -47
  5. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_preprocessor.egg-info/PKG-INFO +1 -1
  6. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/LICENSE +0 -0
  7. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/README.md +0 -0
  8. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/setup.cfg +0 -0
  9. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/__init__.py +0 -0
  10. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/__init__.py +0 -0
  11. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/biblatex.py +0 -0
  12. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/builtin.py +0 -0
  13. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/captions.py +0 -0
  14. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/cleveref.py +0 -0
  15. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/colors.py +0 -0
  16. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/conditionals.py +0 -0
  17. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/counters.py +0 -0
  18. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/definitions.py +0 -0
  19. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/floats.py +0 -0
  20. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/font.py +0 -0
  21. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/fontawesome.py +0 -0
  22. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/fontspec.py +0 -0
  23. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/geometry.py +0 -0
  24. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/glossaries.py +0 -0
  25. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/graphics.py +0 -0
  26. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/hooks.py +0 -0
  27. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/hyperref.py +0 -0
  28. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/lengths.py +0 -0
  29. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/listings.py +0 -0
  30. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/mdframed.py +0 -0
  31. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/picture.py +0 -0
  32. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/setspace.py +0 -0
  33. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/commands/tables.py +0 -0
  34. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/helpers/__init__.py +0 -0
  35. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/helpers/coerce.py +0 -0
  36. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/helpers/parenting.py +0 -0
  37. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/helpers/sanitize.py +0 -0
  38. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/helpers/with_package.py +0 -0
  39. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/interface/__init__.py +0 -0
  40. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/interface/control_sequence.py +0 -0
  41. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/interface/package.py +0 -0
  42. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/interface/tex.py +0 -0
  43. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/model/__init__.py +0 -0
  44. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/model/color.py +0 -0
  45. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/model/comment.py +0 -0
  46. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/model/concat.py +0 -0
  47. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/model/control_sequence.py +0 -0
  48. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/model/document.py +0 -0
  49. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/model/document_class.py +0 -0
  50. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/model/empty.py +0 -0
  51. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/model/environment.py +0 -0
  52. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/model/image.py +0 -0
  53. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/model/include.py +0 -0
  54. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/model/length.py +0 -0
  55. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/model/math.py +0 -0
  56. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/model/package.py +0 -0
  57. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/model/raw.py +0 -0
  58. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/packages.py +0 -0
  59. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/registry.py +0 -0
  60. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex/template.py +0 -0
  61. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_analyze/__init__.py +0 -0
  62. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_analyze/analyze.py +0 -0
  63. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_analyze/optimize.py +0 -0
  64. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_api/__init__.py +0 -0
  65. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_api/_models.py +0 -0
  66. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_api/_policy.py +0 -0
  67. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_api/_render.py +0 -0
  68. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_api/_sandbox.py +0 -0
  69. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_api/_security.py +0 -0
  70. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_api/sandbox_init.py +0 -0
  71. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_builder/__init__.py +0 -0
  72. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_builder/build.py +0 -0
  73. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_builder/console.py +0 -0
  74. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_builder/render.py +0 -0
  75. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_builder/tex2py.py +0 -0
  76. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_builder/tree.py +0 -0
  77. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_builder/variants.py +0 -0
  78. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_components/__init__.py +0 -0
  79. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_components/boxes.py +0 -0
  80. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_components/citations.py +0 -0
  81. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_components/cleveref_names.py +0 -0
  82. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_components/pagebreak.py +0 -0
  83. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_components/voting.py +0 -0
  84. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_components/watermark.py +0 -0
  85. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_components/wordcount.py +0 -0
  86. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/__init__.py +0 -0
  87. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Bold.ttf +0 -0
  88. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-BoldItalic.ttf +0 -0
  89. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Book.ttf +0 -0
  90. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-BookItalic.ttf +0 -0
  91. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Medium.ttf +0 -0
  92. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-MediumItalic.ttf +0 -0
  93. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Strong.ttf +0 -0
  94. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Thin.ttf +0 -0
  95. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-ThinItalic.ttf +0 -0
  96. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Black.ttf +0 -0
  97. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Bold.ttf +0 -0
  98. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-BoldItalic.ttf +0 -0
  99. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Italic.ttf +0 -0
  100. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Medium.ttf +0 -0
  101. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Regular.ttf +0 -0
  102. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/fonts/Times New Roman.ttf +0 -0
  103. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/logos/ASTA.svg +0 -0
  104. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/logos/DUMMY.png +0 -0
  105. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/logos/DUMMY_FOOT.png +0 -0
  106. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/logos/ECHO.svg +0 -0
  107. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/logos/HSRT.pdf +0 -0
  108. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/logos/INF.pdf +0 -0
  109. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/logos/MAKERS-Icon.svg +0 -0
  110. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/logos/MAKERS-RAlign.svg +0 -0
  111. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/logos/MAKERS.svg +0 -0
  112. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/logos/STUPA.pdf +0 -0
  113. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/assets/logos/Skyline.pdf +0 -0
  114. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/colors.py +0 -0
  115. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/document.py +0 -0
  116. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/fonts.py +0 -0
  117. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/glossary.py +0 -0
  118. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/hyperref_config.py +0 -0
  119. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/listings.py +0 -0
  120. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/logos.py +0 -0
  121. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/pagesetup.py +0 -0
  122. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/tex/pagesetup.tex +0 -0
  123. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/titlepage.py +0 -0
  124. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_hsrtreport/variants.py +0 -0
  125. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_koma/__init__.py +0 -0
  126. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_koma/commands.py +0 -0
  127. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_koma/document.py +0 -0
  128. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_markdown/__init__.py +0 -0
  129. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_markdown/convert.py +0 -0
  130. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_markdown/escape.py +0 -0
  131. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_markdown/frontmatter.py +0 -0
  132. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_markdown/glyphs.py +0 -0
  133. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_markdown/protocol/__init__.py +0 -0
  134. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_markdown/protocol/convert.py +0 -0
  135. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_markdown/protocol/document.py +0 -0
  136. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_markdown/protocol/entries.py +0 -0
  137. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_markdown/protocol/header.py +0 -0
  138. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_markdown/protocol/shortcodes.py +0 -0
  139. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_markdown/protocol/signatures.py +0 -0
  140. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_preprocessor.egg-info/SOURCES.txt +0 -0
  141. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_preprocessor.egg-info/dependency_links.txt +0 -0
  142. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_preprocessor.egg-info/entry_points.txt +0 -0
  143. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_preprocessor.egg-info/requires.txt +0 -0
  144. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_preprocessor.egg-info/top_level.txt +0 -0
  145. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_protocol/__init__.py +0 -0
  146. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_protocol/frontmatter.py +0 -0
  147. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_tikz/__init__.py +0 -0
  148. {pytex_preprocessor-1.0.3 → pytex_preprocessor-1.0.6}/src/pytex_tikz/tikz.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytex-preprocessor
3
- Version: 1.0.3
3
+ Version: 1.0.6
4
4
  Summary: Type-safe LaTeX document generation with Python
5
5
  Author-email: Frederik Beimgraben <frederik@beimgraben.net>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pytex-preprocessor"
7
- version = "1.0.3"
7
+ version = "1.0.6"
8
8
  authors = [
9
9
  { name="Frederik Beimgraben", email="frederik@beimgraben.net" },
10
10
  ]
@@ -102,18 +102,55 @@ def _kill_group(proc: subprocess.Popen[str]) -> None: # pragma: no cover - timi
102
102
  proc.kill()
103
103
 
104
104
 
105
+ def _biber_env(
106
+ cmd: list[str], build_dir: Path, policy: TrustPolicy, console: Console
107
+ ) -> dict[str, str] | None:
108
+ """A child env whose PATH includes the biber matching the document's BCF.
109
+
110
+ Mirrors ``pytex_builder.tectonic.run_tectonic``: read the BCF (probing with a
111
+ no-op biber if it is not written yet) to pick the right biber release, then
112
+ ensure it (download+cache). Returns ``None`` (inherit env) for documents
113
+ without biblatex, or when biber can't be obtained (e.g. network disabled) —
114
+ tectonic then surfaces its own error rather than us masking it.
115
+ """
116
+ tex = build_dir.parent / f"{_JOB}.tex"
117
+ try:
118
+ source = tex.read_text(encoding="utf-8", errors="ignore")
119
+ except OSError:
120
+ return None
121
+ # No biblatex → tectonic never invokes biber; skip the extra probe pass.
122
+ if "biblatex" not in source:
123
+ return None
124
+ try:
125
+ from pytex_builder.tectonic import biber_for_build, env_with_biber, probe_bcf
126
+ except Exception: # pragma: no cover - import guard
127
+ return None
128
+ job = _JOB
129
+ try:
130
+ biber = biber_for_build(build_dir, job, console)
131
+ if biber is None and policy.allow_network:
132
+ probe_bcf(cmd)
133
+ biber = biber_for_build(build_dir, job, console)
134
+ except Exception as exc: # pragma: no cover - infra
135
+ console.warn(f"biber setup skipped: {exc}")
136
+ return None
137
+ return env_with_biber(biber) if biber is not None else None
138
+
139
+
105
140
  def _run_confined(
106
141
  cmd: list[str],
107
142
  cwd: Path,
108
143
  limits: BuildLimits,
109
144
  *,
110
145
  apply_rlimits: bool,
146
+ env: Mapping[str, str] | None = None,
111
147
  ) -> tuple[int, str]:
112
148
  """Run ``cmd`` with rlimits + a hard wall-clock kill; return (rc, output).
113
149
 
114
150
  ``apply_rlimits`` is the in-process ``setrlimit`` floor; it is switched off
115
151
  for the Podman path, where the container's cgroup flags do the capping (an
116
152
  rlimit on the ``podman`` *client* would not reach the build inside).
153
+ ``env`` overrides the child environment (e.g. a PATH that includes biber).
117
154
  """
118
155
  posix = os.name == "posix"
119
156
  preexec = make_rlimit_preexec(limits) if (apply_rlimits and posix) else None
@@ -126,6 +163,7 @@ def _run_confined(
126
163
  text=True,
127
164
  start_new_session=posix,
128
165
  preexec_fn=preexec,
166
+ env=dict(env) if env is not None else None,
129
167
  )
130
168
  except OSError as exc:
131
169
  raise CompileError(f"could not start tectonic: {exc}") from exc
@@ -256,8 +294,17 @@ def compile_to_pdf(
256
294
  )
257
295
  binary = _locate_tectonic(policy, console)
258
296
  cmd = build_tectonic_cmd(binary, tex_file, build_dir, policy)
297
+ # biblatex docs (report/protocol) make tectonic shell out to ``biber``;
298
+ # it is not bundled, so provide a version-matched one on PATH (probe the
299
+ # BCF, then download+cache the matching biber). Without it tectonic dies
300
+ # with "Running external tool biber ... No such file or directory".
301
+ biber_env = _biber_env(cmd, build_dir, policy, console)
259
302
  rc, out = _run_confined(
260
- cmd, workdir, req.limits, apply_rlimits=policy.apply_rlimits
303
+ cmd,
304
+ workdir,
305
+ req.limits,
306
+ apply_rlimits=policy.apply_rlimits,
307
+ env=biber_env,
261
308
  )
262
309
 
263
310
  log = truncate_log(out, req.limits)
@@ -21,7 +21,15 @@ import zipfile
21
21
  from pathlib import Path
22
22
  from typing import TYPE_CHECKING
23
23
 
24
- __all__ = ["BuildError", "ensure_tectonic", "run_makeindex", "run_tectonic"]
24
+ __all__ = [
25
+ "BuildError",
26
+ "biber_for_build",
27
+ "ensure_tectonic",
28
+ "env_with_biber",
29
+ "probe_bcf",
30
+ "run_makeindex",
31
+ "run_tectonic",
32
+ ]
25
33
 
26
34
  if TYPE_CHECKING:
27
35
  from .console import Console
@@ -254,26 +262,6 @@ def _biber_cached(version: str) -> Path:
254
262
  return CACHE_DIR / "biber" / version / name
255
263
 
256
264
 
257
- def _biber_sources(version: str) -> list[tuple[str, str | None]]:
258
- """``(url, expected_sha256 or None)`` to try in order.
259
-
260
- For each platform candidate the mirror is tried first, then SourceForge.
261
- """
262
- sources: list[tuple[str, str | None]] = []
263
- for sf_dir, filename, asset in _biber_candidates(version):
264
- sha = BIBER_SHA256.get(asset)
265
- sources.append((BIBER_MIRROR_URL.format(asset=asset), sha))
266
- sources.append(
267
- (
268
- BIBER_RELEASE_URL.format(
269
- version=version, sf_dir=sf_dir, filename=filename
270
- ),
271
- sha,
272
- )
273
- )
274
- return sources
275
-
276
-
277
265
  def _is_biber_member(name: str) -> bool:
278
266
  """Whether an archive member is the biber executable.
279
267
 
@@ -335,11 +323,36 @@ def _download_to(url: str, dest: Path, sha: str | None, console: Console) -> boo
335
323
  return True
336
324
 
337
325
 
326
+ def _biber_runs(binary: Path) -> bool:
327
+ """``True`` if ``binary`` actually executes here (``biber --version`` exits 0).
328
+
329
+ The musl build is offered first (no glibc shared-lib deps) but is *dynamically*
330
+ linked against the musl loader; on a glibc-only host (e.g. Debian-slim) it
331
+ cannot exec at all ("No such file or directory"). Running it is the only
332
+ reliable cross-check, so we verify and fall back to the glibc build."""
333
+ try:
334
+ proc = subprocess.run(
335
+ [str(binary), "--version"], capture_output=True, timeout=30
336
+ )
337
+ except OSError:
338
+ return False
339
+ return proc.returncode == 0
340
+
341
+
338
342
  def _ensure_biber(version: str, console: Console) -> Path:
339
- """Return a path to biber *version*, downloading from the mirror or SourceForge."""
343
+ """Return a path to biber *version*, downloading from the mirror or SourceForge.
344
+
345
+ Each platform candidate (musl first, then glibc) is downloaded, extracted and
346
+ **test-run**; the first one that actually executes here is cached. This makes
347
+ the choice robust on hosts that lack the musl loader or a glibc dependency."""
340
348
  cached = _biber_cached(version)
341
349
  if cached.exists():
342
- return cached
350
+ # Re-validate: a cache poisoned by an earlier release (e.g. a musl binary
351
+ # that cannot exec here) must be replaced, not blindly reused.
352
+ if _biber_runs(cached):
353
+ return cached
354
+ console.detail("cached biber does not execute here; re-downloading")
355
+ cached.unlink(missing_ok=True)
343
356
 
344
357
  if not shutil.which("curl"):
345
358
  raise BuildError(
@@ -349,31 +362,49 @@ def _ensure_biber(version: str, console: Console) -> Path:
349
362
  console.step(f"Downloading biber {version}")
350
363
  cached.parent.mkdir(parents=True, exist_ok=True)
351
364
  tmp = cached.parent / "biber.download"
365
+ cand = cached.parent / "biber.candidate"
366
+ last: str | None = None
352
367
  try:
353
- downloaded = False
354
- for url, sha in _biber_sources(version):
355
- console.detail(f"source: {url}")
356
- if _download_to(url, tmp, sha, console):
357
- downloaded = True
358
- break
359
- if not downloaded:
360
- raise BuildError(
361
- f"failed to download biber {version} from the mirror or SourceForge"
362
- )
363
- cached.write_bytes(_extract_biber_binary(tmp, version))
368
+ for sf_dir, filename, asset in _biber_candidates(version):
369
+ sha = BIBER_SHA256.get(asset)
370
+ urls = [
371
+ BIBER_MIRROR_URL.format(asset=asset),
372
+ BIBER_RELEASE_URL.format(
373
+ version=version, sf_dir=sf_dir, filename=filename
374
+ ),
375
+ ]
376
+ downloaded = False
377
+ for url in urls:
378
+ console.detail(f"source: {url}")
379
+ if _download_to(url, tmp, sha, console):
380
+ downloaded = True
381
+ break
382
+ if not downloaded:
383
+ last = f"{asset}: download failed"
384
+ continue
385
+ try:
386
+ cand.write_bytes(_extract_biber_binary(tmp, version))
387
+ cand.chmod(0o755)
388
+ except Exception as exc:
389
+ last = f"{asset}: extract failed ({exc})"
390
+ continue
391
+ if _biber_runs(cand):
392
+ cand.replace(cached)
393
+ cached.chmod(0o755)
394
+ return cached
395
+ last = f"{asset}: does not execute on this platform"
396
+ cand.unlink(missing_ok=True)
397
+ raise BuildError(f"failed to obtain a working biber {version} ({last})")
364
398
  except Exception:
365
399
  if cached.exists():
366
400
  cached.unlink()
367
401
  raise
368
402
  finally:
369
- if tmp.exists():
370
- tmp.unlink()
371
-
372
- cached.chmod(0o755)
373
- return cached
403
+ tmp.unlink(missing_ok=True)
404
+ cand.unlink(missing_ok=True)
374
405
 
375
406
 
376
- def _biber_for_build(build_dir: Path, job: str, console: Console) -> Path | None:
407
+ def biber_for_build(build_dir: Path, job: str, console: Console) -> Path | None:
377
408
  """Return a correctly-versioned biber if the BCF file reveals a mismatch."""
378
409
  bcf = build_dir / f"{job}.bcf"
379
410
  if not bcf.exists():
@@ -400,13 +431,13 @@ def _biber_for_build(build_dir: Path, job: str, console: Console) -> Path | None
400
431
  return _ensure_biber(biber_ver, console)
401
432
 
402
433
 
403
- def _env_with_biber(biber: Path) -> dict[str, str]:
434
+ def env_with_biber(biber: Path) -> dict[str, str]:
404
435
  env = os.environ.copy()
405
436
  env["PATH"] = str(biber.parent) + os.pathsep + env.get("PATH", "")
406
437
  return env
407
438
 
408
439
 
409
- def _probe_bcf(cmd: list[str]) -> None:
440
+ def probe_bcf(cmd: list[str]) -> None:
410
441
  """Run tectonic with a no-op biber so the BCF file is written to build_dir.
411
442
 
412
443
  Tectonic cleans up intermediates when biber fails, so the BCF is never
@@ -419,7 +450,7 @@ def _probe_bcf(cmd: list[str]) -> None:
419
450
  fake = tmpdir / "biber"
420
451
  fake.write_text("#!/bin/sh\nexit 0\n")
421
452
  fake.chmod(0o755)
422
- subprocess.run(cmd, env=_env_with_biber(fake), capture_output=True)
453
+ subprocess.run(cmd, env=env_with_biber(fake), capture_output=True)
423
454
  finally:
424
455
  shutil.rmtree(tmpdir, ignore_errors=True)
425
456
 
@@ -453,12 +484,12 @@ def run_tectonic(
453
484
  # Determine the right biber from the BCF. If no BCF exists yet (first build
454
485
  # or after a clean), run a silent probe pass with a no-op biber so tectonic
455
486
  # writes the BCF to build_dir without actually needing biber installed.
456
- biber = _biber_for_build(build_dir, job, console)
487
+ biber = biber_for_build(build_dir, job, console)
457
488
  if biber is None:
458
- _probe_bcf(cmd)
459
- biber = _biber_for_build(build_dir, job, console)
489
+ probe_bcf(cmd)
490
+ biber = biber_for_build(build_dir, job, console)
460
491
 
461
- env = _env_with_biber(biber) if biber is not None else None
492
+ env = env_with_biber(biber) if biber is not None else None
462
493
  proc = subprocess.run(cmd, env=env)
463
494
  if proc.returncode != 0:
464
495
  log = build_dir / f"{tex_file.stem}.log"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytex-preprocessor
3
- Version: 1.0.3
3
+ Version: 1.0.6
4
4
  Summary: Type-safe LaTeX document generation with Python
5
5
  Author-email: Frederik Beimgraben <frederik@beimgraben.net>
6
6
  License: GNU GENERAL PUBLIC LICENSE