From ef1bc72603446a11cbf8cc4cd009ae78fcaa47a6 Mon Sep 17 00:00:00 2001 From: laoyang Date: Thu, 29 Oct 2026 12:01:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E5=85=A5=20WSL=20=E7=BC=96=E8=AF=91?= =?UTF-8?q?=E5=8F=96=E5=9B=9E=E8=84=9A=E6=9C=AC=E4=B8=8E=E6=8D=A2=E8=A1=8C?= =?UTF-8?q?=E7=AC=A6=E7=AD=96=E7=95=A5=20#18?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build.sh 在 WSL 内编译(默认 xelatex),成功后用 scp 把 PDF 取回 Windows 侧同路径。支持单模板与 all 全库。路径全部走环境 变量覆盖,默认值可用,不硬编码某个人的目录。 verify_all.sh 批量编译验证全库主文件,打印 error / Overfull / Underfull 与页数。每个模板都在自己目录下编译 —— 模板的 .sty 与 ../fonts/ 是相对编译工作目录解析的,从仓库根调用找不到字体。 .gitattributes 把所有 .sh 钉成 eol=lf。Windows 侧 core.autocrlf=true 会把 脚本存成 CRLF,到 Linux 上执行就报 /bin/bash^M: bad interpreter: No such file or directory 这个错误容易误判成"没装 bash",所以在仓库层面固定规则, 不依赖各人本机的 autocrlf 设置。.tex/.sty/.cls 同样钉 lf, 避免中文注释行尾残留 CR 被 XeTeX 当字符排出来。 关于脚本为什么要写成「编译-取回」而不是纯编译: WSL2 会按空闲把整个虚拟机 poweroff,届时 xelatex 连同 PID 1 一起消失, /tmp 也一并清空(后台日志丢失)。脚本的 ssh 会话在编译期间保持不断, 顺带起了保活作用,所以不要把它改成后台运行。 --- .gitattributes | 36 +++++++++++++++++++ build.sh | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++ verify_all.sh | 80 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 .gitattributes create mode 100755 build.sh create mode 100755 verify_all.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a48cb24 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,36 @@ +# 换行符策略 +# +# 背景:这个仓库同时在 Windows(TeX Live 2022)和 WSL/Linux(TeX Live 2025) +# 上使用。Windows 侧 core.autocrlf=true 会把文本文件存成 CRLF, +# 而 shell 脚本一旦是 CRLF,在 Linux 上执行会报 +# bash: ./build.sh: /bin/bash^M: bad interpreter: No such file or directory +# 这个报错不直观(看起来像"没有 bash",实际是行尾多了回车), +# 所以在仓库层面把规则固定下来,不依赖每个人本机的 autocrlf 设置。 + +# 所有 .sh 一律 LF,Windows 侧检出也不转换 +*.sh text eol=lf + +# TeX 源码与样式:LaTeX 对 CRLF 容忍度好,但 eol=lf 能避免 +# 中文注释在行尾残留 CR 被 XeTeX 当成字符排出来 +*.tex text eol=lf +*.sty text eol=lf +*.cls text eol=lf +*.bib text eol=lf + +# 配置文件 +*.gitignore text eol=lf +.gitignore text eol=lf +.gitattributes text eol=lf +*.md text eol=lf + +# 二进制:不参与换行符转换与文本 diff +*.pdf binary +*.otf binary +*.ttf binary +*.ttc binary +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.zip binary \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..260bfe9 --- /dev/null +++ b/build.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# 在 WSL 内编译模板,并把生成的 PDF 取回 Windows 侧 +# +# 适用场景:开发环境在 WSL 里,编译也在 WSL 里做,只把 PDF 拿回 Windows 看。 +# 不追求跨平台通用,是给「Windows + WSL2 + ssh 连入」这套配置用的。 +# +# 用法: +# bash build.sh 特殊事项说明表 # 编译该目录下默认主文件 +# bash build.sh 试卷 试卷题型说明.tex # 显式指定主文件 +# bash build.sh all # 全库所有主文件,逐个编译并取回 +# +# 可用环境变量覆盖(都有默认值): +# WSL_HOST 默认 yongquan@127.0.0.1 +# WSL_KEY 默认 $HOME/.ssh/wsl_local +# WSL_ROOT WSL 内仓库路径,默认 /home/yongquan/code/ouc_latex_template +# WIN_ROOT Windows 侧仓库路径,默认脚本所在目录 +# TEXBIN WSL 内的 latex 命令,默认 xelatex +# +# ⚠️ 两点务必注意 +# +# 1) 不要让本脚本的后台运行。WSL2 会按「空闲」把整个虚拟机回收(poweroff), +# 一旦回收,正在编译的 xelatex 连同 systemd 的 PID 1 一起消失,日志也随 +# /tmp 清空。本脚本的 ssh 会话在编译期间不断开,正好起到保活作用。 +# 若确实需要长时间无人值守编译,得先在 Windows 侧 .wslconfig 里设 +# vmIdleTimeout=-1 并 wsl --shutdown 一次让它生效,不能只改文件。 +# +# 2) 取回的 PDF 不要提交。同源码编译两次,页面内容一致但内嵌 CreationDate +# 不同,git 会认为文件变了(全库 12 个 PDF 全会变成 M)。提交前用 +# git checkout -- '*.pdf' 还原。 +set -u + +WSL_HOST="${WSL_HOST:-yongquan@127.0.0.1}" +WSL_KEY="${WSL_KEY:-$HOME/.ssh/wsl_local}" +WSL_ROOT="${WSL_ROOT:-/home/yongquan/code/ouc_latex_template}" +WIN_ROOT="${WIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" +TEXBIN="${TEXBIN:-xelatex}" +SSH="ssh -i $WSL_KEY -o StrictHostKeyChecking=no $WSL_HOST" + +die() { echo "$1" >&2; exit 2; } + +[ $# -eq 0 ] && die "用法: bash build.sh <目录名|all> [主文件名]" +DIR="$1"; TEX="${2:-}" + +# 全库:先跑批量验证,再逐个取回 +if [ "$DIR" = "all" ]; then + $SSH "bash '$WSL_ROOT/../verify_all.sh'" + rc=$? + echo + echo "--- 取回全部 PDF ---" + while IFS= read -r rel; do + [ -z "$rel" ] && continue + if scp -i "$WSL_KEY" -q "$WSL_HOST:$WSL_ROOT/$rel" "$WIN_ROOT/$rel" 2>/dev/null; then + echo " $rel" + else + echo " $rel (取回失败)" >&2 + fi + done < <($SSH "cd '$WSL_ROOT' && find . -name '*.pdf' -not -path './.git/*' | sed 's|^\./||'") + exit $rc +fi + +# 单目录:目录名与主文件名不一致的,在这里登记 +if [ -z "$TEX" ]; then + case "$DIR" in + 幻灯片) TEX="slides.tex" ;; + 科技报告) TEX="report.tex" ;; + 试卷) die "试卷/ 有两个主文件,请显式指定:试卷题型示例.tex 或 参考答案与评分标准示例.tex" ;; + *) TEX="$DIR.tex" ;; + esac +fi + +echo "=== WSL 编译: $DIR/$TEX ===" +TMPLOG="$(mktemp)" +$SSH "cd '$WSL_ROOT/$DIR' && $TEXBIN -interaction=nonstopmode -halt-on-error '$TEX'" > "$TMPLOG" 2>&1 +rc=$? + +LOG="${TEX%.tex}.log" +echo "=== 结论 ===" +$SSH "cd '$WSL_ROOT/$DIR' && \ + printf 'error: %s\n' \"\$(grep -c '^!' '$LOG' 2>/dev/null || :)\"; \ + printf 'Overfull: %s\n' \"\$(grep -c 'Overfull' '$LOG' 2>/dev/null || :)\"; \ + printf 'Underfull: %s\n' \"\$(grep -c 'Underfull' '$LOG' 2>/dev/null || :)\"; \ + grep -oE 'Output written on .*\([0-9]+ page' '$LOG' 2>/dev/null | tail -1" + +if [ "$rc" -ne 0 ]; then + echo "编译失败,日志片段:" >&2 + grep -A6 "^!" "$TMPLOG" | head -20 | sed 's/^/ /' >&2 + rm -f "$TMPLOG" + exit $rc +fi +rm -f "$TMPLOG" + +PDF="${TEX%.tex}.pdf" +if scp -i "$WSL_KEY" -q "$WSL_HOST:$WSL_ROOT/$DIR/$PDF" "$WIN_ROOT/$DIR/$PDF"; then + echo "PDF 已取回: $WIN_ROOT/$DIR/$PDF" +else + echo "PDF 取回失败" >&2; exit 1 +fi \ No newline at end of file diff --git a/verify_all.sh b/verify_all.sh new file mode 100755 index 0000000..6724acc --- /dev/null +++ b/verify_all.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# 批量编译验证仓库内全部模板,打印每个的 error / Overfull / Underfull / 页数 +# +# 用法: +# bash verify_all.sh # 全部 +# bash verify_all.sh 试卷 # 只跑指定目录(按目录名精确匹配) +# +# 设计说明: +# · 每个模板都在**自己的目录**里编译。模板的 .sty 与 ../fonts/ 都是相对 +# 编译时工作目录解析的,从仓库根调用会找不到字体。 +# · 只排除 error,不排除 Overfull/Underfull —— 这两类是排版质量的早期信号, +# 要看得见。已知的几处属既有问题(试卷/、课程执行大纲/ 各 1 处 Overfull, +# 幻灯片/、科技报告/ 各 1 处 Underfull),不是回归。 +# · 主文件不像「目录名.tex」的,在 TARGETS 里显式登记(幻灯片/slides.tex、 +# 科技报告/report.tex、试卷/ 的两个示例)。 +# · 简历/ 目录下还有 fontawesomesymbols-*.tex 等宏包片段,不是文档, +# 不能拿 ls 的第一个 .tex 当主文件,否则报 Emergency stop。 +set -u + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$ROOT" || exit 1 +TEXBIN="${TEXBIN:-xelatex}" + +TARGETS=( + "劳务发放说明表|劳务发放说明表.tex" + "年度考核登记表|年度考核登记表.tex" + "幻灯片|slides.tex" + "成绩构成说明|成绩构成说明.tex" + "特殊事项说明表|特殊事项说明表.tex" + "科技报告|report.tex" + "简历|简历.tex" + "试卷|试卷题型示例.tex" + "试卷|参考答案与评分标准示例.tex" + "试卷归档封面|试卷归档封面.tex" + "试卷归档目录|试卷归档目录.tex" + "课程执行大纲|课程执行大纲.tex" + "课程教学日历|课程教学日历.tex" +) + +filter="${1:-}" +total=0; ok=0; fail=0 +failed_list="" + +for entry in "${TARGETS[@]}"; do + dir="${entry%%|*}"; tex="${entry##*|}" + if [ -n "$filter" ] && [ "$dir" != "$filter" ]; then continue; fi + total=$((total+1)) + printf "=== [%d] %s/%s ===\n" "$total" "$dir" "$tex" + + log="$(mktemp)" + ( cd "$dir" && "$TEXBIN" -interaction=nonstopmode -halt-on-error "$tex" > "$log" 2>&1 ) + rc=$? + + # grep -c 无匹配时已经输出 0(退出码 1),不能再 `|| echo 0`, + # 否则变量会变成 "0\n0",后面的整数比较直接报 integer expected。 + err=$(grep -c "^!" "$log" 2>/dev/null || :) + over=$(grep -c "Overfull" "$log" 2>/dev/null || :) + under=$(grep -c "Underfull" "$log" 2>/dev/null || :) + pages=$(grep -oE "Output written on .*\([0-9]+ page" "$log" 2>/dev/null \ + | tail -1 | grep -oE "\([0-9]+" | tr -d "(") + + printf " rc=%s err=%s Overfull=%s Underfull=%s pages=%s\n" \ + "$rc" "${err:-0}" "${over:-0}" "${under:-0}" "${pages:-?}" + + if [ "$rc" -eq 0 ] && [ "${err:-1}" -eq 0 ] && [ -n "${pages:-}" ]; then + ok=$((ok+1)) + else + fail=$((fail+1)) + failed_list="${failed_list} + - $dir/$tex" + grep -A2 "^!" "$log" 2>/dev/null | head -9 | sed "s/^/ /" + fi + rm -f "$log" +done + +echo +echo "================ 汇总 ================" +echo "总数 $total | 通过 $ok | 失败 $fail" +[ -n "$failed_list" ] && printf "%s\n" "$failed_list" +exit 0 \ No newline at end of file