mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-27 17:03:49 +08:00
系统配置页新增「版本与更新」卡片,顶栏版本号旁在有新版时亮出提示胶囊。 更新链路:下载发布包 → 比对 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/;更新即重启会中断运行中的任务。
264 lines
8.9 KiB
Bash
Executable File
264 lines
8.9 KiB
Bash
Executable File
#!/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
|