diff --git a/deploy/build-linux.sh b/deploy/build-linux.sh new file mode 100755 index 0000000..8668e4f --- /dev/null +++ b/deploy/build-linux.sh @@ -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= 指定 Electron 运行时目录(默认取本机 ClockLab 里那份 arm64) +# SERVER_BIN= 跳过交叉编译,直接用现成的 arm64 服务端二进制 +# SERVER_OUT= 交叉编译出的裸服务端另存位置(默认 debug/服务端-arm64/scoreboard; +# 设成空串则不另存,只放进 Console 包) +# OUT_DIR= 输出目录(默认 debug/console-linux-arm64) +# 产物:/ScoreboardConsole--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"