diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aa779d2..3391c7b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,8 +37,8 @@ jobs: go run ./versionctl verify-tag "${GITHUB_REF_NAME}" printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT" - build-linux: - name: Build Linux Assets + build-web: + name: Build Web Asset needs: prepare runs-on: ubuntu-latest permissions: @@ -60,14 +60,65 @@ jobs: - name: Setup Bun 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: | 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 run: | set -euo pipefail + printf 'runner arch: %s\n' "$(uname -m)" command -v go go version command -v bun @@ -77,21 +128,42 @@ jobs: command -v pkg-config pkg-config --modversion ayatana-appindicator3-0.1 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 - name: Build Linux release assets - run: make release-assets-linux + run: make release-assets-linux TARGET_ARCH=${{ matrix.arch }} - name: Upload Linux release assets uses: actions/upload-artifact@v4 with: - name: release-linux + name: release-linux-${{ matrix.arch }} path: build/release/* + if-no-files-found: error build-windows: - name: Build Windows Assets + name: Build Windows ${{ matrix.arch }} Assets needs: prepare 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: contents: read steps: @@ -114,12 +186,10 @@ jobs: - name: Setup MSYS2 toolchain uses: msys2/setup-msys2@v2 with: - msystem: MINGW64 + msystem: ${{ matrix.msystem }} path-type: inherit update: true - install: >- - make - mingw-w64-x86_64-gcc + install: ${{ matrix.packages }} - name: Preflight Windows release toolchain shell: msys2 {0} @@ -131,10 +201,21 @@ jobs: bun --version command -v make make --version - command -v gcc - gcc --version - command -v windres - windres --version + if [ "${{ matrix.arch }}" = "arm64" ]; then + command -v clang + clang --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 powershell.exe -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' else @@ -145,18 +226,19 @@ jobs: - name: Build Windows release assets shell: msys2 {0} - run: make release-assets-windows + run: make release-assets-windows TARGET_ARCH=${{ matrix.arch }} - name: Upload Windows release assets uses: actions/upload-artifact@v4 with: - name: release-windows + name: release-windows-${{ matrix.arch }} path: build/release/* + if-no-files-found: error build-macos: name: Build macOS Assets needs: prepare - runs-on: macos-latest + runs-on: macos-15 permissions: contents: read steps: @@ -179,11 +261,13 @@ jobs: - name: Preflight macOS release toolchain run: | set -euo pipefail + printf 'runner arch: %s\n' "$(uname -m)" command -v go go version command -v bun bun --version command -v ditto + command -v hdiutil xcrun --find lipo xcrun --find vtool make release-assets-check @@ -196,14 +280,18 @@ jobs: with: name: release-macos path: build/release/* + if-no-files-found: error 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 permissions: contents: write steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Download release assets uses: actions/download-artifact@v4 with: @@ -211,6 +299,9 @@ jobs: merge-multiple: true path: dist + - name: Generate checksums + run: make release-assets-checksums RELEASE_DIR=dist + - name: Publish draft release uses: softprops/action-gh-release@v2 with: diff --git a/Makefile b/Makefile index 7c63aa0..ee55591 100644 --- a/Makefile +++ b/Makefile @@ -4,12 +4,17 @@ 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-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 \ _versionctl-lint _versionctl-test \ _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 \ - _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. 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) 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") +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_WIN = $(GO_LDFLAGS) -H=windowsgui -RELEASE_DIR := build/release -SERVER_LINUX_ASSET = $(call lazy_shell,_SERVER_LINUX_ASSET,go run ./versionctl asset-name server linux amd64) -SERVER_WINDOWS_ASSET = $(call lazy_shell,_SERVER_WINDOWS_ASSET,go run ./versionctl asset-name server windows amd64) -SERVER_DARWIN_AMD64_ASSET = $(call lazy_shell,_SERVER_DARWIN_AMD64_ASSET,go run ./versionctl asset-name server darwin amd64) -SERVER_DARWIN_ARM64_ASSET = $(call lazy_shell,_SERVER_DARWIN_ARM64_ASSET,go run ./versionctl asset-name server darwin arm64) -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) -DESKTOP_MACOS_ASSET = $(call lazy_shell,_DESKTOP_MACOS_ASSET,go run ./versionctl asset-name desktop macos) +RELEASE_DIR ?= build/release +LINUX_DESKTOP_BINARY = build/nex-linux-$(TARGET_ARCH) +WINDOWS_DESKTOP_BINARY = build/nex-win-$(TARGET_ARCH).exe +WINDOWS_SERVER_BINARY = build/nex-server-windows-$(TARGET_ARCH).exe +WINDRES ?= windres + +ifeq ($(TARGET_ARCH),arm64) +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=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 -info build/nex-mac-universal | grep -q 'x86_64 arm64' @printf 'Packaging macOS app bundle...\n' + rm -rf build/Nex.app mkdir -p build/Nex.app/Contents/MacOS build/Nex.app/Contents/Resources cp build/nex-mac-universal build/Nex.app/Contents/MacOS/nex @if [ -f assets/icon.icns ]; then \ cp assets/icon.icns build/Nex.app/Contents/Resources/; \ else \ printf 'Missing assets/icon.icns\n'; \ + exit 1; \ fi @MIN_MACOS_VERSION=$$(vtool -show-build build/nex-mac-universal | awk '/minos / {print $$2; exit}'); \ 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 @printf 'macOS desktop build complete\n' -desktop-build-win: version-check _desktop-prepare-frontend _desktop-prepare-embedfs _desktop-prepare-windows-resource - @printf 'Building Windows desktop...\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 +desktop-build-win: version-check _desktop-prepare-frontend _desktop-prepare-embedfs _desktop-prepare-windows-resource _check-windows-target-arch + @printf 'Building Windows desktop $(TARGET_ARCH)...\n' 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 -endif + cd backend && CGO_ENABLED=1 GOOS=windows GOARCH=$(TARGET_ARCH) go build -ldflags "$(GO_LDFLAGS_WIN)" -o ../$(WINDOWS_DESKTOP_BINARY) ./cmd/desktop @printf 'Windows desktop build complete\n' -desktop-build-linux: version-check _desktop-prepare-frontend _desktop-prepare-embedfs - @printf 'Building Linux desktop...\n' - cd backend && CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-linux-amd64 ./cmd/desktop +desktop-build-linux: version-check _desktop-prepare-frontend _desktop-prepare-embedfs _check-linux-target-arch + @printf 'Building Linux desktop $(TARGET_ARCH)...\n' + 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' desktop-lint: _backend-lint _frontend-check @@ -140,44 +160,29 @@ _desktop-test: cd backend && go test ./cmd/desktop/... -v _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 @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 && bun run build rm -f frontend/.env.production.local -endif _desktop-prepare-embedfs: @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 cp -r assets embedfs/assets cp -r frontend/dist embedfs/frontend-dist -endif -_desktop-prepare-windows-resource: - @printf 'Preparing Windows executable icon...\n' -ifeq ($(OS),Windows_NT) - cd backend/cmd/desktop && windres -O coff -F pe-x86-64 -i icon_windows.rc -o rsrc_windows_amd64.syso -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; \ +_desktop-prepare-windows-resource: _check-windows-target-arch + @printf 'Preparing Windows $(TARGET_ARCH) executable icon...\n' + @if [ "$(TARGET_ARCH)" = "arm64" ] && [ "$(WINDRES)" = "windres" ] && command -v llvm-windres >/dev/null 2>&1; then \ + WINDRES_CMD=llvm-windres; \ else \ - printf 'Missing windres for Windows icon resource generation\n'; \ - exit 1; \ - fi -endif + WINDRES_CMD="$(WINDRES)"; \ + fi; \ + command -v "$$WINDRES_CMD" >/dev/null 2>&1 || { printf 'Missing windres tool: %s\n' "$$WINDRES_CMD"; exit 1; }; \ + 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 @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)" 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 - tar -C build -czf "$(RELEASE_DIR)/$(SERVER_LINUX_ASSET)" nex-server-linux-amd64 - tar -C build -czf "$(RELEASE_DIR)/$(DESKTOP_LINUX_ASSET)" nex-linux-amd64 + asset=$$(go run ./versionctl asset-name web tar.gz); \ + tar -C frontend -czf "$(RELEASE_DIR)/$$asset" dist -release-assets-windows: version-check release-assets-check desktop-build-win -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 +release-assets-linux: version-check release-assets-check _check-linux-target-arch rm -rf "$(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 - cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-server-darwin-arm64 ./cmd/server - 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 - ditto -c -k --keepParent build/Nex.app "$(RELEASE_DIR)/$(DESKTOP_MACOS_ASSET)" + @$(MAKE) release-assets-server-linux TARGET_ARCH=$(TARGET_ARCH) RELEASE_DIR="$(RELEASE_DIR)" + @$(MAKE) release-assets-desktop-linux TARGET_ARCH=$(TARGET_ARCH) RELEASE_DIR="$(RELEASE_DIR)" + +release-assets-windows: version-check release-assets-check _check-windows-target-arch + 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 ' \ + '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 diff --git a/README.md b/README.md index b8b3306..5179cc3 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ nex/ │ ├── icon.icns # macOS 应用图标 │ └── icon.ico # Windows 应用图标 │ +├── packaging/ # 桌面发布包元数据(Linux desktop entry、RPM spec 等) +│ └── README.md # 本文件 ``` @@ -107,12 +109,18 @@ make desktop-build-mac # Windows make desktop-build-win +# Windows arm64 +make desktop-build-win TARGET_ARCH=arm64 + # Linux make desktop-build-linux + +# Linux arm64 +make desktop-build-linux TARGET_ARCH=arm64 ``` **使用桌面应用**: -- 双击启动应用(macOS: Nex.app,Windows: nex-win-amd64.exe,Linux: nex-linux-amd64) +- 双击启动应用(macOS: Nex.app,Windows: nex-win-amd64.exe / nex-win-arm64.exe,Linux: nex-linux-amd64 / nex-linux-arm64) - 系统托盘图标出现,浏览器自动打开管理界面 - 点击托盘图标显示菜单,可打开管理界面或退出 - 关闭浏览器后服务继续运行,可通过托盘重新打开 @@ -120,8 +128,10 @@ make desktop-build-linux **注意事项**: - 桌面应用需要 CGO 支持 - macOS: 自带 Xcode Command Line Tools -- Linux: 自带 gcc,部分桌面环境需要 `libappindicator3-dev` -- Windows: 需要 MinGW-w64 或在 Windows 环境构建 +- Linux 构建: 需要 gcc、pkg-config、GTK3 开发包和 Ayatana AppIndicator 开发包(Ubuntu/Debian: `libgtk-3-dev`、`libayatana-appindicator3-dev`) +- Linux 运行: 需要 GTK3、Ayatana AppIndicator 和 xdg-utils;AppImage 也依赖系统提供 AppImage runtime/FUSE 能力,不承诺完全自包含 +- Windows: 需要对应架构的 MinGW-w64/MSYS2 工具链,desktop 使用 GUI linker flags 隐藏控制台窗口 +- macOS DMG: 发布包暂不签名、不 notarize,首次打开可能出现 Gatekeeper 提示 **Linux 桌面环境兼容性**: - GNOME: 需要 AppIndicator 扩展 @@ -151,6 +161,40 @@ make server-run make server-build ``` +### Release 产物 + +发布流程由 Git tag `vX.Y.Z` 触发,GitHub Actions 会创建 Draft Release 并上传 server、web 和 desktop 三类产物,同时生成 `SHA256SUMS`。 + +**server 产物**(不内置 Web 管理界面): + +| 平台 | 产物 | +|------|------| +| Linux amd64 | `nex-server__linux_amd64.tar.gz` | +| Linux arm64 | `nex-server__linux_arm64.tar.gz` | +| macOS amd64 | `nex-server__macos_amd64.tar.gz` | +| macOS arm64 | `nex-server__macos_arm64.tar.gz` | +| macOS universal | `nex-server__macos_universal.tar.gz` | +| Windows amd64 | `nex-server__windows_amd64.zip` | +| Windows arm64 | `nex-server__windows_arm64.zip` | + +**web 产物**: + +| 内容 | 产物 | +|------|------| +| `frontend/dist` | `nex-web_.tar.gz` | + +**desktop 产物**: + +| 平台 | 产物 | +|------|------| +| Linux amd64 | `nex-desktop__linux_amd64.tar.gz`、`.AppImage`、`.deb`、`.rpm` | +| Linux arm64 | `nex-desktop__linux_arm64.tar.gz`、`.AppImage`、`.deb`、`.rpm` | +| macOS universal | `nex-desktop__macos_universal.zip`、`nex-desktop__macos_universal.dmg` | +| Windows amd64 | `nex-desktop__windows_amd64.zip` | +| Windows arm64 | `nex-desktop__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 接口 ### 代理接口(对外部应用) diff --git a/openspec/specs/desktop-app/spec.md b/openspec/specs/desktop-app/spec.md index 45f5d36..d5f6d81 100644 --- a/openspec/specs/desktop-app/spec.md +++ b/openspec/specs/desktop-app/spec.md @@ -139,29 +139,92 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源 ### Requirement: 跨平台构建 -系统 SHALL 支持跨平台构建和打包。构建 target SHALL 按平台分离,中间构建产物文件名 SHALL 保持 `nex-{os}-{arch}[.exe]` 格式,最终桌面发布资产文件名 SHALL 包含统一版本号和平台标识。 +系统 SHALL 支持跨平台构建和打包。构建 target SHALL 按平台和架构分离或参数化,中间构建产物文件名 SHALL 保持可区分目标平台和架构,最终桌面发布资产文件名 SHALL 包含统一版本号、组件、平台、架构和格式信息。 #### Scenario: macOS 构建 -- **WHEN** 执行 `desktop-build-mac` 构建命令且当前版本为 `1.2.3` -- **THEN** 生成 `nex-mac-arm64` 和 `nex-mac-amd64` 可执行文件 -- **AND** 系统生成可打包为 `.app` bundle 的 macOS 桌面产物 -- **AND** 最终发布资产文件名 SHALL 包含 `1.2.3` 和 `macOS` 平台标识 +- **WHEN** 执行 macOS desktop 构建命令且当前版本为 `1.2.3` +- **THEN** 系统 SHALL 生成 macOS arm64 和 amd64 桌面可执行文件 +- **AND** 系统 SHALL 使用 `lipo` 生成 macOS universal 桌面可执行文件 +- **AND** 系统 SHALL 生成可打包为 `.app` bundle 的 macOS desktop 产物 +- **AND** 最终 macOS desktop 发布资产文件名 SHALL 包含 `1.2.3`、`macos` 和 `universal` #### Scenario: Windows 构建 -- **WHEN** 执行 `desktop-build-win` 构建命令且当前版本为 `1.2.3` -- **THEN** 系统生成 Windows 桌面可执行文件 -- **AND** 生成 `nex-win-amd64.exe` 可执行文件 -- **AND** 使用 `-H=windowsgui` linker flag 隐藏控制台窗口 -- **AND** 最终发布资产文件名 SHALL 包含 `1.2.3` 和 `windows` 平台标识 +- **WHEN** 执行 Windows desktop 构建命令且当前版本为 `1.2.3` +- **THEN** 系统 SHALL 生成 Windows amd64 和 arm64 desktop 可执行文件 +- **AND** Windows desktop 构建 SHALL 使用 `-H=windowsgui` linker flag 隐藏控制台窗口 +- **AND** 最终 Windows desktop 发布资产文件名 SHALL 包含 `1.2.3`、`windows` 和对应架构标识 #### Scenario: Linux 构建 -- **WHEN** 执行 `desktop-build-linux` 构建命令且当前版本为 `1.2.3` -- **THEN** 系统生成 Linux 桌面可执行文件 -- **AND** 生成 `nex-linux-amd64` 可执行文件 -- **AND** 最终发布资产文件名 SHALL 包含 `1.2.3` 和 `linux` 平台标识 +- **WHEN** 执行 Linux desktop 构建命令且当前版本为 `1.2.3` +- **THEN** 系统 SHALL 生成 Linux amd64 和 arm64 desktop 可执行文件 +- **AND** Linux desktop 构建 SHALL 使用 CGO 和 GTK/AppIndicator 构建依赖 +- **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__linux_amd64.tar.gz` +- **AND** 系统 SHALL 为 arm64 生成 `nex-desktop__linux_arm64.tar.gz` + +#### Scenario: Linux desktop AppImage 包 + +- **WHEN** 构建 Linux desktop AppImage 发布资产 +- **THEN** 系统 SHALL 为 amd64 生成 `nex-desktop__linux_amd64.AppImage` +- **AND** 系统 SHALL 为 arm64 生成 `nex-desktop__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__linux_amd64.deb` +- **AND** 系统 SHALL 为 arm64 生成 `nex-desktop__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__linux_amd64.rpm` +- **AND** 系统 SHALL 为 arm64 生成 `nex-desktop__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 打包 diff --git a/openspec/specs/release-pipeline/spec.md b/openspec/specs/release-pipeline/spec.md index 212cf4c..10c1014 100644 --- a/openspec/specs/release-pipeline/spec.md +++ b/openspec/specs/release-pipeline/spec.md @@ -32,88 +32,116 @@ ### Requirement: 三平台发布构建 -系统 SHALL 在发布流水线中构建 server 与 desktop 的 Linux、Windows、macOS 三个平台产物。 +系统 SHALL 在发布流水线中构建 server、web 与 desktop 的发布产物,并覆盖 Linux、Windows、macOS 的目标架构和格式矩阵。 -#### Scenario: Linux 发布构建 +#### Scenario: server 发布构建 -- **WHEN** 发布流水线执行 Linux 构建 job -- **THEN** 系统 SHALL 在可访问 Go、Bun 和 Linux 桌面构建依赖的 shell 环境中执行 Linux 发布构建 -- **AND** 系统 SHALL 生成 Linux server 发布资产 -- **AND** 系统 SHALL 生成 Linux desktop 发布资产 +- **WHEN** 发布流水线执行 server 发布构建 +- **THEN** 系统 SHALL 生成 `nex-server__linux_amd64.tar.gz` +- **AND** 系统 SHALL 生成 `nex-server__linux_arm64.tar.gz` +- **AND** 系统 SHALL 生成 `nex-server__macos_amd64.tar.gz` +- **AND** 系统 SHALL 生成 `nex-server__macos_arm64.tar.gz` +- **AND** 系统 SHALL 生成 `nex-server__macos_universal.tar.gz` +- **AND** 系统 SHALL 生成 `nex-server__windows_amd64.zip` +- **AND** 系统 SHALL 生成 `nex-server__windows_arm64.zip` -#### Scenario: Windows 发布构建 +#### Scenario: web 发布构建 -- **WHEN** 发布流水线执行 Windows 构建 job -- **THEN** 系统 SHALL 在包含 MSYS2 / MINGW64 构建工具且可访问 Go 与 Bun 工具链的 shell 环境中执行 Windows 发布构建 -- **AND** 系统 SHALL 生成 Windows server 发布资产 -- **AND** 系统 SHALL 生成 Windows desktop 发布资产 +- **WHEN** 发布流水线执行 web 发布构建 +- **THEN** 系统 SHALL 使用 Bun 构建 `frontend/dist` +- **AND** 系统 SHALL 将前端静态资源打包为 `nex-web_.tar.gz` +- **AND** server 发布资产 SHALL NOT 内置 Web 管理界面静态资源 -#### Scenario: macOS 发布构建 +#### Scenario: Linux desktop 发布构建 -- **WHEN** 发布流水线执行 macOS 构建 job -- **THEN** 系统 SHALL 在可访问 Go、Bun 和 macOS 打包工具链的 shell 环境中执行 macOS 发布构建 -- **AND** 系统 SHALL 生成 darwin-amd64 server 发布资产 -- **AND** 系统 SHALL 生成 darwin-arm64 server 发布资产 -- **AND** 系统 SHALL 生成 macOS desktop universal 发布资产 +- **WHEN** 发布流水线执行 Linux desktop 发布构建 +- **THEN** 系统 SHALL 在可访问 Go、Bun、CGO、GTK3、Ayatana AppIndicator 和 Linux 打包工具链的环境中构建 +- **AND** 系统 SHALL 为 `amd64` 和 `arm64` 分别生成 tar.gz、AppImage、deb 和 rpm desktop 发布资产 +- **AND** Linux arm64 desktop 发布构建 SHALL 使用原生 arm64 runner 或等价的 arm64 Linux 构建环境 + +#### Scenario: Windows desktop 发布构建 + +- **WHEN** 发布流水线执行 Windows desktop 发布构建 +- **THEN** 系统 SHALL 在包含对应架构 MSYS2/MinGW 或等价 CGO 工具链的环境中构建 +- **AND** 系统 SHALL 生成 `nex-desktop__windows_amd64.zip` +- **AND** 系统 SHALL 生成 `nex-desktop__windows_arm64.zip` + +#### Scenario: macOS desktop 发布构建 + +- **WHEN** 发布流水线执行 macOS desktop 发布构建 +- **THEN** 系统 SHALL 在可访问 Go、Bun、Xcode 命令行工具、`lipo`、`hdiutil` 和 zip 打包工具的 macOS 环境中构建 +- **AND** 系统 SHALL 生成 `nex-desktop__macos_universal.zip` +- **AND** 系统 SHALL 生成 `nex-desktop__macos_universal.dmg` ### Requirement: 三平台发布构建预检 -系统 SHALL 在正式执行各平台 `make release-assets-*` 前验证对应发布 job 的关键工具链可用性,并在环境不完整时快速失败且输出明确诊断。 +系统 SHALL 在正式执行各平台 release 构建前验证对应 job 的关键工具链可用性,并在环境不完整时快速失败且输出明确诊断。 #### Scenario: Linux 预检通过后开始构建 -- **WHEN** Linux 发布 job 中的 `go`、`bun` 与 Linux 桌面构建依赖均可用 +- **WHEN** Linux 发布 job 中的 `go`、`bun`、`gcc`、`pkg-config`、GTK3、Ayatana AppIndicator 和 Linux 打包工具均可用 - **THEN** 系统 SHALL 输出关键工具的版本信息或解析路径 -- **AND** 系统 SHALL 继续执行 `make release-assets-linux` +- **AND** 系统 SHALL 继续执行对应 Linux release 构建 #### Scenario: Windows 预检通过后开始构建 -- **WHEN** Windows 发布 job 中的 `go`、`bun` 与 MSYS2 构建工具均可用 +- **WHEN** Windows 发布 job 中的 `go`、`bun`、`make`、对应架构 CGO 编译器和 resource 生成工具均可用 - **THEN** 系统 SHALL 输出关键工具的版本信息或解析路径 -- **AND** 系统 SHALL 继续执行 `make release-assets-windows` +- **AND** 系统 SHALL 继续执行对应 Windows release 构建 #### Scenario: macOS 预检通过后开始构建 -- **WHEN** macOS 发布 job 中的 `go`、`bun` 与 macOS 打包工具均可用 +- **WHEN** macOS 发布 job 中的 `go`、`bun`、`ditto`、`lipo`、`vtool` 和 `hdiutil` 均可用 - **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 在正式构建前失败 - **AND** 系统 SHALL 在日志中标识缺失的工具链名称 ### Requirement: 发布流水线 LFS 资产拉取 -发布流水线 SHALL 在所有会 checkout 仓库并参与版本校验或平台构建的 job 中拉取 Git LFS 真实文件,确保发布构建读取到真实二进制资产而非 LFS pointer 文本。 +发布流水线 SHALL 在所有会 checkout 仓库并参与版本校验、web 构建、server 构建或 desktop 构建的 job 中拉取 Git LFS 真实文件,确保发布构建读取到真实二进制资产而非 LFS pointer 文本。 #### Scenario: 发布 job 获取真实 LFS 图标资产 -- **WHEN** 发布流水线执行 `prepare`、`build-linux`、`build-windows` 或 `build-macos` job 的 checkout 步骤 +- **WHEN** 发布流水线执行任一参与版本校验、web 构建、server 构建或 desktop 构建的 job 的 checkout 步骤 - **THEN** checkout 步骤 SHALL 拉取 Git LFS 文件 - **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: 发布资产图标预检 -发布流水线 SHALL 在正式执行各平台发布构建前校验关键图标资产可用,并在检测到 LFS pointer 或错误格式时快速失败且输出明确诊断。 +发布流水线 SHALL 在正式执行任何需要图标资产、前端 public 图标或 desktop 打包资源的发布构建前校验关键图标资产可用,并在检测到 LFS pointer 或错误格式时快速失败且输出明确诊断。 #### Scenario: 图标资产为 LFS pointer - **WHEN** 发布资产预检发现关键图标文件内容为 Git LFS pointer 文本 -- **THEN** 发布流水线 SHALL 在执行平台发布构建前失败 +- **THEN** 发布流水线 SHALL 在执行对应 release 构建前失败 - **AND** 系统 SHALL 在日志中标识对应图标文件需要拉取 Git LFS 真实内容 #### Scenario: 图标资产格式无效 - **WHEN** 发布资产预检发现关键图标文件不是对应格式的有效资源 -- **THEN** 发布流水线 SHALL 在执行平台发布构建前失败 +- **THEN** 发布流水线 SHALL 在执行对应 release 构建前失败 - **AND** 系统 SHALL 在日志中标识格式无效的图标文件路径 #### Scenario: 图标资产预检通过 - **WHEN** `assets/icon.ico`、`assets/icon.icns`、`assets/icon.png` 和 `frontend/public/icon.png` 均为真实且格式可用的图标资产 -- **THEN** 发布流水线 SHALL 继续执行对应平台的 `make release-assets-*` 构建 +- **THEN** 发布流水线 SHALL 继续执行依赖这些资产的 release 构建 ### Requirement: 发布流水线运行时兼容性 @@ -131,34 +159,54 @@ ### Requirement: 版本化发布资产命名 -系统 SHALL 为 server 与 desktop 的发布资产使用包含统一版本号和目标平台信息的文件名,确保 Release 页面可直接区分产物用途与平台。 +系统 SHALL 为 server、web 与 desktop 发布资产使用包含统一版本号、组件、目标平台和目标架构信息的文件名,确保 Release 页面可直接区分产物用途、平台、架构和格式。 #### Scenario: server 资产命名 - **WHEN** 当前发布版本为 `1.2.3` -- **THEN** Linux server 发布资产文件名 SHALL 包含 `1.2.3`、`linux` 和 `amd64` -- **AND** Windows server 发布资产文件名 SHALL 包含 `1.2.3`、`windows` 和 `amd64` -- **AND** macOS server 发布资产文件名 SHALL 分别包含 `1.2.3`、`darwin`、`amd64` 与 `1.2.3`、`darwin`、`arm64` +- **THEN** Linux server 发布资产文件名 SHALL 为 `nex-server_1.2.3_linux_amd64.tar.gz` 和 `nex-server_1.2.3_linux_arm64.tar.gz` +- **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** 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 资产命名 - **WHEN** 当前发布版本为 `1.2.3` -- **THEN** Linux desktop 发布资产文件名 SHALL 包含 `1.2.3` 和 `linux` -- **AND** Windows desktop 发布资产文件名 SHALL 包含 `1.2.3` 和 `windows` -- **AND** macOS desktop universal 发布资产文件名 SHALL 包含 `1.2.3` 和 `macOS` +- **THEN** Linux desktop 发布资产文件名 SHALL 使用 `nex-desktop_1.2.3_linux_.` 格式 +- **AND** Windows desktop 发布资产文件名 SHALL 为 `nex-desktop_1.2.3_windows_amd64.zip` 和 `nex-desktop_1.2.3_windows_arm64.zip` +- **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 组装 -系统 SHALL 将发布流水线产物上传到 GitHub Draft Release,由人工确认后再公开发布。 +系统 SHALL 将发布流水线产物上传到 GitHub Draft Release,由人工确认后再公开发布,并 SHALL 生成覆盖全部发布资产的校验和清单。 #### Scenario: 发布成功时创建 Draft Release -- **WHEN** 版本校验通过且三平台发布资产构建完成 +- **WHEN** 版本校验通过且 server、web、desktop 的全部目标发布资产构建完成 - **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: 构建失败时阻止完成发布 -- **WHEN** 任一平台发布资产构建失败或版本校验失败 +- **WHEN** 任一目标发布资产构建失败、打包失败、校验失败、artifact 上传为空或版本校验失败 - **THEN** 发布流水线 SHALL 失败 - **AND** 系统 SHALL NOT 产生可直接公开的成功发布结果 + +#### Scenario: artifact 缺失时快速失败 + +- **WHEN** 任一构建 job 尝试上传 release artifact 但匹配不到目标文件 +- **THEN** 该 job SHALL 失败 +- **AND** Draft Release 组装 SHALL NOT 继续发布不完整资产集合 diff --git a/openspec/specs/workspace-command-flows/spec.md b/openspec/specs/workspace-command-flows/spec.md index 9a3b89c..99ce60a 100644 --- a/openspec/specs/workspace-command-flows/spec.md +++ b/openspec/specs/workspace-command-flows/spec.md @@ -8,17 +8,26 @@ ### Requirement: 根目录公开命令分层 -根目录 `Makefile` SHALL 仅暴露全局命令、版本命令、server 产品命令、desktop 产品命令和 release 命令,不再作为 backend 局部维护命令或内部打包步骤的公开入口。 +根目录 `Makefile` SHALL 仅暴露全局命令、版本命令、server 产品命令、desktop 产品命令和 release 命令,不再作为 backend 局部维护命令或内部打包步骤的公开入口。release 命令 SHALL 使用 `release-assets` 前缀,并 SHALL 通过清晰的目标名或变量参数表达 component、platform、arch 和 format。 #### Scenario: 查看根目录公开命令 + - **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: 根目录不暴露局部和内部命令 + - **WHEN** 开发者查看根目录 `Makefile` 的公开 target - **THEN** SHALL NOT 暴露 `backend-*`、`frontend-*`、数据库迁移命令、MySQL 专项测试命令或 `desktop-prepare-*` 之类内部步骤 - **THEN** SHALL NOT 暴露 `dev`、`build`、`all`、`desktop-dev`、`desktop-build` 这类模糊或聚合式公共命令 +#### Scenario: release 内部步骤保持内部化 + +- **WHEN** 根目录 `Makefile` 需要复用 release 构建、打包、校验辅助步骤 +- **THEN** 内部辅助 target SHALL 使用 `_` 前缀或 Make 变量参数化方式表达 +- **AND** 内部辅助 target SHALL NOT 成为文档化的公共入口 + ### Requirement: 全局质量与清理命令 根目录 `Makefile` SHALL 提供 `lint`、`test`、`clean` 作为全仓默认入口。 @@ -97,12 +106,33 @@ ### Requirement: Release 命令沿用根目录入口 -根目录 `Makefile` SHALL 继续提供 `release-assets-*` 作为发布资产入口,并与新的版本校验规则保持一致。 +根目录 `Makefile` SHALL 继续提供 `release-assets` 前缀 target 作为发布资产入口,并与版本校验、发布资产预检和多组件打包规则保持一致。 #### Scenario: 执行 release 资产命令 -- **WHEN** 执行 `make release-assets-linux`、`make release-assets-windows` 或 `make release-assets-macos` + +- **WHEN** 执行任一 `release-assets` 前缀的公共 release target - **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_.tar.gz` +- **AND** SHALL NOT 修改前端版本镜像文件 + +#### Scenario: checksum release 产物生成 + +- **WHEN** 执行 release 汇总或 Draft Release 组装相关命令 +- **THEN** SHALL 能基于当前 release 产物目录生成 `SHA256SUMS` +- **AND** `SHA256SUMS` SHALL 覆盖除自身以外的全部 release 资产 ### Requirement: Backend 局部命令下沉 diff --git a/packaging/linux/AppRun b/packaging/linux/AppRun new file mode 100644 index 0000000..f9826fd --- /dev/null +++ b/packaging/linux/AppRun @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +APPDIR=$(dirname "$(readlink -f "$0")") +exec "$APPDIR/usr/bin/nex" "$@" diff --git a/packaging/linux/nex.desktop b/packaging/linux/nex.desktop new file mode 100644 index 0000000..75f7d23 --- /dev/null +++ b/packaging/linux/nex.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=Nex +Comment=AI Gateway +Exec=nex +Icon=nex +Terminal=false +Categories=Development;Network; +StartupNotify=false diff --git a/packaging/linux/nex.spec b/packaging/linux/nex.spec new file mode 100644 index 0000000..f89c903 --- /dev/null +++ b/packaging/linux/nex.spec @@ -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 diff --git a/scripts/push-all-remotes.sh b/scripts/push-all-remotes.sh new file mode 100755 index 0000000..f9a2f90 --- /dev/null +++ b/scripts/push-all-remotes.sh @@ -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 到所有远端" diff --git a/versionctl/main.go b/versionctl/main.go index d40fb26..2a2aa69 100644 --- a/versionctl/main.go +++ b/versionctl/main.go @@ -106,8 +106,8 @@ func printMacOSPlist(root, minMacOSVersion string) error { } func printAssetName(root string, args []string) error { - if len(args) < 2 { - return fmt.Errorf("asset-name 至少需要 kind 和 platform 参数") + if len(args) == 0 { + return fmt.Errorf("asset-name 需要组件参数: server|web|desktop") } version, err := projectversion.ReadString(root) @@ -115,30 +115,31 @@ func printAssetName(root string, args []string) error { return err } + var platform, arch, format string switch args[0] { - case "server": - if len(args) != 3 { - return fmt.Errorf("server 资产命名需要 platform 和 arch 参数") + case "server", "desktop": + if len(args) != 4 { + return fmt.Errorf("%s 资产命名需要 platform、arch 和 format 参数", args[0]) } - name, nameErr := projectversion.ServerAssetName(version, args[1], args[2]) - if nameErr != nil { - return nameErr - } - fmt.Println(name) - return nil - case "desktop": + platform = args[1] + arch = args[2] + format = args[3] + case "web": if len(args) != 2 { - return fmt.Errorf("desktop 资产命名只需要 platform 参数") + return fmt.Errorf("web 资产命名只需要 format 参数") } - name, nameErr := projectversion.DesktopAssetName(version, args[1]) - if nameErr != nil { - return nameErr - } - fmt.Println(name) - return nil + format = args[1] 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 { diff --git a/versionctl/projectversion/version.go b/versionctl/projectversion/version.go index aa38a78..a41f5d9 100644 --- a/versionctl/projectversion/version.go +++ b/versionctl/projectversion/version.go @@ -263,44 +263,86 @@ func ReadEnvVar(content, key string) (string, bool) { 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 { return "", err } - switch goos { - case "linux", "windows", "darwin": + switch component { + 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: - 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) { - if _, err := Parse(version); err != nil { - return "", err +func serverAssetName(version, platform, arch, format string) (string, error) { + if !validCombination(platform, arch, format, []releaseAssetTarget{ + {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 { - case "linux": - return fmt.Sprintf("Nex_%s_linux_amd64.tar.gz", version), nil - case "windows": - return fmt.Sprintf("Nex_%s_windows_amd64.zip", version), nil - case "macos": - return fmt.Sprintf("Nex_%s_macOS_universal.zip", version), nil - default: - return "", fmt.Errorf("不支持的 desktop 平台 %q", platform) + return fmt.Sprintf("nex-server_%s_%s_%s.%s", version, platform, arch, format), nil +} + +func webAssetName(version, platform, arch, format string) (string, error) { + if platform != "" || arch != "" { + return "", errors.New("web 资产命名不支持平台或架构参数") } + + 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) { diff --git a/versionctl/projectversion/version_test.go b/versionctl/projectversion/version_test.go index a54cae1..895d62f 100644 --- a/versionctl/projectversion/version_test.go +++ b/versionctl/projectversion/version_test.go @@ -83,20 +83,72 @@ func TestVerifyTag(t *testing.T) { } func TestAssetNames(t *testing.T) { - linuxServer, err := ServerAssetName("1.2.3", "linux", "amd64") - require.NoError(t, err) - assert.Equal(t, "nex-server_1.2.3_linux_amd64.tar.gz", linuxServer) + testCases := []struct { + name string + 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") - require.NoError(t, err) - assert.Equal(t, "nex-server_1.2.3_darwin_arm64.tar.gz", macServer) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + 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") - require.NoError(t, err) - assert.Equal(t, "Nex_1.2.3_macOS_universal.zip", macDesktop) + invalidCases := []struct { + name string + 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") - assert.Error(t, err) + for _, tc := range invalidCases { + 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) {