From dfcf33f9f5553d6b62e330f352d63d28d474693b Mon Sep 17 00:00:00 2001 From: pphua Date: Sat, 22 Aug 2026 13:33:34 +0800 Subject: [PATCH] build: add compressed cross-platform release artifacts --- .github/workflows/release.yml | 43 +++--- CHANGELOG.md | 5 +- README.md | 16 ++ build.sh | 267 ++++++++++++++++++++++++++++------ 4 files changed, 268 insertions(+), 63 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4d82262..d58936e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,36 +48,32 @@ jobs: with: name: web-dist path: server/webui/dist - - name: Build + - name: Install UPX + run: | + sudo apt-get update + sudo apt-get install -y upx-ucl + - name: Build and package with build.sh env: - GOOS: ${{ matrix.goos }} - GOARCH: ${{ matrix.goarch }} - CGO_ENABLED: "0" + ARTEX_TARGET_OS: ${{ matrix.goos }} + ARTEX_TARGET_ARCH: ${{ matrix.goarch }} + ARTEX_BUILD_VERSION: ${{ github.ref_name }} + ARTEX_SKIP_FRONTEND: "1" + ARTEX_SKIP_NPM_CI: "1" + ARTEX_COMPRESS: required + ARTEX_PACKAGE: "1" + ARTEX_PACKAGE_DIR: dist run: | - BIN=artex - VERSION="${GITHUB_REF_NAME#v}" - [ "$GOOS" = "windows" ] && BIN=artex.exe - go build -tags embedui -trimpath \ - -ldflags "-s -w -X main.version=${VERSION}" \ - -o "$BIN" ./cmd/artex - - name: Package - run: | - NAME="artex-${GITHUB_REF_NAME}-${{ matrix.goos }}-${{ matrix.goarch }}" - mkdir -p "$NAME" - cp -r skills "$NAME"/ - cp config.example.json "$NAME"/ - if [ "${{ matrix.goos }}" = "windows" ]; then cp artex.exe "$NAME"/; else cp artex "$NAME"/; fi - zip -r "$NAME.zip" "$NAME" + ./build.sh --target "${ARTEX_TARGET_OS}/${ARTEX_TARGET_ARCH}" - uses: actions/upload-artifact@v4 with: name: pkg-${{ matrix.goos }}-${{ matrix.goarch }} - path: "*.zip" + path: "dist/*.zip" # Linux 生二进制单独上传,供 docker job 复用(免在镜像里重编前端+Go) - if: matrix.goos == 'linux' uses: actions/upload-artifact@v4 with: name: bin-linux-${{ matrix.goarch }} - path: artex + path: dist/artex-linux-${{ matrix.goarch }}/artex # ③ 汇总所有 zip → 创建 GitHub Release release: @@ -89,9 +85,14 @@ jobs: pattern: pkg-* merge-multiple: true path: dist + - name: Generate checksums + working-directory: dist + run: sha256sum *.zip > SHA256SUMS - uses: softprops/action-gh-release@v2 with: - files: dist/*.zip + files: | + dist/*.zip + dist/SHA256SUMS generate_release_notes: true # ④ 多架构镜像 → 推 Docker Hub(复用 binaries 编好的 Linux 二进制, diff --git a/CHANGELOG.md b/CHANGELOG.md index 16680c1..7be73df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,10 @@ ## [Unreleased] -暂无未发布变更。 +### Build + +- `build.sh --release` 支持一次构建 Linux amd64/arm64、macOS amd64/arm64 和 Windows amd64,并生成带 `skills/`、配置示例和 README 的 zip 发布包。 +- 使用 Go linker 去除调试信息,并在可用时使用 UPX 压缩二进制;Release Workflow 将 UPX 压缩结果和 `SHA256SUMS` 发布到 GitHub Release。 ## [0.3.3] - 2026-08-21 diff --git a/README.md b/README.md index d7ab094..e5b57c1 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,22 @@ CGO_ENABLED=0 go build -tags embedui -o artex ./cmd/artex ./artex ``` +### 方式五:构建跨平台 Release 压缩包 + +`build.sh` 会先构建并嵌入前端,再使用 Go linker 去除调试信息,并在本机安装了 UPX 时进一步压缩二进制。Release 模式默认生成 Linux amd64/arm64、macOS amd64/arm64 和 Windows amd64 的 zip 包: + +```bash +./build.sh --release +# 产物:dist/artex-0.3.3-*.zip +``` + +没有 UPX 时脚本仍可生成 linker 压缩后的二进制;CI Release 会将 UPX 视为必需依赖。可用 `ARTEX_TARGETS` 自定义目标,或用 `--no-compress` 禁用 UPX: + +```bash +ARTEX_TARGETS=linux/amd64,windows/amd64 ./build.sh --release +./build.sh --target darwin/arm64 --no-compress +``` + --- ## 更新升级 diff --git a/build.sh b/build.sh index e43bbd3..8e9b457 100755 --- a/build.sh +++ b/build.sh @@ -1,59 +1,244 @@ #!/usr/bin/env bash -# ARTEX 单二进制构建脚本:静态导出前端并嵌入 Go 二进制。 +# ARTEX cross-platform release builder. # -# 可选环境变量: -# ARTEX_TARGET_OS=linux 目标系统,默认当前系统 -# ARTEX_TARGET_ARCH=amd64 目标架构,默认当前架构 -# ARTEX_BUILD_VERSION=0.3.3 构建版本,默认使用当前应用版本 -# ARTEX_OUTPUT=/path/to/artex 输出路径 -# ARTEX_SKIP_NPM_CI=1 跳过 npm ci,复用现有依赖 -# ARTEX_GOSUMDB=sum.golang.org Go checksum database +# 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=0.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=auto UPX mode: auto, required, or 0/off. +# 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 + --no-compress 不使用 UPX,仅保留 Go linker 压缩 + --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:-auto}" +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 + ;; + --help|-h) + usage + exit 0 + ;; + *) die "未知参数:$1(使用 --help 查看用法)" ;; + esac +done + command -v go >/dev/null 2>&1 || die "未检测到 Go(项目需要 Go 1.26 或更高版本)" -command -v npm >/dev/null 2>&1 || die "未检测到 npm(前端静态构建需要 Node.js/npm)" -command -v rsync >/dev/null 2>&1 || die "未检测到 rsync" ARTEX_GOSUMDB="${ARTEX_GOSUMDB:-sum.golang.org}" -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_BUILD_VERSION="${ARTEX_BUILD_VERSION:-0.3.3}" +# 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}" -ARTEX_BINARY_NAME="artex" -[ "$ARTEX_TARGET_OS" = "windows" ] && ARTEX_BINARY_NAME="artex.exe" -ARTEX_OUTPUT="${ARTEX_OUTPUT:-dist/artex-${ARTEX_TARGET_OS}-${ARTEX_TARGET_ARCH}/${ARTEX_BINARY_NAME}}" - -info "构建前端静态资源" -if [ "${ARTEX_SKIP_NPM_CI:-0}" != "1" ]; then - (cd web && npm ci) +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 -(cd web && npm run build:static) -info "同步前端资源到 server/webui/dist" -mkdir -p server/webui/dist -rsync -a --delete web/out/ server/webui/dist/ - -info "编译 ${ARTEX_TARGET_OS}/${ARTEX_TARGET_ARCH},版本 ${ARTEX_BUILD_VERSION}" -mkdir -p "$(dirname "$ARTEX_OUTPUT")" -GOSUMDB="$ARTEX_GOSUMDB" \ -CGO_ENABLED=0 \ -GOOS="$ARTEX_TARGET_OS" \ -GOARCH="$ARTEX_TARGET_ARCH" \ -go build \ - -tags embedui \ - -trimpath \ - -ldflags "-s -w -X main.version=${ARTEX_BUILD_VERSION}" \ - -o "$ARTEX_OUTPUT" \ - ./cmd/artex - -ok "编译完成:$ARTEX_OUTPUT" -if command -v file >/dev/null 2>&1; then - file "$ARTEX_OUTPUT" +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 必须是 auto、required 或 0/off" ;; + 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/" + 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