加入 WSL 编译取回脚本与换行符策略 #18
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 会话在编译期间保持不断,
顺带起了保活作用,所以不要把它改成后台运行。
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
Executable
+80
@@ -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
|
||||
Reference in New Issue
Block a user