#!/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()