Files
Scoreboard/deploy/build-linux.sh
T
AskaEth a85115c96e build(linux): 新增 deploy/build-linux.sh —— 手工拼 arm64 Console 包(绕开 electron-builder 在 Linux 上的必崩路径)
为什么手工拼:electron-builder 26.x 在「需要下载 Electron 运行时/7zip 工具」那条分支上会抛
`Cannot read properties of undefined (reading 'ReadWrite')`(`app-builder-lib/util/electronGet.ts` 里
`ElectronDownloadCacheMode` 未定义),本机与构建机都复现过。

脚本做三件事:
1. 交叉编译 arm64 服务端(CGO_ENABLED=0,纯 Go SQLite 驱动,产物静态链接);
2. 用本地现成的 Electron linux-arm64 运行时光手拼出可运行的包树
   (Electron 可执行改名 + resources/app/ 放 console 代码 + 服务端二进制放包根);
3. 打 tar.gz(幂等:每次先清旧产物)。

包内布局依据 console/main 里的路径解析链:服务端与 Console 同级(包根),
config.yaml / rules/ 也在包根(首启由 userpaths 复制到 $XDG_DATA_HOME/ScoreboardConsole/)。
本机 Xvfb 实测:Console 起得来、拉起服务端、/api/health 200、XDG 路径与 Linux 文件名都生效。
2026-09-15 01:14:50 +08:00

134 lines
8.0 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Scoreboard Console · Linux/arm64 发布包(tar.gz)构建脚本
#
# 为什么手工拼包,而不是让 electron-builder 出 Linux 包:
# 1) electron-builder 在 Linux 上有一条必崩的路径(打 win 目标时读不到的
# 'ReadWrite' 属性),本项目今晚实测复现过,跑不通就没法作为发布手段;
# 2) 它还要现下 Linux 版 Electron 运行时,本机网络下这个下载极不稳定;
# 3) Linux 的 tar.gz 本质就是「Electron 运行时 + resources/app/ 里的应用文件」,
# 布局是可数的几层目录,手工拼比驱动一个有崩溃路径的工具更可靠,也不需要联网。
# 因此:运行时直接复用本机已有的 electron/dist(arm64),应用文件按 console/package.json
# 的 build.files / build.extraFiles 声明的位置原样摆放 —— 摆错了 Console 就找不到服务端
# 二进制(见下面「服务端二进制放哪」那一段)。
#
# 服务端二进制放哪(依据 console/main/ 的实际代码,不是猜的):
# console/main/index.js 的 appDir() 在打包态返回 path.dirname(app.getPath('exe')),
# 即 Console 可执行文件所在目录 = 部署包根目录;index.js 再把它作为 baseDir 交给
# config.resolveExecutable(),候选表第一项就是 <部署包根>/scoreboard(文件名由
# platform.serverExeName() 按平台给出,Linux 无 .exe 后缀)。
# 所以服务端二进制与 Console 可执行文件**同级**放在包根,不能塞进 resources/。
# 同一段代码还决定 config.yaml / rules / config.json 也按包根解析(userpaths.js 首次启动
# 从包根复制到用户目录当模板),所以这几样同样摆在包根。
#
# 用法:deploy/build-linux.sh
# 环境变量:
# ELECTRON_DIST=<dir> 指定 Electron 运行时目录(默认取本机 ClockLab 里那份 arm64)
# SERVER_BIN=<file> 跳过交叉编译,直接用现成的 arm64 服务端二进制
# SERVER_OUT=<file> 交叉编译出的裸服务端另存位置(默认 debug/服务端-arm64/scoreboard;
# 设成空串则不另存,只放进 Console 包)
# OUT_DIR=<dir> 输出目录(默认 debug/console-linux-arm64)
# 产物:<OUT_DIR>/ScoreboardConsole-<VERSION>-linux-arm64.tar.gz
# 解压后顶层目录是 ScoreboardConsole/,进去直接跑 ./ScoreboardConsole 即可。
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
OUT_DIR="${OUT_DIR:-$ROOT/debug/console-linux-arm64}"
ELECTRON_DIST="${ELECTRON_DIST:-$HOME/Project/ClockLab/node_modules/electron/dist}"
SERVER_BIN="${SERVER_BIN:-}"
# 默认要另存一份裸服务端;显式设 SERVER_OUT= 空串表示只放进 Console 包、不另存
SERVER_OUT="${SERVER_OUT-$ROOT/debug/服务端-arm64/scoreboard}"
die() { echo "[build-linux] 错误:$*" >&2; exit 1; }
# ---- 0. 版本号:只认仓库根的 VERSION(唯一真相),与 deploy/build.bat 同一口径 ----
[ -f "$ROOT/VERSION" ] || die "找不到 $ROOT/VERSION"
VERSION="$(sed -e 's/^\xEF\xBB\xBF//' "$ROOT/VERSION" | grep -m1 -v '^[[:space:]]*$' | tr -d '\r\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
[ -n "$VERSION" ] || die "VERSION 文件是空的"
# ---- 1. 清干净再拼:重复执行不会把上一版的文件叠进包里 ----
STAGE="$OUT_DIR/ScoreboardConsole"
TARBALL="$OUT_DIR/ScoreboardConsole-${VERSION}-linux-arm64.tar.gz"
mkdir -p "$OUT_DIR"
rm -rf "$STAGE"
rm -f "$TARBALL"
# ---- 2. Electron 运行时:只用本机已有的,绝不下载 ----
[ -x "$ELECTRON_DIST/electron" ] || die "本机没有可用的 Electron 运行时:$ELECTRON_DIST(本脚本不联网下载)"
ELECTRON_VERSION="$(cat "$ELECTRON_DIST/version" 2>/dev/null || echo '?')"
echo "[build-linux] 版本 $VERSION,Electron 运行时 $ELECTRON_VERSION($ELECTRON_DIST)"
cp -a "$ELECTRON_DIST" "$STAGE"
# 可执行文件改名成产品名:窗口标题、任务栏分组、托盘都按它走;
# 同时这一步定下了 app.getPath('exe'),也就是后面所有路径的基准
mv "$STAGE/electron" "$STAGE/ScoreboardConsole"
# 运行时自带的是「没给 app 目录时打开的那张欢迎页」,我们的应用不放这儿,留着只是多余的 100KB
rm -rf "$STAGE/resources/default_app.asar"
# ---- 3. 应用文件:对应 console/package.json 的 build.files ----
APP_DIR="$STAGE/resources/app"
mkdir -p "$APP_DIR"
for item in main renderer scripts config.json package.json; do
[ -e "$ROOT/console/$item" ] || die "console/$item 不存在,应用文件不全"
cp -a "$ROOT/console/$item" "$APP_DIR/"
done
# 单元测试不进发布包:*.test.js 是开发期的东西,带上只会让包内代码与「跑的是什么」对不上
find "$APP_DIR" -name '*.test.js' -delete
# ---- 4. 服务端二进制(linux/arm64):与 Console 可执行文件同级,见文件头说明 ----
if [ -n "$SERVER_BIN" ]; then
[ -f "$SERVER_BIN" ] || die "SERVER_BIN 指向的文件不存在:$SERVER_BIN"
echo "[build-linux] 使用现成服务端二进制:$SERVER_BIN"
cp -f "$SERVER_BIN" "$STAGE/scoreboard"
else
command -v go >/dev/null 2>&1 || die "没装 Go,交叉编译不了 linux/arm64 服务端(可用 SERVER_BIN=... 指定现成二进制)"
echo "[build-linux] 交叉编译服务端 linux/arm64(CGO 关掉 = 静态链接,不依赖目标机的 libc)"
BUILD_TIME="$(date '+%Y-%m-%d %H:%M:%S')"
# 版本号与构建时间在编译这一刻写死进二进制;运行期不再读任何文件
( cd "$ROOT/server" && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
go build -trimpath \
-ldflags "-s -w -X 'main.version=$VERSION' -X 'git.yeij.top/AskaEth/Scoreboard/server/internal/version.BuildTime=$BUILD_TIME'" \
-o "$STAGE/scoreboard" . )
# 裸服务端也单独留一份:排障时要能脱离 Console 直接起它看日志,
# 与 Windows 侧 build.bat 把服务端落到 dist\ 是同一个用途。SERVER_OUT= 可关掉。
if [ -n "$SERVER_OUT" ]; then
mkdir -p "$(dirname "$SERVER_OUT")"
cp -f "$STAGE/scoreboard" "$SERVER_OUT"
echo "[build-linux] 裸服务端另存:$SERVER_OUT"
fi
fi
chmod +x "$STAGE/scoreboard"
# ---- 5. 包根随包文件:对应 console/package.json 的 build.extraFiles ----
cp -a "$ROOT/config.yaml" "$STAGE/config.yaml"
cp -a "$ROOT/console/config.json" "$STAGE/config.json"
cp -a "$ROOT/console/scoreboard.png" "$STAGE/scoreboard.png"
cp -a "$ROOT/rules" "$STAGE/rules"
# 说明书之类平铺在包根(extraFiles 的 from: extra, to: .)。
# 这些 .txt 是 deploy/prepare-extra.mjs 从 docs/ 生成到 console/extra/ 的,本脚本不重新生成,
# 只负责把它们放进包里;没有就跳过(Linux 包不该因为没有说明书而构建失败)。
if [ -d "$ROOT/console/extra" ]; then
find "$ROOT/console/extra" -maxdepth 1 -type f ! -name '.gitkeep' -exec cp -a {} "$STAGE/" \;
fi
# ---- 6. 打 tar.gz ----
chmod +x "$STAGE/ScoreboardConsole" "$STAGE/chrome-sandbox" 2>/dev/null || true
# --owner/--group 归零:包里的属主信息没有意义(非 root 解压时本来就会被忽略),
# 归一以后谁解出来的东西都一样。
# 注意 setuid 位没法靠 tar 交付:Electron 的 chrome-sandbox 在禁用 user namespace 的机器上
# 需要 root:root + 4755,那是解压后由现场自己补的一步(见末尾提示),包里带不了。
tar --create --gzip --file "$TARBALL" --owner=0 --group=0 --numeric-owner -C "$OUT_DIR" ScoreboardConsole
echo
echo "[build-linux] 完成"
echo " 包内布局(前两层):"
tar -tzf "$TARBALL" | awk -F/ 'NF<=2' | sed 's/^/ /'
echo " 产物:$TARBALL"
echo " 大小:$(du -h "$TARBALL" | cut -f1)($(stat -c%s "$TARBALL") 字节)"
echo " SHA256:$(sha256sum "$TARBALL" | cut -d' ' -f1)"
echo
echo " 无头环境试跑(有桌面环境时不需要 --no-sandbox):"
echo " ./ScoreboardConsole --no-sandbox"
echo " 内核禁用了 user namespace 时,沙箱要现场补一次(tar 交付不了 setuid):"
echo " sudo chown root:root chrome-sandbox && sudo chmod 4755 chrome-sandbox"