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 文件名都生效。
This commit is contained in:
1 parent
efa049c827
commit
a85115c96e
1 file changed
+133
Executable
+133
@@ -0,0 +1,133 @@
|
||||
#!/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"
|
||||
Reference in new issue
Block a user