Files
ARTEX-chixuweihu/build.sh
T
Autumn-27 44156a0a4c feat(update): 页面一键更新 + 守护启动脚本
系统配置页新增「版本与更新」卡片,顶栏版本号旁在有新版时亮出提示胶囊。
更新链路:下载发布包 → 比对 SHA256SUMS → -h 冒烟测试 → 暂存 artex.new
→ 以退出码 75 退出 → 守护脚本重新拉起并完成换装,页面轮询 /api/health
等版本号变化后自动刷新。

新增 start.sh / start.bat 作为正式启动入口,只按退出码决定是否重新拉起
(0 停止 / 75 应用新版 / 其余崩溃退避)。校验、冒烟、换装、回滚全部收在
selfupdate 包里而不写进脚本:这些逻辑在 sh 和 bat 上要写两套,而它们恰恰
最不能出错——换上跑不起来的二进制后守护进程会反复拉起它,只能上机器手工救。
Docker 的 ENTRYPOINT 一并改为该脚本,并由它转发 SIGTERM,否则 docker stop
只发给 PID 1,artex 做不了优雅关闭会被 SIGKILL 硬杀。

失败保护两道闸门互补:无法 exec 由换装前的冒烟挡住,能执行但初始化崩溃
由启动计数兜住(连续 3 次失败自动回滚 artex.old,坏的留作 artex.failed)。

GitHub 查询结果后端缓存 30 分钟(失败 2 分钟)。未认证 API 是每 IP 每小时
60 次,且这 60 次是服务器 IP 上所有用户共用的,顶栏每次整页加载都查一次,
不缓存会很快耗光配额导致真想更新时反而查不动。手动「检查更新」带 force=1
绕过缓存。

安全:只认 GitHub 域名且强制 HTTPS(逐跳校验重定向),发布源写死不可配置;
升级文件挂在可执行文件所在目录而非 CWD(服务化运行时 CWD 可能是 /);
开发构建版本号不可比较,禁用一键更新。

走完整 zip 而非裸二进制,因为现有 Release 的 SHA256SUMS 只覆盖 zip,
这样无需改 CI,也兼容已发布的历史版本。

已知限制:Docker 下只换程序不换镜像(工具链不同步、重建容器会退回镜像
版本);不同步发布包里的 skills/;更新即重启会中断运行中的任务。
2026-09-11 12:39:59 -04:00

264 lines
8.9 KiB
Bash
Executable File
Raw Permalink 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
# ARTEX cross-platform release builder.
#
# The default mode builds one target and embeds the already-exported frontend.
# `./build.sh --release` builds and packages all supported desktop/server targets.
#
# Environment variables:
# ARTEX_TARGET_OS=linux One target OS in single-target mode.
# ARTEX_TARGET_ARCH=amd64 One target arch in single-target mode.
# ARTEX_TARGETS=linux/amd64,... Comma-separated targets for multi-target mode.
# ARTEX_BUILD_VERSION=v0.3.3 Version embedded in the binary and archive name.
# ARTEX_OUTPUT=/path/to/artex Explicit binary path in single-target mode.
# ARTEX_OUTPUT_DIR=dist Directory for default binary paths.
# ARTEX_PACKAGE=1 Create a zip archive for each target.
# ARTEX_PACKAGE_DIR=dist Directory for release archives.
# ARTEX_COMPRESS=off UPX mode: off, auto, or required.
# ARTEX_UPX_ARGS="--best --lzma" Arguments passed to UPX.
# ARTEX_SKIP_FRONTEND=1 Reuse server/webui/dist (for CI artifact builds).
# ARTEX_SKIP_NPM_CI=1 Skip npm ci while rebuilding the frontend.
# ARTEX_GOSUMDB=sum.golang.org Go checksum database.
set -euo pipefail
cd "$(cd "$(dirname "$0")" && pwd)"
info() { printf '\033[36m[*]\033[0m %s\n' "$*"; }
ok() { printf '\033[32m[+]\033[0m %s\n' "$*"; }
warn() { printf '\033[33m[!]\033[0m %s\n' "$*" >&2; }
die() { printf '\033[31m[x]\033[0m %s\n' "$*" >&2; exit 1; }
usage() {
cat <<'EOF'
用法:
./build.sh 编译当前系统当前架构
./build.sh --target linux/amd64 编译一个指定目标
./build.sh --release 编译并打包全部支持的目标
选项:
--release 构建 Linux、macOS、Windows 的 amd64/arm64 目标并生成 zip
--target OS/ARCH 设置单个目标,例如 windows/amd64
--upx 强制使用 UPX 压缩二进制(可能影响部分 Linux 环境兼容性)
--no-compress 不使用 UPX,仅使用 Go linker 裁剪并压缩 zip
--help 显示帮助
多目标列表可通过 ARTEX_TARGETS 覆盖,例如:
ARTEX_TARGETS=linux/amd64,windows/amd64 ./build.sh --release
EOF
}
RELEASE_TARGETS_DEFAULT="linux/amd64,linux/arm64,darwin/amd64,darwin/arm64,windows/amd64"
ARTEX_RELEASE="${ARTEX_RELEASE:-0}"
ARTEX_COMPRESS="${ARTEX_COMPRESS:-off}"
ARTEX_PACKAGE="${ARTEX_PACKAGE:-0}"
while [ "$#" -gt 0 ]; do
case "$1" in
--release)
ARTEX_RELEASE=1
ARTEX_PACKAGE=1
shift
;;
--target)
[ "$#" -ge 2 ] || die "--target 需要 OS/ARCH 参数"
target_arg="$2"
case "$target_arg" in
*/*)
ARTEX_TARGET_OS="${target_arg%%/*}"
ARTEX_TARGET_ARCH="${target_arg##*/}"
ARTEX_TARGETS="$target_arg"
;;
*) die "目标必须是 OS/ARCH,例如 linux/amd64" ;;
esac
shift 2
;;
--no-compress)
ARTEX_COMPRESS=0
shift
;;
--upx)
ARTEX_COMPRESS=required
shift
;;
--help|-h)
usage
exit 0
;;
*) die "未知参数:$1(使用 --help 查看用法)" ;;
esac
done
command -v go >/dev/null 2>&1 || die "未检测到 Go(项目需要 Go 1.26 或更高版本)"
ARTEX_GOSUMDB="${ARTEX_GOSUMDB:-sum.golang.org}"
if [ -z "${ARTEX_BUILD_VERSION:-}" ]; then
if command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
ARTEX_BUILD_VERSION="$(git describe --tags --always --dirty)"
else
ARTEX_BUILD_VERSION="dev"
fi
fi
# Release tags are commonly passed as v0.3.3; keep the binary version consistent.
ARTEX_BUILD_VERSION="${ARTEX_BUILD_VERSION#v}"
ARTEX_OUTPUT_DIR="${ARTEX_OUTPUT_DIR:-dist}"
ARTEX_PACKAGE_DIR="${ARTEX_PACKAGE_DIR:-$ARTEX_OUTPUT_DIR}"
ARTEX_UPX_ARGS="${ARTEX_UPX_ARGS:---best --lzma}"
if [ "${ARTEX_RELEASE}" = "1" ]; then
ARTEX_TARGETS="${ARTEX_TARGETS:-$RELEASE_TARGETS_DEFAULT}"
else
ARTEX_TARGET_OS="${ARTEX_TARGET_OS:-$(GOSUMDB="$ARTEX_GOSUMDB" go env GOOS)}"
ARTEX_TARGET_ARCH="${ARTEX_TARGET_ARCH:-$(GOSUMDB="$ARTEX_GOSUMDB" go env GOARCH)}"
ARTEX_TARGETS="${ARTEX_TARGETS:-${ARTEX_TARGET_OS}/${ARTEX_TARGET_ARCH}}"
fi
if [ "${ARTEX_SKIP_FRONTEND:-0}" = "1" ]; then
[ -d server/webui/dist ] || die "ARTEX_SKIP_FRONTEND=1 但 server/webui/dist 不存在"
else
command -v npm >/dev/null 2>&1 || die "未检测到 npm(前端静态构建需要 Node.js/npm)"
command -v rsync >/dev/null 2>&1 || die "未检测到 rsync"
info "构建前端静态资源"
if [ "${ARTEX_SKIP_NPM_CI:-0}" != "1" ]; then
(cd web && npm ci)
fi
(cd web && npm run build:static)
info "同步前端资源到 server/webui/dist"
mkdir -p server/webui/dist
rsync -a --delete web/out/ server/webui/dist/
fi
compress_binary() {
binary="$1"
goos="$2"
case "$ARTEX_COMPRESS" in
0|off|false|none)
info "跳过 UPX:$binary"
return 0
;;
auto|required|true|1) ;;
*) die "ARTEX_COMPRESS 必须是 off、auto 或 required" ;;
esac
if ! command -v upx >/dev/null 2>&1; then
if [ "$ARTEX_COMPRESS" = "required" ]; then
die "ARTEX_COMPRESS=required 但未检测到 upx"
fi
warn "未检测到 upx,保留 linker 压缩结果:$binary"
return 0
fi
before=$(wc -c < "$binary" | tr -d ' ')
upx_args="$ARTEX_UPX_ARGS"
[ "$goos" = "darwin" ] && upx_args="$upx_args --force-macos"
# shellcheck disable=SC2086
if ! upx $upx_args -- "$binary"; then
if [ "$ARTEX_COMPRESS" = "required" ]; then
die "UPX 压缩失败:$binary"
fi
warn "UPX 不支持该目标格式,保留未压缩二进制:$binary"
return 0
fi
after=$(wc -c < "$binary" | tr -d ' ')
ok "UPX 压缩完成:$binary (${before} -> ${after} bytes)"
}
package_binary() {
binary="$1"
goos="$2"
goarch="$3"
package_name="artex-${ARTEX_BUILD_VERSION}-${goos}-${goarch}"
package_root="${ARTEX_PACKAGE_DIR}/${package_name}"
archive="${ARTEX_PACKAGE_DIR}/${package_name}.zip"
command -v zip >/dev/null 2>&1 || die "打包需要 zip"
rm -rf "$package_root" "$archive"
mkdir -p "$package_root"
cp "$binary" "$package_root/"
# 守护启动脚本是正式入口:页面上的一键更新要靠它在进程退出后重新拉起,
# 直接跑 artex 本体的话更新完就再也起不来了。按目标系统只带对应的那一份。
if [ "$goos" = "windows" ]; then
cp start.bat "$package_root/"
else
cp start.sh "$package_root/"
chmod +x "$package_root/start.sh"
fi
cp -R skills "$package_root/"
cp config.example.json "$package_root/"
if [ -f README.md ]; then cp README.md "$package_root/"; fi
(cd "$ARTEX_PACKAGE_DIR" && zip -q -r -9 "$(basename "$archive")" "$(basename "$package_root")")
rm -rf "$package_root"
ok "Release 压缩包:$archive"
}
build_target() {
target="$1"
case "$target" in
*/*) ;;
*) die "无效目标:$target(必须是 OS/ARCH)" ;;
esac
goos="${target%%/*}"
goarch="${target##*/}"
case "$goos" in
linux|darwin|windows) ;;
*) die "不支持的系统:$goos(支持 linux、darwin、windows)" ;;
esac
binary_name="artex"
[ "$goos" = "windows" ] && binary_name="artex.exe"
if [ -n "${ARTEX_OUTPUT:-}" ] && [ "$ARTEX_RELEASE" != "1" ]; then
output="$ARTEX_OUTPUT"
else
output="${ARTEX_OUTPUT_DIR}/artex-${goos}-${goarch}/${binary_name}"
fi
mkdir -p "$(dirname "$output")"
info "编译 ${goos}/${goarch},版本 ${ARTEX_BUILD_VERSION}"
GOSUMDB="$ARTEX_GOSUMDB" \
CGO_ENABLED=0 \
GOOS="$goos" \
GOARCH="$goarch" \
go build \
-tags embedui \
-trimpath \
-ldflags "-s -w -buildid= -X main.version=${ARTEX_BUILD_VERSION}" \
-o "$output" \
./cmd/artex
compress_binary "$output" "$goos"
if command -v file >/dev/null 2>&1; then file "$output"; fi
if [ "$ARTEX_PACKAGE" = "1" ]; then package_binary "$output" "$goos" "$goarch"; fi
ok "编译完成:$output"
}
write_checksums() {
[ "$ARTEX_PACKAGE" = "1" ] || return 0
checksum_file="$ARTEX_PACKAGE_DIR/SHA256SUMS"
if command -v sha256sum >/dev/null 2>&1; then
(cd "$ARTEX_PACKAGE_DIR" && for archive in *.zip; do sha256sum "$archive"; done > "$(basename "$checksum_file")")
elif command -v shasum >/dev/null 2>&1; then
(cd "$ARTEX_PACKAGE_DIR" && for archive in *.zip; do shasum -a 256 "$archive"; done > "$(basename "$checksum_file")")
else
warn "未检测到 sha256sum 或 shasum,跳过 SHA256SUMS"
return 0
fi
ok "校验文件:$checksum_file"
}
mkdir -p "$ARTEX_OUTPUT_DIR"
if [ "$ARTEX_PACKAGE" = "1" ]; then mkdir -p "$ARTEX_PACKAGE_DIR"; fi
old_ifs="$IFS"
IFS=','
read -r -a targets <<< "$ARTEX_TARGETS"
IFS="$old_ifs"
[ "${#targets[@]}" -gt 0 ] || die "ARTEX_TARGETS 不能为空"
for target in "${targets[@]}"; do
target="${target//[[:space:]]/}"
[ -n "$target" ] || continue
build_target "$target"
done
if [ "$ARTEX_PACKAGE" = "1" ]; then
write_checksums
info "Release 包已生成于:$ARTEX_PACKAGE_DIR"
fi