#!/usr/bin/env bash set -uo pipefail shopt -s nocasematch export LC_NUMERIC=C VERSION="2.0" WORKDIR="benchmark" SRC="QFT" RUNS=3 API="https://web.itp3.uni-stuttgart.de/latex-benchmark/submit.php" SCOREBOARD="https://itp3.info/latexscores" YELLOW='\033[1;33m' GREEN='\033[0;32m' RED='\033[0;31m' NC='\033[0m' SYSTEM="Unknown" HOST="Unknown" CPU="Unknown" KERNEL="Unknown" RUN_USER="Unknown" DATE="Unknown" LVERSION="Unknown" BVERSION="Unknown" avg="" max="" min="" COMMENT="" KEY="" PDFLATEX_CMD=(pdflatex -interaction=nonstopmode -halt-on-error "$SRC.tex") BIBTEX_CMD=(bibtex "$SRC.aux") REQUIRED_TEX_FILES=( scrreprt.cls ucs.sty babel.sty caption.sty hyperref.sty geometry.sty etoolbox.sty authblk.sty tabularx.sty makecell.sty tikz.sty circuitikz.sty tikzscale.sty pgfplots.sty pgflibraryarrows.code.tex scrlayer-scrpage.sty mdframed.sty xcolor.sty empheq.sty tcolorbox.sty enumitem.sty rotating.sty amsmath.sty amssymb.sty amsthm.sty bm.sty wasysym.sty dsfont.sty fdsymbol.sty latexsym.sty tensor.sty tikz-feynman.sty slashed.sty simplewick.sty stackrel.sty pifont.sty multirow.sty cleveref.sty doi.sty cite.sty ) GENERATED_FILES=( "$WORKDIR/$SRC.aux" "$WORKDIR/$SRC.bbl" "$WORKDIR/$SRC.blg" "$WORKDIR/$SRC.log" "$WORKDIR/$SRC.out" "$WORKDIR/$SRC.pdf" "$WORKDIR/$SRC.toc" ) section() { printf '\n%b%s%b\n' "$GREEN" "$1" "$NC" } ok() { printf '%b[OK]%b\n' "$GREEN" "$NC" } warn() { printf '%b%s%b\n' "$YELLOW" "$1" "$NC" } fail() { printf '%b%s%b\n' "$RED" "$1" "$NC" } abort() { fail "$1" printf '\n' exit 1 } command_exists() { command -v "$1" >/dev/null 2>&1 } first_line() { "$@" 2>/dev/null | awk 'NR == 1 { print; exit }' } trim() { awk '{$1=$1; print}' } detect_system() { local uname_s uname_r uname_s="$(uname -s 2>/dev/null || printf 'Unknown')" uname_r="$(uname -r 2>/dev/null || printf '')" case "$uname_s" in Darwin*) SYSTEM="macOS" ;; Linux*) if printf '%s\n%s\n' "$uname_r" "$(cat /proc/version 2>/dev/null)" | grep -qiE 'microsoft|wsl'; then SYSTEM="WSL" else SYSTEM="Linux" fi ;; *) SYSTEM="Unsupported ($uname_s)" warn "→ This system is not officially supported. The benchmark may still run." ;; esac } print_install_hint() { case "$SYSTEM" in macOS) printf ' Install MacTeX, or install BasicTeX and add the missing packages with tlmgr.\n' ;; WSL) printf ' On Ubuntu/Debian WSL, try: sudo apt install texlive-full\n' ;; Linux) printf ' On Ubuntu/Debian, try: sudo apt install texlive-full\n' printf ' On other distributions, install the equivalent full TeX Live package set.\n' ;; *) printf ' Install a TeX Live distribution that contains the missing packages.\n' ;; esac } check_prerequisites() { local missing=0 section "Checking prerequisites:" if ! command_exists awk; then fail "→ awk was not found. It is needed to calculate benchmark statistics." missing=1 fi if ! command_exists pdflatex; then fail "→ pdflatex was not found." missing=1 else LVERSION="$(first_line pdflatex --version)" printf '→ pdfTeX found:\t%s\n' "$LVERSION" fi if ! command_exists bibtex; then fail "→ bibtex was not found." missing=1 else BVERSION="$(first_line bibtex --version)" printf '→ BibTeX found:\t%s\n' "$BVERSION" fi if (( missing )); then printf '\nThe benchmark cannot run until the required command-line tools are installed.\n' print_install_hint exit 1 fi if command_exists kpsewhich; then check_tex_files else warn "→ kpsewhich was not found. Skipping detailed LaTeX package checks." warn " If compilation fails, install a broader TeX Live/MacTeX package set." fi if ! command_exists curl; then warn "→ curl was not found. Uploading results will be disabled." fi if ! command_exists git; then warn "→ git was not found. Manual clone/update workflows and the one-line runner may fail." fi } check_tex_files() { local missing_files=() local tex_file for tex_file in "${REQUIRED_TEX_FILES[@]}"; do if ! kpsewhich "$tex_file" >/dev/null 2>&1; then missing_files+=("$tex_file") fi done if ((${#missing_files[@]} > 0)); then fail "→ Some LaTeX packages required by the benchmark are missing:" printf ' - %s\n' "${missing_files[@]}" printf '\n' print_install_hint exit 1 fi printf '→ LaTeX package check:\t%s\n' "all required files found" } collect_metadata() { section "Collecting data:" HOST="$(hostname 2>/dev/null || printf 'Unknown')" KERNEL="$(uname -r 2>/dev/null || printf 'Unknown')" RUN_USER="$(id -un 2>/dev/null || whoami 2>/dev/null || printf 'Unknown')" DATE="$(date '+%Y-%m-%d %T' 2>/dev/null || printf 'Unknown')" case "$SYSTEM" in macOS) CPU="$(sysctl -n machdep.cpu.brand_string 2>/dev/null | trim)" ;; Linux|WSL) CPU="$(awk -F: '/model name/ { gsub(/^[ \t]+/, "", $2); print $2; exit }' /proc/cpuinfo 2>/dev/null)" ;; esac CPU="${CPU:-Unknown}" printf '→ Version:\t%s\n' "$VERSION" printf '→ System:\t%s\n' "$SYSTEM" printf '→ Host:\t\t%s\n' "$HOST" printf '→ CPU:\t\t%s\n' "$CPU" printf '→ Kernel:\t%s\n' "$KERNEL" printf '→ User:\t\t%s\n' "$RUN_USER" printf '→ Timestamp:\t%s\n' "$DATE" } json_escape() { local value="${1-}" value="${value//\\/\\\\}" value="${value//\"/\\\"}" value="${value//$'\n'/\\n}" value="${value//$'\r'/\\r}" value="${value//$'\t'/\\t}" printf '%s' "$value" } generate_post_data() { printf '{\n' printf '"timestamp":"%s",\n' "$(json_escape "$DATE")" printf '"avg_score":"%s",\n' "$(json_escape "$avg")" printf '"max_score":"%s",\n' "$(json_escape "$max")" printf '"min_score":"%s",\n' "$(json_escape "$min")" printf '"version":"%s",\n' "$(json_escape "$VERSION")" printf '"host":"%s",\n' "$(json_escape "$HOST")" printf '"system":"%s",\n' "$(json_escape "$SYSTEM")" printf '"cpu":"%s",\n' "$(json_escape "$CPU")" printf '"kernel":"%s",\n' "$(json_escape "$KERNEL")" printf '"user":"%s",\n' "$(json_escape "$RUN_USER")" printf '"latex":"%s",\n' "$(json_escape "$LVERSION")" printf '"bibtex":"%s",\n' "$(json_escape "$BVERSION")" printf '"comment":"%s",\n' "$(json_escape "$COMMENT")" printf '"key":"%s"\n' "$(json_escape "$KEY")" printf '}\n' } upload() { printf '\n' if ! command_exists curl; then fail "→ curl is not installed. Skipping upload." print_scoreboard return 0 fi printf '→ Uploading ... ' if curl --fail --show-error --silent \ --connect-timeout 10 \ --max-time 30 \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -X POST \ --data "$(generate_post_data)" \ "$API"; then printf '\n\n' else printf '\n' fail "→ Upload failed. Your local benchmark result is still valid." printf '\n' fi print_scoreboard } print_scoreboard() { printf '┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n' printf '┃ Scoreboard: %b%s%b ┃\n' "$YELLOW" "$SCOREBOARD" "$NC" printf '┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n' } run_pdflatex() { (cd "$WORKDIR" && "${PDFLATEX_CMD[@]}") } run_bibtex() { (cd "$WORKDIR" && "${BIBTEX_CMD[@]}") } prepare_document() { local ret if [[ ! -d "$WORKDIR" ]]; then abort "Benchmark directory '$WORKDIR' was not found. Run this script from the repository root." fi section "Preparing compilation of test project:" printf '→ Running PDFLaTeX for the first time ... \t' if ret="$(run_pdflatex 2>&1)"; then ok else fail "[FAILED]" printf '\n%s\n\n' "$ret" abort "PDFLaTeX could not compile the benchmark document. Check the missing package messages above." fi if [[ "$ret" =~ "latex warning" ]]; then printf '→ Running BibTeX ... \t\t\t\t' if ret="$(run_bibtex 2>&1)"; then ok else fail "[FAILED]" printf '\n%s\n\n' "$ret" abort "BibTeX could not process the benchmark document." fi printf '→ Running PDFLaTeX for the second time ... \t' if ret="$(run_pdflatex 2>&1)"; then ok else fail "[FAILED]" printf '\n%s\n\n' "$ret" abort "PDFLaTeX could not compile the benchmark document on the second run." fi fi if [[ "$ret" =~ "latex warning" ]]; then printf '→ Running PDFLaTeX for the third time ... \t' if ret="$(run_pdflatex 2>&1)"; then if [[ "$ret" =~ "latex warning" ]]; then printf '%b[OK?]%b\n' "$YELLOW" "$NC" warn "The third run still reported LaTeX warnings. Continuing with the benchmark." else ok fi else fail "[FAILED]" printf '\n%s\n\n' "$ret" abort "PDFLaTeX could not compile the benchmark document on the third run." fi fi } run_benchmark() { local i t local times=() section "Running benchmark:" for ((i = 1; i <= RUNS; i++)); do printf '→ RUN %s ... ' "$i" t="$(TIMEFORMAT=%R; { time run_pdflatex >/dev/null; } 2>&1)" t="$(printf '%s' "$t" | tr ',' '.' | awk 'NF { print $NF; exit }')" t="$(awk -v value="$t" 'BEGIN { printf "%.1f", value + 0 }')" printf 'T = %b%s%b s\n' "$YELLOW" "$t" "$NC" times+=("$t") done read -r avg min max < <( awk ' NR == 1 { min = max = $1 } { sum += $1; if ($1 < min) min = $1; if ($1 > max) max = $1 } END { printf "%.1f %.1f %.1f\n", sum / NR, min, max } ' < <(printf '%s\n' "${times[@]}") ) } print_results() { local info_plain info_colored left_padding right_padding padding visible_width=53 info_plain="MIN = $min s | AVG = $avg s | MAX = $max s" info_colored="MIN = ${GREEN}$min${NC} s | AVG = ${YELLOW}$avg${NC} s | MAX = ${RED}$max${NC} s" padding=$((visible_width - ${#info_plain})) if ((padding < 0)); then padding=0 fi left_padding=$((padding / 2)) right_padding=$((padding - left_padding)) printf '\n' printf '┏━━━━━━━━━━━━━━━━━━━━━━ %bRESULTS%b ━━━━━━━━━━━━━━━━━━━━━━┓\n' "$YELLOW" "$NC" printf '┃%*s%b%*s┃\n' "$left_padding" '' "$info_colored" "$right_padding" '' printf '┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n' } print_box_line() { local text="$1" prefix="${2-}" suffix="${3-}" line padding visible_width=53 line=" $text" padding=$((visible_width - ${#line})) if ((padding < 0)); then padding=0 fi printf '┃%b%s%b%*s┃\n' "$prefix" "$line" "$suffix" "$padding" '' } prompt_upload() { printf '\n' printf '┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n' print_box_line "To obtain a submission key, write an email to" print_box_line "nicolai.lang@itp3.uni-stuttgart.de" "$YELLOW" "$NC" printf '┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n' printf '\n' read -r -p "→ Enter key to submit score to database (leave empty to skip): " KEY if [[ -n "$KEY" ]]; then read -r -p "→ Comment (max 200 characters): " COMMENT if ((${#COMMENT} > 200)); then COMMENT="${COMMENT:0:200}" warn "→ Comment was longer than 200 characters and has been truncated." fi upload fi } cleanup_generated_files() { local generated_file for generated_file in "${GENERATED_FILES[@]}"; do rm -f "$generated_file" done } main() { trap cleanup_generated_files EXIT printf '\n' printf '┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n' printf '┃ %bLATEX BENCHMARK%b ┃\n' "$YELLOW" "$NC" printf '┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n' section "Checking system:" detect_system printf '→ System: %s\n' "$SYSTEM" check_prerequisites collect_metadata prepare_document run_benchmark print_results prompt_upload printf '\nDone!\n' } if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then main "$@" fi