1
0

2 Commits

Author SHA1 Message Date
993c0a72d6 chore: 版本升迁 v0.1.3 2026-05-05 12:38:12 +08:00
c9c3a84b33 feat: 扩展发布打包支持多组件多架构多格式产物
- 新增 web 组件独立发布为 nex-web_<version>.tar.gz
- server 新增 arm64 架构、macOS universal、Windows arm64 产物
- desktop 新增 arm64 架构支持(Linux/Windows)
- Linux desktop 新增 AppImage、deb、rpm 安装包格式
- macOS desktop 新增 unsigned DMG 安装包
- 统一发布资产命名为 {component}_{version}_{platform}_{arch}.{ext}
- 新增 SHA256SUMS 校验和清单覆盖全部发布资产
- versionctl 新增 asset-name CLI 支持按参数生成资产文件名
- Makefile release target 重构为组件/平台/架构参数化
- GitHub Actions release workflow 扩展多组件多架构构建矩阵
- 同步更新 openspec 主规范(desktop-app/release-pipeline/workspace-command-flows)
2026-05-05 12:36:33 +08:00
18 changed files with 811 additions and 213 deletions

View File

@@ -37,8 +37,8 @@ jobs:
go run ./versionctl verify-tag "${GITHUB_REF_NAME}" go run ./versionctl verify-tag "${GITHUB_REF_NAME}"
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT" printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"
build-linux: build-web:
name: Build Linux Assets name: Build Web Asset
needs: prepare needs: prepare
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -60,14 +60,65 @@ jobs:
- name: Setup Bun - name: Setup Bun
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2
- name: Install Linux desktop build dependencies - name: Preflight web release toolchain
run: |
set -euo pipefail
command -v go
go version
command -v bun
bun --version
make release-assets-check
- name: Build web release asset
run: make release-assets-web
- name: Upload web release asset
uses: actions/upload-artifact@v4
with:
name: release-web
path: build/release/*
if-no-files-found: error
build-linux:
name: Build Linux ${{ matrix.arch }} Assets
needs: prepare
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v5
with:
lfs: true
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: go.work
cache-dependency-path: |
backend/go.sum
versionctl/go.sum
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install Linux desktop and package dependencies
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libayatana-appindicator3-dev libgtk-3-dev sudo apt-get install -y curl file libayatana-appindicator3-dev libgtk-3-dev rpm
- name: Preflight Linux release toolchain - name: Preflight Linux release toolchain
run: | run: |
set -euo pipefail set -euo pipefail
printf 'runner arch: %s\n' "$(uname -m)"
command -v go command -v go
go version go version
command -v bun command -v bun
@@ -77,21 +128,42 @@ jobs:
command -v pkg-config command -v pkg-config
pkg-config --modversion ayatana-appindicator3-0.1 pkg-config --modversion ayatana-appindicator3-0.1
pkg-config --modversion gtk+-3.0 pkg-config --modversion gtk+-3.0
command -v curl
command -v dpkg-deb
dpkg-deb --version
command -v rpmbuild
rpmbuild --version
make release-assets-check make release-assets-check
- name: Build Linux release assets - name: Build Linux release assets
run: make release-assets-linux run: make release-assets-linux TARGET_ARCH=${{ matrix.arch }}
- name: Upload Linux release assets - name: Upload Linux release assets
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: release-linux name: release-linux-${{ matrix.arch }}
path: build/release/* path: build/release/*
if-no-files-found: error
build-windows: build-windows:
name: Build Windows Assets name: Build Windows ${{ matrix.arch }} Assets
needs: prepare needs: prepare
runs-on: windows-latest runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
msystem: MINGW64
packages: >-
make
mingw-w64-x86_64-gcc
- arch: arm64
msystem: CLANGARM64
packages: >-
make
mingw-w64-clang-aarch64-clang
mingw-w64-clang-aarch64-llvm
permissions: permissions:
contents: read contents: read
steps: steps:
@@ -114,12 +186,10 @@ jobs:
- name: Setup MSYS2 toolchain - name: Setup MSYS2 toolchain
uses: msys2/setup-msys2@v2 uses: msys2/setup-msys2@v2
with: with:
msystem: MINGW64 msystem: ${{ matrix.msystem }}
path-type: inherit path-type: inherit
update: true update: true
install: >- install: ${{ matrix.packages }}
make
mingw-w64-x86_64-gcc
- name: Preflight Windows release toolchain - name: Preflight Windows release toolchain
shell: msys2 {0} shell: msys2 {0}
@@ -131,10 +201,21 @@ jobs:
bun --version bun --version
command -v make command -v make
make --version make --version
command -v gcc if [ "${{ matrix.arch }}" = "arm64" ]; then
gcc --version command -v clang
command -v windres clang --version
windres --version if command -v llvm-windres >/dev/null 2>&1; then
llvm-windres --version
else
command -v windres
windres --version
fi
else
command -v gcc
gcc --version
command -v windres
windres --version
fi
if command -v powershell.exe >/dev/null 2>&1; then if command -v powershell.exe >/dev/null 2>&1; then
powershell.exe -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' powershell.exe -NoProfile -Command '$PSVersionTable.PSVersion.ToString()'
else else
@@ -145,18 +226,19 @@ jobs:
- name: Build Windows release assets - name: Build Windows release assets
shell: msys2 {0} shell: msys2 {0}
run: make release-assets-windows run: make release-assets-windows TARGET_ARCH=${{ matrix.arch }}
- name: Upload Windows release assets - name: Upload Windows release assets
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: release-windows name: release-windows-${{ matrix.arch }}
path: build/release/* path: build/release/*
if-no-files-found: error
build-macos: build-macos:
name: Build macOS Assets name: Build macOS Assets
needs: prepare needs: prepare
runs-on: macos-latest runs-on: macos-15
permissions: permissions:
contents: read contents: read
steps: steps:
@@ -179,11 +261,13 @@ jobs:
- name: Preflight macOS release toolchain - name: Preflight macOS release toolchain
run: | run: |
set -euo pipefail set -euo pipefail
printf 'runner arch: %s\n' "$(uname -m)"
command -v go command -v go
go version go version
command -v bun command -v bun
bun --version bun --version
command -v ditto command -v ditto
command -v hdiutil
xcrun --find lipo xcrun --find lipo
xcrun --find vtool xcrun --find vtool
make release-assets-check make release-assets-check
@@ -196,14 +280,18 @@ jobs:
with: with:
name: release-macos name: release-macos
path: build/release/* path: build/release/*
if-no-files-found: error
draft-release: draft-release:
name: Create Draft Release name: Create Draft Release
needs: [prepare, build-linux, build-windows, build-macos] needs: [prepare, build-web, build-linux, build-windows, build-macos]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
steps: steps:
- name: Checkout
uses: actions/checkout@v5
- name: Download release assets - name: Download release assets
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
@@ -211,6 +299,9 @@ jobs:
merge-multiple: true merge-multiple: true
path: dist path: dist
- name: Generate checksums
run: make release-assets-checksums RELEASE_DIR=dist
- name: Publish draft release - name: Publish draft release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:

286
Makefile
View File

@@ -4,12 +4,17 @@
server-run server-build server-lint server-test server-clean \ server-run server-build server-lint server-test server-clean \
desktop-build-mac desktop-build-win desktop-build-linux \ desktop-build-mac desktop-build-win desktop-build-linux \
desktop-lint desktop-test desktop-clean \ desktop-lint desktop-test desktop-clean \
release-assets-check release-assets-linux release-assets-windows release-assets-macos \ release-assets-check release-assets-web release-assets-linux release-assets-windows release-assets-macos release-assets-checksums \
release-assets-server-linux release-assets-server-windows release-assets-server-macos \
release-assets-desktop-linux release-assets-desktop-windows release-assets-desktop-macos \
_backend-lint _backend-test _backend-clean _backend-build \ _backend-lint _backend-test _backend-clean _backend-build \
_versionctl-lint _versionctl-test \ _versionctl-lint _versionctl-test \
_frontend-install _frontend-build _frontend-check _frontend-test _frontend-dev _frontend-clean \ _frontend-install _frontend-build _frontend-check _frontend-test _frontend-dev _frontend-clean \
_desktop-test _desktop-clean _desktop-prepare-frontend _desktop-prepare-embedfs _desktop-prepare-windows-resource \ _desktop-test _desktop-clean _desktop-prepare-frontend _desktop-prepare-embedfs _desktop-prepare-windows-resource \
_server-run-backend _server-run-frontend _server-run-backend _server-run-frontend \
_check-linux-target-arch _check-windows-target-arch _ensure-appimagetool \
_package-linux-tar _package-linux-appimage _package-linux-deb _package-linux-rpm \
_package-macos-zip _package-macos-dmg
# Delay shell lookups until a target needs them, then cache the result for this make run. # Delay shell lookups until a target needs them, then cache the result for this make run.
lazy_shell = $(or $($(1)),$(eval $(1) := $(shell $(2)))$($(1))) lazy_shell = $(or $($(1)),$(eval $(1) := $(shell $(2)))$($(1)))
@@ -17,16 +22,32 @@ lazy_shell = $(or $($(1)),$(eval $(1) := $(shell $(2)))$($(1)))
VERSION = $(call lazy_shell,_VERSION,go run ./versionctl print) VERSION = $(call lazy_shell,_VERSION,go run ./versionctl print)
GIT_COMMIT ?= $(call lazy_shell,_GIT_COMMIT,git rev-parse --short HEAD 2>/dev/null || printf 'unknown') GIT_COMMIT ?= $(call lazy_shell,_GIT_COMMIT,git rev-parse --short HEAD 2>/dev/null || printf 'unknown')
BUILD_TIME ?= $(call lazy_shell,_BUILD_TIME,date -u +"%Y-%m-%dT%H:%M:%SZ") BUILD_TIME ?= $(call lazy_shell,_BUILD_TIME,date -u +"%Y-%m-%dT%H:%M:%SZ")
TARGET_ARCH ?= $(call lazy_shell,_TARGET_ARCH,go env GOARCH)
GO_LDFLAGS = -X nex/backend/pkg/buildinfo.version=$(VERSION) -X nex/backend/pkg/buildinfo.commit=$(GIT_COMMIT) -X nex/backend/pkg/buildinfo.buildTime=$(BUILD_TIME) GO_LDFLAGS = -X nex/backend/pkg/buildinfo.version=$(VERSION) -X nex/backend/pkg/buildinfo.commit=$(GIT_COMMIT) -X nex/backend/pkg/buildinfo.buildTime=$(BUILD_TIME)
GO_LDFLAGS_WIN = $(GO_LDFLAGS) -H=windowsgui GO_LDFLAGS_WIN = $(GO_LDFLAGS) -H=windowsgui
RELEASE_DIR := build/release RELEASE_DIR ?= build/release
SERVER_LINUX_ASSET = $(call lazy_shell,_SERVER_LINUX_ASSET,go run ./versionctl asset-name server linux amd64) LINUX_DESKTOP_BINARY = build/nex-linux-$(TARGET_ARCH)
SERVER_WINDOWS_ASSET = $(call lazy_shell,_SERVER_WINDOWS_ASSET,go run ./versionctl asset-name server windows amd64) WINDOWS_DESKTOP_BINARY = build/nex-win-$(TARGET_ARCH).exe
SERVER_DARWIN_AMD64_ASSET = $(call lazy_shell,_SERVER_DARWIN_AMD64_ASSET,go run ./versionctl asset-name server darwin amd64) WINDOWS_SERVER_BINARY = build/nex-server-windows-$(TARGET_ARCH).exe
SERVER_DARWIN_ARM64_ASSET = $(call lazy_shell,_SERVER_DARWIN_ARM64_ASSET,go run ./versionctl asset-name server darwin arm64) WINDRES ?= windres
DESKTOP_LINUX_ASSET = $(call lazy_shell,_DESKTOP_LINUX_ASSET,go run ./versionctl asset-name desktop linux)
DESKTOP_WINDOWS_ASSET = $(call lazy_shell,_DESKTOP_WINDOWS_ASSET,go run ./versionctl asset-name desktop windows) ifeq ($(TARGET_ARCH),arm64)
DESKTOP_MACOS_ASSET = $(call lazy_shell,_DESKTOP_MACOS_ASSET,go run ./versionctl asset-name desktop macos) APPIMAGE_ARCH := aarch64
DEB_ARCH := arm64
RPM_ARCH := aarch64
WINDOWS_WINDRES_FORMAT := pe-aarch64
WINDOWS_RESOURCE := rsrc_windows_arm64.syso
else
APPIMAGE_ARCH := x86_64
DEB_ARCH := amd64
RPM_ARCH := x86_64
WINDOWS_WINDRES_FORMAT := pe-x86-64
WINDOWS_RESOURCE := rsrc_windows_amd64.syso
endif
APPIMAGETOOL_PATH := build/tools/appimagetool-$(APPIMAGE_ARCH).AppImage
APPIMAGETOOL_URL ?= https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-$(APPIMAGE_ARCH).AppImage
APPIMAGETOOL ?= $(APPIMAGETOOL_PATH)
# ============================================ # ============================================
# 全局命令 # 全局命令
@@ -94,13 +115,16 @@ desktop-build-mac: version-check _desktop-prepare-frontend _desktop-prepare-embe
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-mac-arm64 ./cmd/desktop cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-mac-arm64 ./cmd/desktop
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-mac-amd64 ./cmd/desktop cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-mac-amd64 ./cmd/desktop
lipo -create build/nex-mac-arm64 build/nex-mac-amd64 -output build/nex-mac-universal lipo -create build/nex-mac-arm64 build/nex-mac-amd64 -output build/nex-mac-universal
lipo -info build/nex-mac-universal | grep -q 'x86_64 arm64'
@printf 'Packaging macOS app bundle...\n' @printf 'Packaging macOS app bundle...\n'
rm -rf build/Nex.app
mkdir -p build/Nex.app/Contents/MacOS build/Nex.app/Contents/Resources mkdir -p build/Nex.app/Contents/MacOS build/Nex.app/Contents/Resources
cp build/nex-mac-universal build/Nex.app/Contents/MacOS/nex cp build/nex-mac-universal build/Nex.app/Contents/MacOS/nex
@if [ -f assets/icon.icns ]; then \ @if [ -f assets/icon.icns ]; then \
cp assets/icon.icns build/Nex.app/Contents/Resources/; \ cp assets/icon.icns build/Nex.app/Contents/Resources/; \
else \ else \
printf 'Missing assets/icon.icns\n'; \ printf 'Missing assets/icon.icns\n'; \
exit 1; \
fi fi
@MIN_MACOS_VERSION=$$(vtool -show-build build/nex-mac-universal | awk '/minos / {print $$2; exit}'); \ @MIN_MACOS_VERSION=$$(vtool -show-build build/nex-mac-universal | awk '/minos / {print $$2; exit}'); \
if [ -z "$$MIN_MACOS_VERSION" ]; then \ if [ -z "$$MIN_MACOS_VERSION" ]; then \
@@ -111,20 +135,16 @@ desktop-build-mac: version-check _desktop-prepare-frontend _desktop-prepare-embe
chmod +x build/Nex.app/Contents/MacOS/nex chmod +x build/Nex.app/Contents/MacOS/nex
@printf 'macOS desktop build complete\n' @printf 'macOS desktop build complete\n'
desktop-build-win: version-check _desktop-prepare-frontend _desktop-prepare-embedfs _desktop-prepare-windows-resource desktop-build-win: version-check _desktop-prepare-frontend _desktop-prepare-embedfs _desktop-prepare-windows-resource _check-windows-target-arch
@printf 'Building Windows desktop...\n' @printf 'Building Windows desktop $(TARGET_ARCH)...\n'
ifeq ($(OS),Windows_NT)
powershell -NoProfile -Command "New-Item -ItemType Directory -Path 'build' -Force | Out-Null"
cd backend && set "CGO_ENABLED=1"&& set "GOOS=windows"&& set "GOARCH=amd64"&& go build -ldflags "$(GO_LDFLAGS_WIN)" -o ../build/nex-win-amd64.exe ./cmd/desktop
else
mkdir -p build mkdir -p build
cd backend && CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS_WIN)" -o ../build/nex-win-amd64.exe ./cmd/desktop cd backend && CGO_ENABLED=1 GOOS=windows GOARCH=$(TARGET_ARCH) go build -ldflags "$(GO_LDFLAGS_WIN)" -o ../$(WINDOWS_DESKTOP_BINARY) ./cmd/desktop
endif
@printf 'Windows desktop build complete\n' @printf 'Windows desktop build complete\n'
desktop-build-linux: version-check _desktop-prepare-frontend _desktop-prepare-embedfs desktop-build-linux: version-check _desktop-prepare-frontend _desktop-prepare-embedfs _check-linux-target-arch
@printf 'Building Linux desktop...\n' @printf 'Building Linux desktop $(TARGET_ARCH)...\n'
cd backend && CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-linux-amd64 ./cmd/desktop mkdir -p build
cd backend && CGO_ENABLED=1 GOOS=linux GOARCH=$(TARGET_ARCH) go build -ldflags "$(GO_LDFLAGS)" -o ../$(LINUX_DESKTOP_BINARY) ./cmd/desktop
@printf 'Linux desktop build complete\n' @printf 'Linux desktop build complete\n'
desktop-lint: _backend-lint _frontend-check desktop-lint: _backend-lint _frontend-check
@@ -140,44 +160,29 @@ _desktop-test:
cd backend && go test ./cmd/desktop/... -v cd backend && go test ./cmd/desktop/... -v
_desktop-clean: _desktop-clean:
rm -rf build/ embedfs/assets embedfs/frontend-dist backend/cmd/desktop/rsrc_windows_amd64.syso rm -rf build/ embedfs/assets embedfs/frontend-dist backend/cmd/desktop/rsrc_windows_amd64.syso backend/cmd/desktop/rsrc_windows_arm64.syso
_desktop-prepare-frontend: _frontend-install _desktop-prepare-frontend: _frontend-install
@printf 'Preparing frontend for desktop...\n' @printf 'Preparing frontend for desktop...\n'
ifeq ($(OS),Windows_NT)
powershell -NoProfile -Command "Copy-Item -LiteralPath 'frontend/.env.desktop' -Destination 'frontend/.env.production.local' -Force"
cd frontend && bun run build
powershell -NoProfile -Command "Remove-Item -LiteralPath 'frontend/.env.production.local' -Force -ErrorAction SilentlyContinue"
else
cd frontend && cp .env.desktop .env.production.local cd frontend && cp .env.desktop .env.production.local
cd frontend && bun run build cd frontend && bun run build
rm -f frontend/.env.production.local rm -f frontend/.env.production.local
endif
_desktop-prepare-embedfs: _desktop-prepare-embedfs:
@printf 'Preparing embedded filesystem...\n' @printf 'Preparing embedded filesystem...\n'
ifeq ($(OS),Windows_NT)
powershell -NoProfile -Command "Remove-Item -LiteralPath 'embedfs/assets' -Recurse -Force -ErrorAction SilentlyContinue; Remove-Item -LiteralPath 'embedfs/frontend-dist' -Recurse -Force -ErrorAction SilentlyContinue; Copy-Item -LiteralPath 'assets' -Destination 'embedfs/assets' -Recurse; Copy-Item -LiteralPath 'frontend/dist' -Destination 'embedfs/frontend-dist' -Recurse"
else
rm -rf embedfs/assets embedfs/frontend-dist rm -rf embedfs/assets embedfs/frontend-dist
cp -r assets embedfs/assets cp -r assets embedfs/assets
cp -r frontend/dist embedfs/frontend-dist cp -r frontend/dist embedfs/frontend-dist
endif
_desktop-prepare-windows-resource: _desktop-prepare-windows-resource: _check-windows-target-arch
@printf 'Preparing Windows executable icon...\n' @printf 'Preparing Windows $(TARGET_ARCH) executable icon...\n'
ifeq ($(OS),Windows_NT) @if [ "$(TARGET_ARCH)" = "arm64" ] && [ "$(WINDRES)" = "windres" ] && command -v llvm-windres >/dev/null 2>&1; then \
cd backend/cmd/desktop && windres -O coff -F pe-x86-64 -i icon_windows.rc -o rsrc_windows_amd64.syso WINDRES_CMD=llvm-windres; \
else
@if command -v x86_64-w64-mingw32-windres >/dev/null 2>&1; then \
cd backend/cmd/desktop && x86_64-w64-mingw32-windres -O coff -F pe-x86-64 -i icon_windows.rc -o rsrc_windows_amd64.syso; \
elif command -v windres >/dev/null 2>&1; then \
cd backend/cmd/desktop && windres -O coff -F pe-x86-64 -i icon_windows.rc -o rsrc_windows_amd64.syso; \
else \ else \
printf 'Missing windres for Windows icon resource generation\n'; \ WINDRES_CMD="$(WINDRES)"; \
exit 1; \ fi; \
fi command -v "$$WINDRES_CMD" >/dev/null 2>&1 || { printf 'Missing windres tool: %s\n' "$$WINDRES_CMD"; exit 1; }; \
endif cd backend/cmd/desktop && "$$WINDRES_CMD" -O coff -F $(WINDOWS_WINDRES_FORMAT) -i icon_windows.rc -o $(WINDOWS_RESOURCE)
# ============================================ # ============================================
# 发布资产 # 发布资产
@@ -187,32 +192,181 @@ release-assets-check:
go run ./versionctl release-assets-check go run ./versionctl release-assets-check
@printf 'Release assets check passed\n' @printf 'Release assets check passed\n'
release-assets-linux: version-check release-assets-check desktop-build-linux release-assets-web: version-check release-assets-check _frontend-build
rm -rf "$(RELEASE_DIR)" rm -rf "$(RELEASE_DIR)"
mkdir -p "$(RELEASE_DIR)" mkdir -p "$(RELEASE_DIR)"
cd backend && CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-server-linux-amd64 ./cmd/server asset=$$(go run ./versionctl asset-name web tar.gz); \
tar -C build -czf "$(RELEASE_DIR)/$(SERVER_LINUX_ASSET)" nex-server-linux-amd64 tar -C frontend -czf "$(RELEASE_DIR)/$$asset" dist
tar -C build -czf "$(RELEASE_DIR)/$(DESKTOP_LINUX_ASSET)" nex-linux-amd64
release-assets-windows: version-check release-assets-check desktop-build-win release-assets-linux: version-check release-assets-check _check-linux-target-arch
ifeq ($(OS),Windows_NT)
powershell -NoProfile -Command "Remove-Item -LiteralPath '$(RELEASE_DIR)' -Recurse -Force -ErrorAction SilentlyContinue; New-Item -ItemType Directory -Path '$(RELEASE_DIR)' -Force | Out-Null"
cd backend && set "CGO_ENABLED=1"&& set "GOOS=windows"&& set "GOARCH=amd64"&& go build -ldflags "$(GO_LDFLAGS_WIN)" -o ../build/nex-server-win-amd64.exe ./cmd/server
powershell -NoProfile -Command "Compress-Archive -LiteralPath 'build/nex-server-win-amd64.exe' -DestinationPath '$(RELEASE_DIR)/$(SERVER_WINDOWS_ASSET)' -Force"
powershell -NoProfile -Command "Compress-Archive -LiteralPath 'build/nex-win-amd64.exe' -DestinationPath '$(RELEASE_DIR)/$(DESKTOP_WINDOWS_ASSET)' -Force"
else
@printf 'release-assets-windows requires Windows\n'
@exit 1
endif
release-assets-macos: version-check release-assets-check desktop-build-mac
rm -rf "$(RELEASE_DIR)" rm -rf "$(RELEASE_DIR)"
mkdir -p "$(RELEASE_DIR)" mkdir -p "$(RELEASE_DIR)"
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-server-darwin-amd64 ./cmd/server @$(MAKE) release-assets-server-linux TARGET_ARCH=$(TARGET_ARCH) RELEASE_DIR="$(RELEASE_DIR)"
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-server-darwin-arm64 ./cmd/server @$(MAKE) release-assets-desktop-linux TARGET_ARCH=$(TARGET_ARCH) RELEASE_DIR="$(RELEASE_DIR)"
tar -C build -czf "$(RELEASE_DIR)/$(SERVER_DARWIN_AMD64_ASSET)" nex-server-darwin-amd64
tar -C build -czf "$(RELEASE_DIR)/$(SERVER_DARWIN_ARM64_ASSET)" nex-server-darwin-arm64 release-assets-windows: version-check release-assets-check _check-windows-target-arch
ditto -c -k --keepParent build/Nex.app "$(RELEASE_DIR)/$(DESKTOP_MACOS_ASSET)" rm -rf "$(RELEASE_DIR)"
mkdir -p "$(RELEASE_DIR)"
@$(MAKE) release-assets-server-windows TARGET_ARCH=$(TARGET_ARCH) RELEASE_DIR="$(RELEASE_DIR)"
@$(MAKE) release-assets-desktop-windows TARGET_ARCH=$(TARGET_ARCH) RELEASE_DIR="$(RELEASE_DIR)"
release-assets-macos: version-check release-assets-check
rm -rf "$(RELEASE_DIR)"
mkdir -p "$(RELEASE_DIR)"
@$(MAKE) release-assets-server-macos RELEASE_DIR="$(RELEASE_DIR)"
@$(MAKE) release-assets-desktop-macos RELEASE_DIR="$(RELEASE_DIR)"
release-assets-server-linux: version-check _check-linux-target-arch
mkdir -p build "$(RELEASE_DIR)"
cd backend && CGO_ENABLED=1 GOOS=linux GOARCH=$(TARGET_ARCH) go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-server-linux-$(TARGET_ARCH) ./cmd/server
asset=$$(go run ./versionctl asset-name server linux $(TARGET_ARCH) tar.gz); \
tar -C build -czf "$(RELEASE_DIR)/$$asset" nex-server-linux-$(TARGET_ARCH)
release-assets-server-windows: version-check _check-windows-target-arch
mkdir -p build "$(RELEASE_DIR)"
cd backend && CGO_ENABLED=1 GOOS=windows GOARCH=$(TARGET_ARCH) go build -ldflags "$(GO_LDFLAGS)" -o ../$(WINDOWS_SERVER_BINARY) ./cmd/server
asset=$$(go run ./versionctl asset-name server windows $(TARGET_ARCH) zip); \
if command -v powershell.exe >/dev/null 2>&1; then POWERSHELL=powershell.exe; else POWERSHELL=powershell; fi; \
"$$POWERSHELL" -NoProfile -Command "Compress-Archive -LiteralPath '$(WINDOWS_SERVER_BINARY)' -DestinationPath '$(RELEASE_DIR)/$$asset' -Force"
release-assets-server-macos: version-check
mkdir -p build "$(RELEASE_DIR)"
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-server-macos-amd64 ./cmd/server
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-server-macos-arm64 ./cmd/server
lipo -create build/nex-server-macos-amd64 build/nex-server-macos-arm64 -output build/nex-server-macos-universal
lipo -info build/nex-server-macos-universal | grep -q 'x86_64 arm64'
asset=$$(go run ./versionctl asset-name server macos amd64 tar.gz); \
tar -C build -czf "$(RELEASE_DIR)/$$asset" nex-server-macos-amd64
asset=$$(go run ./versionctl asset-name server macos arm64 tar.gz); \
tar -C build -czf "$(RELEASE_DIR)/$$asset" nex-server-macos-arm64
asset=$$(go run ./versionctl asset-name server macos universal tar.gz); \
tar -C build -czf "$(RELEASE_DIR)/$$asset" nex-server-macos-universal
release-assets-desktop-linux: version-check release-assets-check desktop-build-linux _package-linux-tar _package-linux-appimage _package-linux-deb _package-linux-rpm
release-assets-desktop-windows: version-check release-assets-check desktop-build-win
mkdir -p "$(RELEASE_DIR)"
asset=$$(go run ./versionctl asset-name desktop windows $(TARGET_ARCH) zip); \
if command -v powershell.exe >/dev/null 2>&1; then POWERSHELL=powershell.exe; else POWERSHELL=powershell; fi; \
"$$POWERSHELL" -NoProfile -Command "Compress-Archive -LiteralPath '$(WINDOWS_DESKTOP_BINARY)' -DestinationPath '$(RELEASE_DIR)/$$asset' -Force"
release-assets-desktop-macos: version-check release-assets-check desktop-build-mac _package-macos-zip _package-macos-dmg
release-assets-checksums:
@cd "$(RELEASE_DIR)" && \
rm -f SHA256SUMS && \
for asset in *; do \
[ -f "$$asset" ] || continue; \
if command -v sha256sum >/dev/null 2>&1; then \
sha256sum "$$asset"; \
elif command -v shasum >/dev/null 2>&1; then \
shasum -a 256 "$$asset"; \
else \
printf 'Missing sha256sum or shasum\n' >&2; \
exit 1; \
fi; \
done > SHA256SUMS && \
test -s SHA256SUMS
_check-linux-target-arch:
@if [ "$(TARGET_ARCH)" != "amd64" ] && [ "$(TARGET_ARCH)" != "arm64" ]; then \
printf 'Unsupported Linux TARGET_ARCH: %s\n' "$(TARGET_ARCH)"; \
exit 1; \
fi
_check-windows-target-arch:
@if [ "$(TARGET_ARCH)" != "amd64" ] && [ "$(TARGET_ARCH)" != "arm64" ]; then \
printf 'Unsupported Windows TARGET_ARCH: %s\n' "$(TARGET_ARCH)"; \
exit 1; \
fi
_ensure-appimagetool:
@mkdir -p build/tools
@if [ ! -x "$(APPIMAGETOOL)" ]; then \
printf 'Downloading appimagetool for %s...\n' "$(APPIMAGE_ARCH)"; \
command -v curl >/dev/null 2>&1 || { printf 'Missing curl for appimagetool download\n'; exit 1; }; \
curl -L "$(APPIMAGETOOL_URL)" -o "$(APPIMAGETOOL)"; \
chmod +x "$(APPIMAGETOOL)"; \
fi; \
printf 'Using appimagetool: %s\n' "$(APPIMAGETOOL)"
_package-linux-tar:
mkdir -p "$(RELEASE_DIR)"
asset=$$(go run ./versionctl asset-name desktop linux $(TARGET_ARCH) tar.gz); \
tar -C build -czf "$(RELEASE_DIR)/$$asset" nex-linux-$(TARGET_ARCH)
_package-linux-appimage: _ensure-appimagetool
mkdir -p "$(RELEASE_DIR)"
appdir="build/appimage/nex-$(TARGET_ARCH).AppDir"; \
rm -rf "$$appdir"; \
mkdir -p "$$appdir/usr/bin" "$$appdir/usr/share/applications" "$$appdir/usr/share/icons"; \
install -m 0755 "$(LINUX_DESKTOP_BINARY)" "$$appdir/usr/bin/nex"; \
install -m 0644 packaging/linux/nex.desktop "$$appdir/nex.desktop"; \
install -m 0644 packaging/linux/nex.desktop "$$appdir/usr/share/applications/nex.desktop"; \
install -m 0755 packaging/linux/AppRun "$$appdir/AppRun"; \
cp -R assets/icons/hicolor "$$appdir/usr/share/icons/"; \
cp assets/icon.png "$$appdir/nex.png"; \
asset=$$(go run ./versionctl asset-name desktop linux $(TARGET_ARCH) AppImage); \
ARCH=$(APPIMAGE_ARCH) APPIMAGE_EXTRACT_AND_RUN=1 "$(APPIMAGETOOL)" "$$appdir" "$(RELEASE_DIR)/$$asset"; \
chmod +x "$(RELEASE_DIR)/$$asset"; \
test -s "$(RELEASE_DIR)/$$asset"
_package-linux-deb:
mkdir -p "$(RELEASE_DIR)"
pkgdir="build/pkg/deb/nex-$(TARGET_ARCH)"; \
rm -rf "$$pkgdir"; \
mkdir -p "$$pkgdir/DEBIAN" "$$pkgdir/usr/bin" "$$pkgdir/usr/share/applications" "$$pkgdir/usr/share/icons"; \
install -m 0755 "$(LINUX_DESKTOP_BINARY)" "$$pkgdir/usr/bin/nex"; \
install -m 0644 packaging/linux/nex.desktop "$$pkgdir/usr/share/applications/nex.desktop"; \
cp -R assets/icons/hicolor "$$pkgdir/usr/share/icons/"; \
printf '%s\n' \
'Package: nex' \
'Version: $(VERSION)' \
'Section: utils' \
'Priority: optional' \
'Architecture: $(DEB_ARCH)' \
'Maintainer: Nex Maintainers <noreply@example.com>' \
'Depends: libgtk-3-0, libayatana-appindicator3-1, xdg-utils' \
'Description: AI Gateway desktop application' \
' Nex is an AI Gateway desktop application.' \
> "$$pkgdir/DEBIAN/control"; \
asset=$$(go run ./versionctl asset-name desktop linux $(TARGET_ARCH) deb); \
dpkg-deb --build --root-owner-group "$$pkgdir" "$(RELEASE_DIR)/$$asset"; \
dpkg-deb -I "$(RELEASE_DIR)/$$asset" >/dev/null
_package-linux-rpm:
mkdir -p "$(RELEASE_DIR)"
topdir="$(abspath build/rpmbuild-$(TARGET_ARCH))"; \
rm -rf "$$topdir"; \
mkdir -p "$$topdir/BUILD" "$$topdir/BUILDROOT" "$$topdir/RPMS" "$$topdir/SOURCES" "$$topdir/SPECS" "$$topdir/SRPMS"; \
rpmbuild -bb --target "$(RPM_ARCH)" \
--define "_topdir $$topdir" \
--define "nex_version $(VERSION)" \
--define "nex_binary $(abspath $(LINUX_DESKTOP_BINARY))" \
--define "nex_desktop_file $(abspath packaging/linux/nex.desktop)" \
--define "nex_icons_dir $(abspath assets/icons/hicolor)" \
packaging/linux/nex.spec; \
rpm_file=$$(find "$$topdir/RPMS" -type f -name '*.rpm' | sort | tail -n 1); \
test -n "$$rpm_file"; \
asset=$$(go run ./versionctl asset-name desktop linux $(TARGET_ARCH) rpm); \
cp "$$rpm_file" "$(RELEASE_DIR)/$$asset"; \
rpm -qip "$(RELEASE_DIR)/$$asset" >/dev/null
_package-macos-zip:
mkdir -p "$(RELEASE_DIR)"
asset=$$(go run ./versionctl asset-name desktop macos universal zip); \
ditto -c -k --keepParent build/Nex.app "$(RELEASE_DIR)/$$asset"
_package-macos-dmg:
mkdir -p "$(RELEASE_DIR)"
dmgdir="build/dmg/Nex"; \
rm -rf "$$dmgdir"; \
mkdir -p "$$dmgdir"; \
cp -R build/Nex.app "$$dmgdir/Nex.app"; \
ln -s /Applications "$$dmgdir/Applications"; \
asset=$$(go run ./versionctl asset-name desktop macos universal dmg); \
hdiutil create -volname Nex -srcfolder "$$dmgdir" -ov -format UDZO "$(RELEASE_DIR)/$$asset"; \
hdiutil verify "$(RELEASE_DIR)/$$asset" >/dev/null
# ============================================ # ============================================
# 共享 helper targets # 共享 helper targets

View File

@@ -39,6 +39,8 @@ nex/
│ ├── icon.icns # macOS 应用图标 │ ├── icon.icns # macOS 应用图标
│ └── icon.ico # Windows 应用图标 │ └── icon.ico # Windows 应用图标
├── packaging/ # 桌面发布包元数据Linux desktop entry、RPM spec 等)
└── README.md # 本文件 └── README.md # 本文件
``` ```
@@ -107,12 +109,18 @@ make desktop-build-mac
# Windows # Windows
make desktop-build-win make desktop-build-win
# Windows arm64
make desktop-build-win TARGET_ARCH=arm64
# Linux # Linux
make desktop-build-linux make desktop-build-linux
# Linux arm64
make desktop-build-linux TARGET_ARCH=arm64
``` ```
**使用桌面应用** **使用桌面应用**
- 双击启动应用macOS: Nex.appWindows: nex-win-amd64.exeLinux: nex-linux-amd64 - 双击启动应用macOS: Nex.appWindows: nex-win-amd64.exe / nex-win-arm64.exeLinux: nex-linux-amd64 / nex-linux-arm64
- 系统托盘图标出现,浏览器自动打开管理界面 - 系统托盘图标出现,浏览器自动打开管理界面
- 点击托盘图标显示菜单,可打开管理界面或退出 - 点击托盘图标显示菜单,可打开管理界面或退出
- 关闭浏览器后服务继续运行,可通过托盘重新打开 - 关闭浏览器后服务继续运行,可通过托盘重新打开
@@ -120,8 +128,10 @@ make desktop-build-linux
**注意事项** **注意事项**
- 桌面应用需要 CGO 支持 - 桌面应用需要 CGO 支持
- macOS: 自带 Xcode Command Line Tools - macOS: 自带 Xcode Command Line Tools
- Linux: 自带 gcc部分桌面环境需要 `libappindicator3-dev` - Linux 构建: 需要 gcc、pkg-config、GTK3 开发包和 Ayatana AppIndicator 开发包Ubuntu/Debian: `libgtk-3-dev``libayatana-appindicator3-dev`
- Windows: 需要 MinGW-w64 或在 Windows 环境构建 - Linux 运行: 需要 GTK3、Ayatana AppIndicator 和 xdg-utilsAppImage 也依赖系统提供 AppImage runtime/FUSE 能力,不承诺完全自包含
- Windows: 需要对应架构的 MinGW-w64/MSYS2 工具链desktop 使用 GUI linker flags 隐藏控制台窗口
- macOS DMG: 发布包暂不签名、不 notarize首次打开可能出现 Gatekeeper 提示
**Linux 桌面环境兼容性** **Linux 桌面环境兼容性**
- GNOME: 需要 AppIndicator 扩展 - GNOME: 需要 AppIndicator 扩展
@@ -151,6 +161,40 @@ make server-run
make server-build make server-build
``` ```
### Release 产物
发布流程由 Git tag `vX.Y.Z` 触发GitHub Actions 会创建 Draft Release 并上传 server、web 和 desktop 三类产物,同时生成 `SHA256SUMS`
**server 产物**(不内置 Web 管理界面):
| 平台 | 产物 |
|------|------|
| Linux amd64 | `nex-server_<version>_linux_amd64.tar.gz` |
| Linux arm64 | `nex-server_<version>_linux_arm64.tar.gz` |
| macOS amd64 | `nex-server_<version>_macos_amd64.tar.gz` |
| macOS arm64 | `nex-server_<version>_macos_arm64.tar.gz` |
| macOS universal | `nex-server_<version>_macos_universal.tar.gz` |
| Windows amd64 | `nex-server_<version>_windows_amd64.zip` |
| Windows arm64 | `nex-server_<version>_windows_arm64.zip` |
**web 产物**
| 内容 | 产物 |
|------|------|
| `frontend/dist` | `nex-web_<version>.tar.gz` |
**desktop 产物**
| 平台 | 产物 |
|------|------|
| Linux amd64 | `nex-desktop_<version>_linux_amd64.tar.gz``.AppImage``.deb``.rpm` |
| Linux arm64 | `nex-desktop_<version>_linux_arm64.tar.gz``.AppImage``.deb``.rpm` |
| macOS universal | `nex-desktop_<version>_macos_universal.zip``nex-desktop_<version>_macos_universal.dmg` |
| Windows amd64 | `nex-desktop_<version>_windows_amd64.zip` |
| Windows arm64 | `nex-desktop_<version>_windows_arm64.zip` |
Linux deb 包声明 `libgtk-3-0``libayatana-appindicator3-1``xdg-utils` 运行依赖rpm 包声明 `gtk3``libayatana-appindicator-gtk3``xdg-utils` 运行依赖。Rocky Linux 9 等发行版可能需要启用 EPEL 才能解析 Ayatana AppIndicator 依赖。
## API 接口 ## API 接口
### 代理接口(对外部应用) ### 代理接口(对外部应用)

View File

@@ -1 +1 @@
0.1.2 0.1.3

View File

@@ -1,2 +1,2 @@
VITE_API_BASE= VITE_API_BASE=
VITE_APP_VERSION=0.1.2 VITE_APP_VERSION=0.1.3

View File

@@ -1,2 +1,2 @@
VITE_API_BASE= VITE_API_BASE=
VITE_APP_VERSION=0.1.2 VITE_APP_VERSION=0.1.3

View File

@@ -1,2 +1,2 @@
VITE_API_BASE=/api VITE_API_BASE=/api
VITE_APP_VERSION=0.1.2 VITE_APP_VERSION=0.1.3

View File

@@ -1,7 +1,7 @@
{ {
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"version": "0.1.2", "version": "0.1.3",
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -139,29 +139,92 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
### Requirement: 跨平台构建 ### Requirement: 跨平台构建
系统 SHALL 支持跨平台构建和打包。构建 target SHALL 按平台分离,中间构建产物文件名 SHALL 保持 `nex-{os}-{arch}[.exe]` 格式,最终桌面发布资产文件名 SHALL 包含统一版本号和平台标识 系统 SHALL 支持跨平台构建和打包。构建 target SHALL 按平台和架构分离或参数化,中间构建产物文件名 SHALL 保持可区分目标平台和架构,最终桌面发布资产文件名 SHALL 包含统一版本号、组件、平台、架构和格式信息
#### Scenario: macOS 构建 #### Scenario: macOS 构建
- **WHEN** 执行 `desktop-build-mac` 构建命令且当前版本为 `1.2.3` - **WHEN** 执行 macOS desktop 构建命令且当前版本为 `1.2.3`
- **THEN** 生成 `nex-mac-arm64``nex-mac-amd64` 可执行文件 - **THEN** 系统 SHALL 生成 macOS arm64 和 amd64 桌面可执行文件
- **AND** 系统生成可打包为 `.app` bundle 的 macOS 桌面产物 - **AND** 系统 SHALL 使用 `lipo` 生成 macOS universal 桌面可执行文件
- **AND** 最终发布资产文件名 SHALL 包含 `1.2.3``macOS` 平台标识 - **AND** 系统 SHALL 生成可打包为 `.app` bundle 的 macOS desktop 产物
- **AND** 最终 macOS desktop 发布资产文件名 SHALL 包含 `1.2.3``macos``universal`
#### Scenario: Windows 构建 #### Scenario: Windows 构建
- **WHEN** 执行 `desktop-build-win` 构建命令且当前版本为 `1.2.3` - **WHEN** 执行 Windows desktop 构建命令且当前版本为 `1.2.3`
- **THEN** 系统生成 Windows 桌面可执行文件 - **THEN** 系统 SHALL 生成 Windows amd64 和 arm64 desktop 可执行文件
- **AND** 生成 `nex-win-amd64.exe` 可执行文件 - **AND** Windows desktop 构建 SHALL 使用 `-H=windowsgui` linker flag 隐藏控制台窗口
- **AND** 使用 `-H=windowsgui` linker flag 隐藏控制台窗口 - **AND** 最终 Windows desktop 发布资产文件名 SHALL 包含 `1.2.3``windows` 和对应架构标识
- **AND** 最终发布资产文件名 SHALL 包含 `1.2.3``windows` 平台标识
#### Scenario: Linux 构建 #### Scenario: Linux 构建
- **WHEN** 执行 `desktop-build-linux` 构建命令且当前版本为 `1.2.3` - **WHEN** 执行 Linux desktop 构建命令且当前版本为 `1.2.3`
- **THEN** 系统生成 Linux 桌面可执行文件 - **THEN** 系统 SHALL 生成 Linux amd64 和 arm64 desktop 可执行文件
- **AND** 生成 `nex-linux-amd64` 可执行文件 - **AND** Linux desktop 构建 SHALL 使用 CGO 和 GTK/AppIndicator 构建依赖
- **AND** 最终发布资产文件名 SHALL 包含 `1.2.3``linux` 平台标识 - **AND** 最终 Linux desktop 发布资产文件名 SHALL 包含 `1.2.3``linux` 和对应架构标识
### Requirement: Linux 桌面发布安装包
系统 SHALL 为 Linux desktop amd64 和 arm64 生成 tar.gz、AppImage、deb 和 rpm 发布安装包,并 SHALL 在安装包中包含标准桌面集成元数据。
#### Scenario: Linux desktop tar.gz 裸包
- **WHEN** 构建 Linux desktop 发布资产
- **THEN** 系统 SHALL 为 amd64 生成 `nex-desktop_<version>_linux_amd64.tar.gz`
- **AND** 系统 SHALL 为 arm64 生成 `nex-desktop_<version>_linux_arm64.tar.gz`
#### Scenario: Linux desktop AppImage 包
- **WHEN** 构建 Linux desktop AppImage 发布资产
- **THEN** 系统 SHALL 为 amd64 生成 `nex-desktop_<version>_linux_amd64.AppImage`
- **AND** 系统 SHALL 为 arm64 生成 `nex-desktop_<version>_linux_arm64.AppImage`
- **AND** AppImage SHALL 包含 desktop entry、应用图标和 desktop 可执行文件
- **AND** AppImage SHALL 依赖目标系统提供 GTK3、Ayatana AppIndicator 和运行 AppImage 所需的 runtime/FUSE 能力
#### Scenario: Linux desktop deb 包
- **WHEN** 构建 Linux desktop deb 发布资产
- **THEN** 系统 SHALL 为 amd64 生成 `nex-desktop_<version>_linux_amd64.deb`
- **AND** 系统 SHALL 为 arm64 生成 `nex-desktop_<version>_linux_arm64.deb`
- **AND** deb 包 SHALL 将可执行文件安装到 `/usr/bin/nex`
- **AND** deb 包 SHALL 将 desktop entry 安装到 `/usr/share/applications/nex.desktop`
- **AND** deb 包 SHALL 将 hicolor 图标安装到 `/usr/share/icons/hicolor/.../apps/nex.png`
- **AND** deb 包 SHALL 声明 `libgtk-3-0``libayatana-appindicator3-1``xdg-utils` 运行时依赖
- **AND** deb 包 metadata 的架构字段 SHALL 使用 `amd64``arm64`
#### Scenario: Linux desktop rpm 包
- **WHEN** 构建 Linux desktop rpm 发布资产
- **THEN** 系统 SHALL 为 amd64 生成 `nex-desktop_<version>_linux_amd64.rpm`
- **AND** 系统 SHALL 为 arm64 生成 `nex-desktop_<version>_linux_arm64.rpm`
- **AND** rpm 包 SHALL 将可执行文件安装到 `/usr/bin/nex`
- **AND** rpm 包 SHALL 将 desktop entry 安装到 `/usr/share/applications/nex.desktop`
- **AND** rpm 包 SHALL 将 hicolor 图标安装到 `/usr/share/icons/hicolor/.../apps/nex.png`
- **AND** rpm 包 SHALL 声明 `gtk3``libayatana-appindicator-gtk3``xdg-utils` 运行时依赖
- **AND** rpm 包 metadata 的架构字段 SHALL 使用 `x86_64``aarch64`
### Requirement: macOS DMG 打包
系统 SHALL 为 macOS desktop universal `.app` 生成 unsigned DMG 安装包,并 SHALL 保留 universal zip 发布资产。
#### Scenario: macOS universal zip 包
- **WHEN** 构建 macOS desktop 发布资产且当前版本为 `1.2.3`
- **THEN** 系统 SHALL 生成 `nex-desktop_1.2.3_macos_universal.zip`
- **AND** zip 包 SHALL 包含 `Nex.app`
#### Scenario: macOS universal DMG 包
- **WHEN** 构建 macOS desktop DMG 发布资产且当前版本为 `1.2.3`
- **THEN** 系统 SHALL 生成 `nex-desktop_1.2.3_macos_universal.dmg`
- **AND** DMG SHALL 包含 `Nex.app`
- **AND** DMG SHALL 包含指向 `/Applications` 的快捷方式
- **AND** DMG SHALL NOT 要求 macOS 签名或 notarization 才能完成构建
#### Scenario: macOS universal 架构校验
- **WHEN** macOS desktop universal 可执行文件生成完成
- **THEN** 系统 SHALL 验证该可执行文件包含 amd64 和 arm64 架构
### Requirement: macOS .app 打包 ### Requirement: macOS .app 打包

View File

@@ -32,88 +32,116 @@
### Requirement: 三平台发布构建 ### Requirement: 三平台发布构建
系统 SHALL 在发布流水线中构建 server 与 desktop 的 Linux、Windows、macOS 三个平台产物 系统 SHALL 在发布流水线中构建 server、web 与 desktop 的发布产物,并覆盖 Linux、Windows、macOS 的目标架构和格式矩阵
#### Scenario: Linux 发布构建 #### Scenario: server 发布构建
- **WHEN** 发布流水线执行 Linux 构建 job - **WHEN** 发布流水线执行 server 发布构建
- **THEN** 系统 SHALL 在可访问 Go、Bun 和 Linux 桌面构建依赖的 shell 环境中执行 Linux 发布构建 - **THEN** 系统 SHALL 生成 `nex-server_<version>_linux_amd64.tar.gz`
- **AND** 系统 SHALL 生成 Linux server 发布资产 - **AND** 系统 SHALL 生成 `nex-server_<version>_linux_arm64.tar.gz`
- **AND** 系统 SHALL 生成 Linux desktop 发布资产 - **AND** 系统 SHALL 生成 `nex-server_<version>_macos_amd64.tar.gz`
- **AND** 系统 SHALL 生成 `nex-server_<version>_macos_arm64.tar.gz`
- **AND** 系统 SHALL 生成 `nex-server_<version>_macos_universal.tar.gz`
- **AND** 系统 SHALL 生成 `nex-server_<version>_windows_amd64.zip`
- **AND** 系统 SHALL 生成 `nex-server_<version>_windows_arm64.zip`
#### Scenario: Windows 发布构建 #### Scenario: web 发布构建
- **WHEN** 发布流水线执行 Windows 构建 job - **WHEN** 发布流水线执行 web 发布构建
- **THEN** 系统 SHALL 在包含 MSYS2 / MINGW64 构建工具且可访问 Go 与 Bun 工具链的 shell 环境中执行 Windows 发布构建 - **THEN** 系统 SHALL 使用 Bun 构建 `frontend/dist`
- **AND** 系统 SHALL 生成 Windows server 发布资产 - **AND** 系统 SHALL 将前端静态资源打包为 `nex-web_<version>.tar.gz`
- **AND** 系统 SHALL 生成 Windows desktop 发布资产 - **AND** server 发布资产 SHALL NOT 内置 Web 管理界面静态资源
#### Scenario: macOS 发布构建 #### Scenario: Linux desktop 发布构建
- **WHEN** 发布流水线执行 macOS 构建 job - **WHEN** 发布流水线执行 Linux desktop 发布构建
- **THEN** 系统 SHALL 在可访问 Go、Bun 和 macOS 打包工具链的 shell 环境中执行 macOS 发布构建 - **THEN** 系统 SHALL 在可访问 Go、Bun、CGO、GTK3、Ayatana AppIndicator 和 Linux 打包工具链的环境中构建
- **AND** 系统 SHALL 生成 darwin-amd64 server 发布资产 - **AND** 系统 SHALL `amd64``arm64` 分别生成 tar.gz、AppImage、deb 和 rpm desktop 发布资产
- **AND** 系统 SHALL 生成 darwin-arm64 server 发布资产 - **AND** Linux arm64 desktop 发布构建 SHALL 使用原生 arm64 runner 或等价的 arm64 Linux 构建环境
- **AND** 系统 SHALL 生成 macOS desktop universal 发布资产
#### Scenario: Windows desktop 发布构建
- **WHEN** 发布流水线执行 Windows desktop 发布构建
- **THEN** 系统 SHALL 在包含对应架构 MSYS2/MinGW 或等价 CGO 工具链的环境中构建
- **AND** 系统 SHALL 生成 `nex-desktop_<version>_windows_amd64.zip`
- **AND** 系统 SHALL 生成 `nex-desktop_<version>_windows_arm64.zip`
#### Scenario: macOS desktop 发布构建
- **WHEN** 发布流水线执行 macOS desktop 发布构建
- **THEN** 系统 SHALL 在可访问 Go、Bun、Xcode 命令行工具、`lipo``hdiutil` 和 zip 打包工具的 macOS 环境中构建
- **AND** 系统 SHALL 生成 `nex-desktop_<version>_macos_universal.zip`
- **AND** 系统 SHALL 生成 `nex-desktop_<version>_macos_universal.dmg`
### Requirement: 三平台发布构建预检 ### Requirement: 三平台发布构建预检
系统 SHALL 在正式执行各平台 `make release-assets-*` 前验证对应发布 job 的关键工具链可用性,并在环境不完整时快速失败且输出明确诊断。 系统 SHALL 在正式执行各平台 release 构建前验证对应 job 的关键工具链可用性,并在环境不完整时快速失败且输出明确诊断。
#### Scenario: Linux 预检通过后开始构建 #### Scenario: Linux 预检通过后开始构建
- **WHEN** Linux 发布 job 中的 `go``bun` Linux 桌面构建依赖均可用 - **WHEN** Linux 发布 job 中的 `go``bun``gcc``pkg-config`、GTK3、Ayatana AppIndicator 和 Linux 打包工具均可用
- **THEN** 系统 SHALL 输出关键工具的版本信息或解析路径 - **THEN** 系统 SHALL 输出关键工具的版本信息或解析路径
- **AND** 系统 SHALL 继续执行 `make release-assets-linux` - **AND** 系统 SHALL 继续执行对应 Linux release 构建
#### Scenario: Windows 预检通过后开始构建 #### Scenario: Windows 预检通过后开始构建
- **WHEN** Windows 发布 job 中的 `go``bun` 与 MSYS2 构建工具均可用 - **WHEN** Windows 发布 job 中的 `go``bun``make`、对应架构 CGO 编译器和 resource 生成工具均可用
- **THEN** 系统 SHALL 输出关键工具的版本信息或解析路径 - **THEN** 系统 SHALL 输出关键工具的版本信息或解析路径
- **AND** 系统 SHALL 继续执行 `make release-assets-windows` - **AND** 系统 SHALL 继续执行对应 Windows release 构建
#### Scenario: macOS 预检通过后开始构建 #### Scenario: macOS 预检通过后开始构建
- **WHEN** macOS 发布 job 中的 `go``bun` 与 macOS 打包工具均可用 - **WHEN** macOS 发布 job 中的 `go``bun``ditto``lipo``vtool``hdiutil` 均可用
- **THEN** 系统 SHALL 输出关键工具的版本信息或解析路径 - **THEN** 系统 SHALL 输出关键工具的版本信息或解析路径
- **AND** 系统 SHALL 继续执行 `make release-assets-macos` - **AND** 系统 SHALL 继续执行对应 macOS release 构建
#### Scenario: 任一平台预检发现工具缺失 #### Scenario: web 预检通过后开始构建
- **WHEN** 任一平台发布 job 中存在关键工具不可用 - **WHEN** web 发布 job 中`bun` 和前端构建依赖均可用
- **THEN** 系统 SHALL 输出 Bun 版本信息
- **AND** 系统 SHALL 继续执行 web release 构建
#### Scenario: 任一预检发现工具缺失
- **WHEN** 任一发布 job 中存在关键工具不可用
- **THEN** 发布流水线 SHALL 在正式构建前失败 - **THEN** 发布流水线 SHALL 在正式构建前失败
- **AND** 系统 SHALL 在日志中标识缺失的工具链名称 - **AND** 系统 SHALL 在日志中标识缺失的工具链名称
### Requirement: 发布流水线 LFS 资产拉取 ### Requirement: 发布流水线 LFS 资产拉取
发布流水线 SHALL 在所有会 checkout 仓库并参与版本校验或平台构建的 job 中拉取 Git LFS 真实文件,确保发布构建读取到真实二进制资产而非 LFS pointer 文本。 发布流水线 SHALL 在所有会 checkout 仓库并参与版本校验、web 构建、server 构建或 desktop 构建的 job 中拉取 Git LFS 真实文件,确保发布构建读取到真实二进制资产而非 LFS pointer 文本。
#### Scenario: 发布 job 获取真实 LFS 图标资产 #### Scenario: 发布 job 获取真实 LFS 图标资产
- **WHEN** 发布流水线执行 `prepare``build-linux``build-windows``build-macos` job 的 checkout 步骤 - **WHEN** 发布流水线执行任一参与版本校验、web 构建、server 构建或 desktop 构建的 job 的 checkout 步骤
- **THEN** checkout 步骤 SHALL 拉取 Git LFS 文件 - **THEN** checkout 步骤 SHALL 拉取 Git LFS 文件
- **AND** `assets/icon.ico``assets/icon.icns``assets/icon.png``frontend/public/icon.png` SHALL 在后续步骤中表现为真实图标文件而非 LFS pointer 文本 - **AND** `assets/icon.ico``assets/icon.icns``assets/icon.png``frontend/public/icon.png` SHALL 在后续步骤中表现为真实图标文件而非 LFS pointer 文本
#### Scenario: 新增矩阵 job 获取真实 LFS 资产
- **WHEN** 发布流水线新增 server、web、desktop、platform 或 arch 矩阵 job
- **THEN** 该 job 的 checkout 步骤 SHALL 使用与现有发布 job 一致的 Git LFS 拉取配置
### Requirement: 发布资产图标预检 ### Requirement: 发布资产图标预检
发布流水线 SHALL 在正式执行各平台发布构建前校验关键图标资产可用,并在检测到 LFS pointer 或错误格式时快速失败且输出明确诊断。 发布流水线 SHALL 在正式执行任何需要图标资产、前端 public 图标或 desktop 打包资源的发布构建前校验关键图标资产可用,并在检测到 LFS pointer 或错误格式时快速失败且输出明确诊断。
#### Scenario: 图标资产为 LFS pointer #### Scenario: 图标资产为 LFS pointer
- **WHEN** 发布资产预检发现关键图标文件内容为 Git LFS pointer 文本 - **WHEN** 发布资产预检发现关键图标文件内容为 Git LFS pointer 文本
- **THEN** 发布流水线 SHALL 在执行平台发布构建前失败 - **THEN** 发布流水线 SHALL 在执行对应 release 构建前失败
- **AND** 系统 SHALL 在日志中标识对应图标文件需要拉取 Git LFS 真实内容 - **AND** 系统 SHALL 在日志中标识对应图标文件需要拉取 Git LFS 真实内容
#### Scenario: 图标资产格式无效 #### Scenario: 图标资产格式无效
- **WHEN** 发布资产预检发现关键图标文件不是对应格式的有效资源 - **WHEN** 发布资产预检发现关键图标文件不是对应格式的有效资源
- **THEN** 发布流水线 SHALL 在执行平台发布构建前失败 - **THEN** 发布流水线 SHALL 在执行对应 release 构建前失败
- **AND** 系统 SHALL 在日志中标识格式无效的图标文件路径 - **AND** 系统 SHALL 在日志中标识格式无效的图标文件路径
#### Scenario: 图标资产预检通过 #### Scenario: 图标资产预检通过
- **WHEN** `assets/icon.ico``assets/icon.icns``assets/icon.png``frontend/public/icon.png` 均为真实且格式可用的图标资产 - **WHEN** `assets/icon.ico``assets/icon.icns``assets/icon.png``frontend/public/icon.png` 均为真实且格式可用的图标资产
- **THEN** 发布流水线 SHALL 继续执行对应平台的 `make release-assets-*` 构建 - **THEN** 发布流水线 SHALL 继续执行依赖这些资产的 release 构建
### Requirement: 发布流水线运行时兼容性 ### Requirement: 发布流水线运行时兼容性
@@ -131,34 +159,54 @@
### Requirement: 版本化发布资产命名 ### Requirement: 版本化发布资产命名
系统 SHALL 为 server 与 desktop 发布资产使用包含统一版本号和目标平台信息的文件名,确保 Release 页面可直接区分产物用途平台。 系统 SHALL 为 server、web 与 desktop 发布资产使用包含统一版本号、组件、目标平台和目标架构信息的文件名,确保 Release 页面可直接区分产物用途平台、架构和格式
#### Scenario: server 资产命名 #### Scenario: server 资产命名
- **WHEN** 当前发布版本为 `1.2.3` - **WHEN** 当前发布版本为 `1.2.3`
- **THEN** Linux server 发布资产文件名 SHALL 包含 `1.2.3``linux``amd64` - **THEN** Linux server 发布资产文件名 SHALL `nex-server_1.2.3_linux_amd64.tar.gz``nex-server_1.2.3_linux_arm64.tar.gz`
- **AND** Windows server 发布资产文件名 SHALL 包含 `1.2.3``windows``amd64` - **AND** macOS server 发布资产文件名 SHALL `nex-server_1.2.3_macos_amd64.tar.gz``nex-server_1.2.3_macos_arm64.tar.gz``nex-server_1.2.3_macos_universal.tar.gz`
- **AND** macOS server 发布资产文件名 SHALL 分别包含 `1.2.3``darwin``amd64` `1.2.3``darwin``arm64` - **AND** Windows server 发布资产文件名 SHALL `nex-server_1.2.3_windows_amd64.zip` `nex-server_1.2.3_windows_arm64.zip`
#### Scenario: web 资产命名
- **WHEN** 当前发布版本为 `1.2.3`
- **THEN** web 发布资产文件名 SHALL 为 `nex-web_1.2.3.tar.gz`
- **AND** web 发布资产文件名 SHALL NOT 包含平台或架构字段
#### Scenario: desktop 资产命名 #### Scenario: desktop 资产命名
- **WHEN** 当前发布版本为 `1.2.3` - **WHEN** 当前发布版本为 `1.2.3`
- **THEN** Linux desktop 发布资产文件名 SHALL 包含 `1.2.3``linux` - **THEN** Linux desktop 发布资产文件名 SHALL 使用 `nex-desktop_1.2.3_linux_<arch>.<format>` 格式
- **AND** Windows desktop 发布资产文件名 SHALL 包含 `1.2.3``windows` - **AND** Windows desktop 发布资产文件名 SHALL `nex-desktop_1.2.3_windows_amd64.zip``nex-desktop_1.2.3_windows_arm64.zip`
- **AND** macOS desktop universal 发布资产文件名 SHALL 包含 `1.2.3``macOS` - **AND** macOS desktop 发布资产文件名 SHALL `nex-desktop_1.2.3_macos_universal.zip``nex-desktop_1.2.3_macos_universal.dmg`
- **AND** 发布资产文件名中的 macOS 平台字段 SHALL 使用 `macos` 而非 `darwin`
### Requirement: Draft Release 组装 ### Requirement: Draft Release 组装
系统 SHALL 将发布流水线产物上传到 GitHub Draft Release由人工确认后再公开发布。 系统 SHALL 将发布流水线产物上传到 GitHub Draft Release由人工确认后再公开发布,并 SHALL 生成覆盖全部发布资产的校验和清单
#### Scenario: 发布成功时创建 Draft Release #### Scenario: 发布成功时创建 Draft Release
- **WHEN** 版本校验通过且三平台发布资产构建完成 - **WHEN** 版本校验通过且 server、web、desktop 的全部目标发布资产构建完成
- **THEN** 系统 SHALL 创建或更新与该 tag 对应的 GitHub Draft Release - **THEN** 系统 SHALL 创建或更新与该 tag 对应的 GitHub Draft Release
- **AND** 系统 SHALL 上传 server 与 desktop 的全部发布资产 - **AND** 系统 SHALL 上传 server、web 与 desktop 的全部发布资产
- **AND** 系统 SHALL 上传 `SHA256SUMS`
#### Scenario: 校验和覆盖全部资产
- **WHEN** Draft Release 组装步骤生成 `SHA256SUMS`
- **THEN** `SHA256SUMS` SHALL 包含除自身以外的全部发布资产文件
- **AND** `SHA256SUMS` 中的文件名 SHALL 与实际上传的 release asset 文件名一致
#### Scenario: 构建失败时阻止完成发布 #### Scenario: 构建失败时阻止完成发布
- **WHEN** 任一平台发布资产构建失败或版本校验失败 - **WHEN** 任一目标发布资产构建失败、打包失败、校验失败、artifact 上传为空或版本校验失败
- **THEN** 发布流水线 SHALL 失败 - **THEN** 发布流水线 SHALL 失败
- **AND** 系统 SHALL NOT 产生可直接公开的成功发布结果 - **AND** 系统 SHALL NOT 产生可直接公开的成功发布结果
#### Scenario: artifact 缺失时快速失败
- **WHEN** 任一构建 job 尝试上传 release artifact 但匹配不到目标文件
- **THEN** 该 job SHALL 失败
- **AND** Draft Release 组装 SHALL NOT 继续发布不完整资产集合

View File

@@ -8,17 +8,26 @@
### Requirement: 根目录公开命令分层 ### Requirement: 根目录公开命令分层
根目录 `Makefile` SHALL 仅暴露全局命令、版本命令、server 产品命令、desktop 产品命令和 release 命令,不再作为 backend 局部维护命令或内部打包步骤的公开入口。 根目录 `Makefile` SHALL 仅暴露全局命令、版本命令、server 产品命令、desktop 产品命令和 release 命令,不再作为 backend 局部维护命令或内部打包步骤的公开入口。release 命令 SHALL 使用 `release-assets` 前缀,并 SHALL 通过清晰的目标名或变量参数表达 component、platform、arch 和 format。
#### Scenario: 查看根目录公开命令 #### Scenario: 查看根目录公开命令
- **WHEN** 开发者查看根目录 `Makefile` 的公开 target - **WHEN** 开发者查看根目录 `Makefile` 的公开 target
- **THEN** SHALL 仅看到 `lint``test``clean``version-sync``version-check``server-run``server-build``server-lint``server-test``server-clean``desktop-build-mac``desktop-build-win``desktop-build-linux``desktop-lint``desktop-test``desktop-clean``release-assets-linux``release-assets-windows``release-assets-macos` 这类公共入口 - **THEN** SHALL 仅看到 `lint``test``clean``version-sync``version-check``server-run``server-build``server-lint``server-test``server-clean``desktop-build-mac``desktop-build-win``desktop-build-linux``desktop-lint``desktop-test``desktop-clean``release-assets` 前缀的 release 公共入口
- **AND** release 公共入口 SHALL 能覆盖 server、web、desktop 的目标发布产物
#### Scenario: 根目录不暴露局部和内部命令 #### Scenario: 根目录不暴露局部和内部命令
- **WHEN** 开发者查看根目录 `Makefile` 的公开 target - **WHEN** 开发者查看根目录 `Makefile` 的公开 target
- **THEN** SHALL NOT 暴露 `backend-*``frontend-*`、数据库迁移命令、MySQL 专项测试命令或 `desktop-prepare-*` 之类内部步骤 - **THEN** SHALL NOT 暴露 `backend-*``frontend-*`、数据库迁移命令、MySQL 专项测试命令或 `desktop-prepare-*` 之类内部步骤
- **THEN** SHALL NOT 暴露 `dev``build``all``desktop-dev``desktop-build` 这类模糊或聚合式公共命令 - **THEN** SHALL NOT 暴露 `dev``build``all``desktop-dev``desktop-build` 这类模糊或聚合式公共命令
#### Scenario: release 内部步骤保持内部化
- **WHEN** 根目录 `Makefile` 需要复用 release 构建、打包、校验辅助步骤
- **THEN** 内部辅助 target SHALL 使用 `_` 前缀或 Make 变量参数化方式表达
- **AND** 内部辅助 target SHALL NOT 成为文档化的公共入口
### Requirement: 全局质量与清理命令 ### Requirement: 全局质量与清理命令
根目录 `Makefile` SHALL 提供 `lint``test``clean` 作为全仓默认入口。 根目录 `Makefile` SHALL 提供 `lint``test``clean` 作为全仓默认入口。
@@ -97,12 +106,33 @@
### Requirement: Release 命令沿用根目录入口 ### Requirement: Release 命令沿用根目录入口
根目录 `Makefile` SHALL 继续提供 `release-assets-*` 作为发布资产入口,并与新的版本校验规则保持一致。 根目录 `Makefile` SHALL 继续提供 `release-assets` 前缀 target 作为发布资产入口,并与版本校验、发布资产预检和多组件打包规则保持一致。
#### Scenario: 执行 release 资产命令 #### Scenario: 执行 release 资产命令
- **WHEN** 执行 `make release-assets-linux``make release-assets-windows``make release-assets-macos`
- **WHEN** 执行任一 `release-assets` 前缀的公共 release target
- **THEN** SHALL 在构建发布资产前执行版本一致性校验 - **THEN** SHALL 在构建发布资产前执行版本一致性校验
- **THEN** SHALL NOT 隐式执行 `version-sync` 或修改版本镜像文件 - **AND** SHALL 在需要图标或桌面资源的构建前执行发布资产预检
- **AND** SHALL NOT 隐式执行 `version-sync` 或修改版本镜像文件
#### Scenario: release target 职责清晰
- **WHEN** GitHub Actions 调用根目录 `Makefile` 生成 release 产物
- **THEN** 对应 release target SHALL 明确生成的 component、platform、arch 或 format 范围
- **AND** GitHub Actions SHALL NOT 以内联脚本替代 Makefile 中已有的核心构建和打包逻辑
#### Scenario: web release 产物生成
- **WHEN** 执行 web release 资产命令
- **THEN** SHALL 使用 Bun 构建 `frontend/dist`
- **AND** SHALL 打包生成 `nex-web_<version>.tar.gz`
- **AND** SHALL NOT 修改前端版本镜像文件
#### Scenario: checksum release 产物生成
- **WHEN** 执行 release 汇总或 Draft Release 组装相关命令
- **THEN** SHALL 能基于当前 release 产物目录生成 `SHA256SUMS`
- **AND** `SHA256SUMS` SHALL 覆盖除自身以外的全部 release 资产
### Requirement: Backend 局部命令下沉 ### Requirement: Backend 局部命令下沉

5
packaging/linux/AppRun Normal file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
set -eu
APPDIR=$(dirname "$(readlink -f "$0")")
exec "$APPDIR/usr/bin/nex" "$@"

View File

@@ -0,0 +1,9 @@
[Desktop Entry]
Type=Application
Name=Nex
Comment=AI Gateway
Exec=nex
Icon=nex
Terminal=false
Categories=Development;Network;
StartupNotify=false

29
packaging/linux/nex.spec Normal file
View File

@@ -0,0 +1,29 @@
Name: nex
Version: %{nex_version}
Release: 1%{?dist}
Summary: AI Gateway desktop application
License: Apache-2.0
URL: https://github.com/nex/gateway
Requires: gtk3
Requires: libayatana-appindicator-gtk3
Requires: xdg-utils
%description
Nex is an AI Gateway desktop application.
%prep
%build
%install
mkdir -p %{buildroot}/usr/bin
install -m 0755 %{nex_binary} %{buildroot}/usr/bin/nex
mkdir -p %{buildroot}/usr/share/applications
install -m 0644 %{nex_desktop_file} %{buildroot}/usr/share/applications/nex.desktop
mkdir -p %{buildroot}/usr/share/icons/hicolor
cp -a %{nex_icons_dir}/. %{buildroot}/usr/share/icons/hicolor/
%files
/usr/bin/nex
/usr/share/applications/nex.desktop
/usr/share/icons/hicolor/*/apps/nex.png

30
scripts/push-all-remotes.sh Executable file
View File

@@ -0,0 +1,30 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR=$(git rev-parse --show-toplevel)
BRANCH=$(git branch --show-current)
if [[ -z "$BRANCH" ]]; then
echo "当前仓库处于 detached HEAD 状态,无法推送当前分支" >&2
exit 1
fi
cd "$ROOT_DIR"
REMOTES=()
while IFS= read -r REMOTE; do
REMOTES+=("$REMOTE")
done < <(git remote)
if [[ ${#REMOTES[@]} -eq 0 ]]; then
echo "当前仓库未配置任何远端" >&2
exit 1
fi
for REMOTE in "${REMOTES[@]}"; do
echo "推送分支 $BRANCH 和 tags 到远端 $REMOTE"
git push "$REMOTE" "$BRANCH" --tags
done
echo "已推送分支 $BRANCH 和 tags 到所有远端"

View File

@@ -106,8 +106,8 @@ func printMacOSPlist(root, minMacOSVersion string) error {
} }
func printAssetName(root string, args []string) error { func printAssetName(root string, args []string) error {
if len(args) < 2 { if len(args) == 0 {
return fmt.Errorf("asset-name 至少需要 kind 和 platform 参数") return fmt.Errorf("asset-name 需要组件参数: server|web|desktop")
} }
version, err := projectversion.ReadString(root) version, err := projectversion.ReadString(root)
@@ -115,30 +115,31 @@ func printAssetName(root string, args []string) error {
return err return err
} }
var platform, arch, format string
switch args[0] { switch args[0] {
case "server": case "server", "desktop":
if len(args) != 3 { if len(args) != 4 {
return fmt.Errorf("server 资产命名需要 platformarch 参数") return fmt.Errorf("%s 资产命名需要 platformarch 和 format 参数", args[0])
} }
name, nameErr := projectversion.ServerAssetName(version, args[1], args[2]) platform = args[1]
if nameErr != nil { arch = args[2]
return nameErr format = args[3]
} case "web":
fmt.Println(name)
return nil
case "desktop":
if len(args) != 2 { if len(args) != 2 {
return fmt.Errorf("desktop 资产命名只需要 platform 参数") return fmt.Errorf("web 资产命名只需要 format 参数")
} }
name, nameErr := projectversion.DesktopAssetName(version, args[1]) format = args[1]
if nameErr != nil {
return nameErr
}
fmt.Println(name)
return nil
default: default:
return fmt.Errorf("不支持的资产类型 %q", args[0]) return fmt.Errorf("不支持的资产组件 %q", args[0])
} }
name, nameErr := projectversion.ReleaseAssetName(version, args[0], platform, arch, format)
if nameErr != nil {
return nameErr
}
fmt.Println(name)
return nil
} }
func mustGetwd() string { func mustGetwd() string {

View File

@@ -263,44 +263,86 @@ func ReadEnvVar(content, key string) (string, bool) {
return "", false return "", false
} }
func ServerAssetName(version, goos, arch string) (string, error) { func ReleaseAssetName(version, component, platform, arch, format string) (string, error) {
if _, err := Parse(version); err != nil { if _, err := Parse(version); err != nil {
return "", err return "", err
} }
switch goos { switch component {
case "linux", "windows", "darwin": case "server":
return serverAssetName(version, platform, arch, format)
case "web":
return webAssetName(version, platform, arch, format)
case "desktop":
return desktopAssetName(version, platform, arch, format)
default: default:
return "", fmt.Errorf("不支持的 server 平台 %q", goos) return "", fmt.Errorf("不支持的资产组件 %q", component)
} }
if arch == "" {
return "", errors.New("server 资产命名缺少架构")
}
ext := ".tar.gz"
if goos == "windows" {
ext = ".zip"
}
return fmt.Sprintf("nex-server_%s_%s_%s%s", version, goos, arch, ext), nil
} }
func DesktopAssetName(version, platform string) (string, error) { func serverAssetName(version, platform, arch, format string) (string, error) {
if _, err := Parse(version); err != nil { if !validCombination(platform, arch, format, []releaseAssetTarget{
return "", err {platform: "linux", arch: "amd64", format: "tar.gz"},
{platform: "linux", arch: "arm64", format: "tar.gz"},
{platform: "macos", arch: "amd64", format: "tar.gz"},
{platform: "macos", arch: "arm64", format: "tar.gz"},
{platform: "macos", arch: "universal", format: "tar.gz"},
{platform: "windows", arch: "amd64", format: "zip"},
{platform: "windows", arch: "arm64", format: "zip"},
}) {
return "", fmt.Errorf("不支持的 server 资产目标 %s/%s/%s", platform, arch, format)
} }
switch platform { return fmt.Sprintf("nex-server_%s_%s_%s.%s", version, platform, arch, format), nil
case "linux": }
return fmt.Sprintf("Nex_%s_linux_amd64.tar.gz", version), nil
case "windows": func webAssetName(version, platform, arch, format string) (string, error) {
return fmt.Sprintf("Nex_%s_windows_amd64.zip", version), nil if platform != "" || arch != "" {
case "macos": return "", errors.New("web 资产命名不支持平台或架构参数")
return fmt.Sprintf("Nex_%s_macOS_universal.zip", version), nil
default:
return "", fmt.Errorf("不支持的 desktop 平台 %q", platform)
} }
if format != "tar.gz" {
return "", fmt.Errorf("不支持的 web 资产格式 %q", format)
}
return fmt.Sprintf("nex-web_%s.tar.gz", version), nil
}
func desktopAssetName(version, platform, arch, format string) (string, error) {
if !validCombination(platform, arch, format, []releaseAssetTarget{
{platform: "linux", arch: "amd64", format: "tar.gz"},
{platform: "linux", arch: "amd64", format: "AppImage"},
{platform: "linux", arch: "amd64", format: "deb"},
{platform: "linux", arch: "amd64", format: "rpm"},
{platform: "linux", arch: "arm64", format: "tar.gz"},
{platform: "linux", arch: "arm64", format: "AppImage"},
{platform: "linux", arch: "arm64", format: "deb"},
{platform: "linux", arch: "arm64", format: "rpm"},
{platform: "macos", arch: "universal", format: "zip"},
{platform: "macos", arch: "universal", format: "dmg"},
{platform: "windows", arch: "amd64", format: "zip"},
{platform: "windows", arch: "arm64", format: "zip"},
}) {
return "", fmt.Errorf("不支持的 desktop 资产目标 %s/%s/%s", platform, arch, format)
}
return fmt.Sprintf("nex-desktop_%s_%s_%s.%s", version, platform, arch, format), nil
}
type releaseAssetTarget struct {
platform string
arch string
format string
}
func validCombination(platform, arch, format string, targets []releaseAssetTarget) bool {
for _, target := range targets {
if target.platform == platform && target.arch == arch && target.format == format {
return true
}
}
return false
} }
func DesktopInfoPlist(version, minMacOSVersion string) (string, error) { func DesktopInfoPlist(version, minMacOSVersion string) (string, error) {

View File

@@ -83,20 +83,72 @@ func TestVerifyTag(t *testing.T) {
} }
func TestAssetNames(t *testing.T) { func TestAssetNames(t *testing.T) {
linuxServer, err := ServerAssetName("1.2.3", "linux", "amd64") testCases := []struct {
require.NoError(t, err) name string
assert.Equal(t, "nex-server_1.2.3_linux_amd64.tar.gz", linuxServer) component string
platform string
arch string
format string
want string
}{
{"server linux amd64", "server", "linux", "amd64", "tar.gz", "nex-server_1.2.3_linux_amd64.tar.gz"},
{"server linux arm64", "server", "linux", "arm64", "tar.gz", "nex-server_1.2.3_linux_arm64.tar.gz"},
{"server macos amd64", "server", "macos", "amd64", "tar.gz", "nex-server_1.2.3_macos_amd64.tar.gz"},
{"server macos arm64", "server", "macos", "arm64", "tar.gz", "nex-server_1.2.3_macos_arm64.tar.gz"},
{"server macos universal", "server", "macos", "universal", "tar.gz", "nex-server_1.2.3_macos_universal.tar.gz"},
{"server windows amd64", "server", "windows", "amd64", "zip", "nex-server_1.2.3_windows_amd64.zip"},
{"server windows arm64", "server", "windows", "arm64", "zip", "nex-server_1.2.3_windows_arm64.zip"},
{"web", "web", "", "", "tar.gz", "nex-web_1.2.3.tar.gz"},
{"desktop linux amd64 tar", "desktop", "linux", "amd64", "tar.gz", "nex-desktop_1.2.3_linux_amd64.tar.gz"},
{"desktop linux amd64 appimage", "desktop", "linux", "amd64", "AppImage", "nex-desktop_1.2.3_linux_amd64.AppImage"},
{"desktop linux amd64 deb", "desktop", "linux", "amd64", "deb", "nex-desktop_1.2.3_linux_amd64.deb"},
{"desktop linux amd64 rpm", "desktop", "linux", "amd64", "rpm", "nex-desktop_1.2.3_linux_amd64.rpm"},
{"desktop linux arm64 tar", "desktop", "linux", "arm64", "tar.gz", "nex-desktop_1.2.3_linux_arm64.tar.gz"},
{"desktop linux arm64 appimage", "desktop", "linux", "arm64", "AppImage", "nex-desktop_1.2.3_linux_arm64.AppImage"},
{"desktop linux arm64 deb", "desktop", "linux", "arm64", "deb", "nex-desktop_1.2.3_linux_arm64.deb"},
{"desktop linux arm64 rpm", "desktop", "linux", "arm64", "rpm", "nex-desktop_1.2.3_linux_arm64.rpm"},
{"desktop macos zip", "desktop", "macos", "universal", "zip", "nex-desktop_1.2.3_macos_universal.zip"},
{"desktop macos dmg", "desktop", "macos", "universal", "dmg", "nex-desktop_1.2.3_macos_universal.dmg"},
{"desktop windows amd64", "desktop", "windows", "amd64", "zip", "nex-desktop_1.2.3_windows_amd64.zip"},
{"desktop windows arm64", "desktop", "windows", "arm64", "zip", "nex-desktop_1.2.3_windows_arm64.zip"},
}
macServer, err := ServerAssetName("1.2.3", "darwin", "arm64") for _, tc := range testCases {
require.NoError(t, err) t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, "nex-server_1.2.3_darwin_arm64.tar.gz", macServer) got, err := ReleaseAssetName("1.2.3", tc.component, tc.platform, tc.arch, tc.format)
require.NoError(t, err)
assert.Equal(t, tc.want, got)
})
}
macDesktop, err := DesktopAssetName("1.2.3", "macos") invalidCases := []struct {
require.NoError(t, err) name string
assert.Equal(t, "Nex_1.2.3_macOS_universal.zip", macDesktop) component string
platform string
arch string
format string
}{
{"invalid version", "server", "linux", "amd64", "tar.gz"},
{"invalid component", "mobile", "linux", "amd64", "tar.gz"},
{"darwin platform", "server", "darwin", "arm64", "tar.gz"},
{"server unsupported format", "server", "linux", "amd64", "zip"},
{"server unsupported arch", "server", "windows", "universal", "zip"},
{"web with platform", "web", "linux", "amd64", "tar.gz"},
{"web unsupported format", "web", "", "", "zip"},
{"desktop unsupported platform", "desktop", "ios", "arm64", "zip"},
{"desktop unsupported format", "desktop", "macos", "universal", "tar.gz"},
}
_, err = DesktopAssetName("1.2.3", "ios") for _, tc := range invalidCases {
assert.Error(t, err) t.Run(tc.name, func(t *testing.T) {
version := "1.2.3"
if tc.name == "invalid version" {
version = "1.2"
}
_, err := ReleaseAssetName(version, tc.component, tc.platform, tc.arch, tc.format)
assert.Error(t, err)
})
}
} }
func TestDesktopInfoPlist(t *testing.T) { func TestDesktopInfoPlist(t *testing.T) {