#!/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"