Files
smartpark-app/ci/build_manual.py
T

87 lines
4.7 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Build only from a fresh temporary copy; publish output only on success."""
from pathlib import Path
import datetime, os, re, shutil, subprocess, tempfile
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / 'latex-manual'
OUT = ROOT / 'build' / 'manual'
def run(args, cwd=ROOT):
print('+ ' + ' '.join(args), flush=True)
subprocess.run(args, cwd=cwd, check=True)
def git(*args):
return subprocess.check_output(['git', *args], cwd=ROOT, text=True).strip()
def main():
OUT.mkdir(parents=True, exist_ok=True)
# Only remove this script's two fixed output files, so a failed run cannot
# leave a stale deliverable looking successful.
for name in ('SmartPark-Manual.pdf', 'build-info.txt'):
(OUT / name).unlink(missing_ok=True)
sha = git('rev-parse', 'HEAD')
expected = os.environ.get('MANUAL_COMMIT', '')
ci = os.environ.get('MANUAL_CI', '') == 'true'
dirty = bool(git('status', '--porcelain', '--untracked-files=normal'))
if ci and (not expected or expected != sha or dirty):
raise RuntimeError('CI checkout must be clean and match MANUAL_COMMIT')
number = os.environ.get('MANUAL_RUN_NUMBER', 'local')
if ci and not number.isdigit():
raise RuntimeError('Missing numeric Gitea run number')
if not re.fullmatch(r'[A-Za-z0-9.-]+', number):
raise RuntimeError('Unsafe run identifier')
revision = sha[:12] + ('-dirty' if dirty else '')
build_time = datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d %H:%M UTC')
for tool in ('latexmk', 'pdflatex', 'bibtex', 'makeglossaries', 'makeindex', 'pygmentize'):
if shutil.which(tool) is None:
raise RuntimeError('Missing tool: ' + tool)
with tempfile.TemporaryDirectory(prefix='manual-', dir=ROOT / 'build') as temp:
work = Path(temp)
shutil.copytree(SOURCE, work, dirs_exist_ok=True,
ignore=shutil.ignore_patterns('*.aux', '*.log', '*.pdf', '*.toc', '*.out',
'*.bbl', '*.blg', '*.fdb_latexmk', '*.fls', '*.idx', '*.ind', '*.ilg',
'*.glo', '*.gls', '*.glg', '*.ist', '*.acn', '*.acr', '*.alg',
'*.lof', '*.lot', '_minted-*', 'build-metadata.tex'))
# Do not discard legitimate PDF figures: restore tracked/source PDFs
# except the old root document output.
for source_pdf in SOURCE.rglob('*.pdf'):
if source_pdf == SOURCE / 'devopsAssignment.pdf':
continue
target = work / source_pdf.relative_to(SOURCE)
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source_pdf, target)
(work / 'build-metadata.tex').write_text(
'\\renewcommand{\\BuildRevision}{' + revision + '}\n' +
'\\renewcommand{\\BuildRun}{' + number + '}\n' +
'\\renewcommand{\\BuildUTC}{' + build_time + '}\n', encoding='utf-8')
command = ['latexmk', '-pdf', '-interaction=nonstopmode', '-halt-on-error',
'-file-line-error', '-shell-escape', 'devopsAssignment.tex']
run(command, work)
run(['makeglossaries', 'devopsAssignment'], work)
if (work / 'devopsAssignment.idx').stat().st_size:
run(['makeindex', 'devopsAssignment.idx'], work)
run(command, work)
pdf = work / 'devopsAssignment.pdf'
if not pdf.is_file() or pdf.stat().st_size < 5 or pdf.read_bytes()[:5] != b'%PDF-':
raise RuntimeError('Expected nonempty PDF was not produced')
log = (work / 'devopsAssignment.log').read_text(errors='replace')
if 'There were undefined references' in log or 'There were undefined citations' in log:
raise RuntimeError('PDF still contains unresolved references/citations; inspect source')
shutil.copy2(pdf, OUT / 'SmartPark-Manual.pdf')
info = f'commit={sha}\nrevision={revision}\nrun={number}\nutc={build_time}\n'
info += 'mode=' + ('Gitea Actions' if ci else 'LOCAL TEST - not Actions evidence') + '\n'
for tool, flag in [('latexmk','-v'), ('pdflatex','--version'), ('pygmentize','-V')]:
result = subprocess.check_output([tool, flag], text=True)
info += result
packages = subprocess.check_output(['dpkg-query', '-W',
'-f=${Package}\t${Version}\n', 'latexmk', 'texlive-base',
'texlive-latex-extra', 'texlive-fonts-recommended', 'lmodern', 'python3-pygments'], text=True)
info += '\nUbuntu package versions:\n' + packages
(OUT / 'build-info.txt').write_text(info, encoding='utf-8')
print(info)
print('SUCCESS: ' + str(OUT / 'SmartPark-Manual.pdf'))
if __name__ == '__main__':
main()