diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6b2dc70..f918adbe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,7 +44,12 @@ jobs: # Windows x64 - os: windows-latest arch: x64 - build_cmd: pnpm run build:win + build_cmd: pnpm run build:win:x64 + signing_identity: '' + # Windows ARM64 + - os: windows-11-arm + arch: arm64 + build_cmd: pnpm run build:win:arm64 signing_identity: '' steps: @@ -72,10 +77,12 @@ jobs: exit 1 fi - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "tag=v$version" >> "$GITHUB_OUTPUT" - echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT" - echo "make_latest=$([ "$prerelease" = "true" ] && echo false || echo true)" >> "$GITHUB_OUTPUT" + { + echo "version=$version" + echo "tag=v$version" + echo "prerelease=$prerelease" + echo "make_latest=$([ "$prerelease" = "true" ] && echo false || echo true)" + } >> "$GITHUB_OUTPUT" - name: Set package version env: @@ -84,21 +91,26 @@ jobs: node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json','utf8')); pkg.version=process.env.RELEASE_VERSION; fs.writeFileSync('package.json', JSON.stringify(pkg,null,2)+'\n');" - name: Install pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v4 with: version: 9 - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.12' - name: Install Node.js uses: actions/setup-node@v4 with: node-version: 22 + architecture: ${{ matrix.arch }} cache: 'pnpm' + - name: Set up MSBuild + if: startsWith(matrix.os, 'windows') + uses: microsoft/setup-msbuild@v2 + - name: Install plugin dependencies run: | cd internal-plugins/setting @@ -107,6 +119,46 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Build and verify Windows native API + if: startsWith(matrix.os, 'windows') + shell: pwsh + env: + NATIVE_API_COMMIT: bb2025c9dddeedfd0738b7632650b2e4e8147466 + TARGET_ARCH: ${{ matrix.arch }} + run: | + if ((node -p process.arch) -ne $env:TARGET_ARCH) { + throw "Node architecture does not match the target: $(node -p process.arch)" + } + + $nativeRoot = Join-Path $env:RUNNER_TEMP 'ztools-native-api' + git clone https://github.com/ZToolsCenter/ZTools-native-api.git $nativeRoot + git -C $nativeRoot checkout --detach $env:NATIVE_API_COMMIT + + Push-Location $nativeRoot + try { + npm ci --ignore-scripts + npm run build + } finally { + Pop-Location + } + + $binaryPath = Join-Path $nativeRoot 'build/Release/ztools_native.node' + if (-not (Test-Path $binaryPath)) { + throw "Native API build output was not found: $binaryPath" + } + + # Read IMAGE_FILE_HEADER.Machine directly so packaging cannot hide a cross-arch binary. + $bytes = [System.IO.File]::ReadAllBytes($binaryPath) + $peOffset = [System.BitConverter]::ToInt32($bytes, 0x3c) + $machine = [System.BitConverter]::ToUInt16($bytes, $peOffset + 4) + $expectedMachine = if ($env:TARGET_ARCH -eq 'arm64') { 0xaa64 } else { 0x8664 } + if ($machine -ne $expectedMachine) { + throw ('Native API PE machine mismatch: expected 0x{0:x4}, got 0x{1:x4}' -f $expectedMachine, $machine) + } + + Copy-Item $binaryPath 'resources/lib/win/ztools_native.node' -Force + Write-Host ('Verified Windows native API PE machine 0x{0:x4}' -f $machine) + - name: Prepare App Store Connect API key if: startsWith(matrix.os, 'macos') shell: bash @@ -196,10 +248,12 @@ jobs: exit 1 fi - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "tag=v$version" >> "$GITHUB_OUTPUT" - echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT" - echo "make_latest=$([ "$prerelease" = "true" ] && echo false || echo true)" >> "$GITHUB_OUTPUT" + { + echo "version=$version" + echo "tag=v$version" + echo "prerelease=$prerelease" + echo "make_latest=$([ "$prerelease" = "true" ] && echo false || echo true)" + } >> "$GITHUB_OUTPUT" - name: Set package version env: @@ -208,7 +262,7 @@ jobs: node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json','utf8')); pkg.version=process.env.RELEASE_VERSION; fs.writeFileSync('package.json', JSON.stringify(pkg,null,2)+'\n');" - name: Install pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v4 with: version: 9 @@ -233,17 +287,24 @@ jobs: name: artifact-macos-14-arm64 path: dist/mac-arm64 - - name: Download Windows artifact + - name: Download Windows x64 artifact uses: actions/download-artifact@v4 with: name: artifact-windows-latest-x64 - path: dist/windows + path: dist/windows-x64 + + - name: Download Windows ARM64 artifact + uses: actions/download-artifact@v4 + with: + name: artifact-windows-11-arm-arm64 + path: dist/windows-arm64 - # 合并 macOS 双架构元数据,并规范化 Windows 与 macOS 的更新清单。 + # 合并 Windows 与 macOS 双架构元数据,生成各平台的统一更新清单。 - name: Generate update info env: RELEASE_VERSION: ${{ steps.release_meta.outputs.version }} - WINDOWS_UPDATE_METADATA: dist/windows/latest.yml + WINDOWS_X64_UPDATE_METADATA: dist/windows-x64/latest.yml + WINDOWS_ARM64_UPDATE_METADATA: dist/windows-arm64/latest.yml MAC_X64_UPDATE_METADATA: dist/mac-x64/latest-mac.yml MAC_ARM64_UPDATE_METADATA: dist/mac-arm64/latest-mac.yml UPDATE_METADATA_OUTPUT_DIR: dist/release @@ -261,9 +322,12 @@ jobs: make_latest: ${{ steps.release_meta.outputs.make_latest }} body_path: changelog.md files: | - dist/windows/*.exe - dist/windows/*.zip - dist/windows/*.blockmap + dist/windows-x64/*.exe + dist/windows-x64/*.zip + dist/windows-x64/*.blockmap + dist/windows-arm64/*.exe + dist/windows-arm64/*.zip + dist/windows-arm64/*.blockmap dist/mac-x64/*.dmg dist/mac-x64/*.zip dist/mac-x64/*.blockmap diff --git a/README.md b/README.md index d833ddf4..98fb5393 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ _uTools 的开源实现 | 支持 macOS 和 Windows_ 从 [Releases](https://github.com/ZToolsCenter/ZTools/releases) 页面下载最新版本: - **macOS**: `ztools-x.x.x.dmg` 或 `ZTools-x.x.x-arm64-mac.zip` -- **Windows**: `ztools-x.x.x-setup.exe` 或 `ztools-x.x.x-win.zip` +- **Windows x64 / ARM64**: `ZTools-x.x.x-win-{arch}-setup.exe` 或 `ZTools-x.x.x-win-{arch}.zip` #### 方式 2:从源码构建 @@ -112,7 +112,8 @@ pnpm dev # 构建 pnpm build:mac # macOS -pnpm build:win # Windows +pnpm build:win:x64 # Windows x64(build:win 仍为 x64 别名) +pnpm build:win:arm64 # Windows ARM64 pnpm build:linux # Linux (当前主机架构) pnpm build:linux:x64 # Linux (按 amd64/x64 架构打包) pnpm build:linux:arm64 # Linux (按 arm64 架构打包) @@ -217,7 +218,8 @@ pnpm lint # ESLint 检查 # 构建 pnpm build # 仅编译源码 pnpm build:mac # 打包 macOS 应用 -pnpm build:win # 打包 Windows 应用 +pnpm build:win:x64 # 打包 Windows x64 应用(build:win 仍为 x64 别名) +pnpm build:win:arm64 # 打包 Windows ARM64 应用 pnpm build:linux # 打包 Linux 应用 (当前主机架构) pnpm build:linux:x64 # 打包 Linux 应用 (amd64/x64) pnpm build:linux:arm64 # 打包 Linux 应用 (arm64) diff --git a/README_EN.md b/README_EN.md index da4fa3ed..eb8e8728 100644 --- a/README_EN.md +++ b/README_EN.md @@ -95,7 +95,7 @@ English | [简体中文](./README.md) Download the latest version from [Releases](https://github.com/ZToolsCenter/ZTools/releases): - **macOS**: `ztools-x.x.x.dmg` or `ZTools-x.x.x-arm64-mac.zip` -- **Windows**: `ztools-x.x.x-setup.exe` or `ztools-x.x.x-win.zip` +- **Windows x64 / ARM64**: `ZTools-x.x.x-win-{arch}-setup.exe` or `ZTools-x.x.x-win-{arch}.zip` #### Method 2: Build from Source @@ -112,7 +112,8 @@ pnpm dev # Build pnpm build:mac # macOS -pnpm build:win # Windows +pnpm build:win:x64 # Windows x64 (build:win remains an x64 alias) +pnpm build:win:arm64 # Windows ARM64 pnpm build:linux # Linux (Default Arch) pnpm build:linux:x64 # Linux (amd64/x64) pnpm build:linux:arm64 # Linux (arm64) @@ -205,7 +206,8 @@ pnpm lint # ESLint check # Build pnpm build # Compile source code only pnpm build:mac # Package macOS app -pnpm build:win # Package Windows app +pnpm build:win:x64 # Package Windows x64 app (build:win remains an x64 alias) +pnpm build:win:arm64 # Package Windows ARM64 app pnpm build:linux # Package Linux app (Default Arch) pnpm build:linux:x64 # Package Linux app (amd64/x64) pnpm build:linux:arm64 # Package Linux app (arm64) diff --git a/electron-builder.yml b/electron-builder.yml index 4f0e46fd..331b444e 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -44,12 +44,8 @@ win: executableName: ZTools artifactName: ${name}-${version}-${os}-${arch}.${ext} target: - - target: nsis - arch: - - x64 - - target: zip - arch: - - x64 + - nsis + - zip nsis: artifactName: ${name}-${version}-${os}-${arch}-setup.${ext} include: build/installer.nsh diff --git a/package.json b/package.json index 7ab6877e..21596804 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,9 @@ "build": "pnpm typecheck && pnpm build:setting && electron-vite build", "build:setting": "cd internal-plugins/setting && pnpm build", "build:unpack": "pnpm build && electron-builder --dir", - "build:win": "cross-env ZTOOLS_TARGET_PLATFORM=win32 pnpm build && electron-builder --win --publish never", + "build:win": "pnpm build:win:x64", + "build:win:x64": "cross-env ZTOOLS_TARGET_PLATFORM=win32 pnpm build && electron-builder --win --x64 --publish never", + "build:win:arm64": "cross-env ZTOOLS_TARGET_PLATFORM=win32 pnpm build && electron-builder --win --arm64 --publish never", "build:mac": "cross-env ZTOOLS_TARGET_PLATFORM=darwin pnpm build && electron-builder --mac --publish never", "build:mac:x64": "cross-env ZTOOLS_TARGET_PLATFORM=darwin pnpm build && electron-builder --mac --x64 --publish never", "build:mac:arm64": "cross-env ZTOOLS_TARGET_PLATFORM=darwin pnpm build && electron-builder --mac --arm64 --publish never", @@ -67,7 +69,7 @@ "simple-plist": "^1.3.1", "tar": "^7.5.9", "turndown": "^7.2.2", - "uiohook-napi": "^1.5.4", + "uiohook-napi": "^1.5.5", "uuid": "^13.0.0", "vuedraggable": "^4.1.0", "winreg": "^1.2.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 447a76ea..af5180b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,8 +66,8 @@ importers: specifier: ^7.2.2 version: 7.2.2 uiohook-napi: - specifier: ^1.5.4 - version: 1.5.4 + specifier: ^1.5.5 + version: 1.5.5 uuid: specifier: ^13.0.0 version: 13.0.0 @@ -3001,8 +3001,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - uiohook-napi@1.5.4: - resolution: {integrity: sha512-7vPVDNwgb6MwTgviA/dnF2MrW0X5xm76fAqaOAC3cEKkswqAZOPw1USu14Sr6383s5qhXegcJaR63CpJOPCNAg==} + uiohook-napi@1.5.5: + resolution: {integrity: sha512-oSlTdnECw2GBfsJPTbBQBeE4v/EXP0EZmX6BJq5nzH/JgFaBE8JpFwEA/kLhiEP7HxQw28FViWiYgdIZzWuuJQ==} engines: {node: '>= 16'} undici-types@6.21.0: @@ -6243,7 +6243,7 @@ snapshots: typescript@5.9.3: {} - uiohook-napi@1.5.4: + uiohook-napi@1.5.5: dependencies: node-gyp-build: 4.8.4 diff --git a/scripts/sync-release-server.mjs b/scripts/sync-release-server.mjs index 2b03ab95..90014a07 100644 --- a/scripts/sync-release-server.mjs +++ b/scripts/sync-release-server.mjs @@ -1,6 +1,6 @@ import { readFileSync } from 'fs' import path from 'path' -import { readUpdateMetadata } from './update-metadata.mjs' +import { readUpdateMetadata, selectWindowsInstaller } from './update-metadata.mjs' /* eslint-disable @typescript-eslint/explicit-function-return-type */ @@ -107,10 +107,8 @@ async function main() { process.env.MAC_UPDATE_METADATA || 'dist/release/latest-mac.yml' ) const release = await fetchGitHubRelease(repository, tag, githubToken) - const windowsFile = (windowsMetadata.files || []).find((file) => - String(file?.url || '').endsWith('-setup.exe') - ) - if (!windowsFile) throw new Error('Windows 更新元数据缺少 NSIS 安装包') + const windowsX64Installer = selectWindowsInstaller(windowsMetadata, 'x64') + const windowsArm64Installer = selectWindowsInstaller(windowsMetadata, 'arm64') const payload = { providerReleaseId: release.id, @@ -121,7 +119,8 @@ async function main() { publishedAt: release.published_at, prerelease: Boolean(release.prerelease), artifacts: [ - toArtifact(release, windowsFile, 'windows-x64-installer'), + toArtifact(release, windowsX64Installer, 'windows-x64-installer'), + toArtifact(release, windowsArm64Installer, 'windows-arm64-installer'), toArtifact(release, selectMetadataFile(macMetadata, '-x64.zip'), 'macos-x64'), toArtifact(release, selectMetadataFile(macMetadata, '-arm64.zip'), 'macos-arm64') ] diff --git a/scripts/update-metadata.mjs b/scripts/update-metadata.mjs index c6f17d52..1ed8a34f 100644 --- a/scripts/update-metadata.mjs +++ b/scripts/update-metadata.mjs @@ -64,6 +64,51 @@ export function mergeMacUpdateMetadata(x64Metadata, arm64Metadata, releaseNotes) } } +/** + * 从单架构 Windows 元数据中选择标准 NSIS 安装包。 + * @param {Record} metadata 单架构 Windows 更新元数据。 + * @param {'x64' | 'arm64'} arch 目标架构。 + * @returns {Record} 对应架构的 NSIS 安装包文件信息。 + * @throws {Error} 找不到唯一且带 SHA-512 的架构安装包时抛出错误。 + */ +export function selectWindowsInstaller(metadata, arch) { + const files = Array.isArray(metadata.files) ? metadata.files : [] + const suffix = `-${arch}-setup.exe` + const matches = files.filter((file) => String(file?.url || '').endsWith(suffix)) + + if (matches.length !== 1) { + throw new Error(`Windows ${arch} 元数据必须包含且仅包含一个 ${suffix} 安装包`) + } + if (!matches[0].sha512) throw new Error(`Windows ${arch} 安装包缺少 SHA-512`) + return { ...matches[0] } +} + +/** + * 合并两个架构的 Windows 元数据,供 electron-updater 按运行架构选择安装包。 + * @param {Record} x64Metadata Windows x64 更新元数据。 + * @param {Record} arm64Metadata Windows ARM64 更新元数据。 + * @param {string} releaseNotes 发布说明。 + * @returns {Record} 合并后的 latest.yml 元数据。 + * @throws {Error} 版本不一致或任一架构安装包无效时抛出错误。 + */ +export function mergeWindowsUpdateMetadata(x64Metadata, arm64Metadata, releaseNotes) { + if (!x64Metadata.version || x64Metadata.version !== arm64Metadata.version) { + throw new Error('Windows x64 与 arm64 更新元数据版本不一致') + } + + // 保持 x64 在前并保留顶层兼容字段,旧客户端仍会下载 x64 安装包。 + const x64Installer = selectWindowsInstaller(x64Metadata, 'x64') + const arm64Installer = selectWindowsInstaller(arm64Metadata, 'arm64') + + return { + ...x64Metadata, + files: [x64Installer, arm64Installer], + path: x64Installer.url, + sha512: x64Installer.sha512, + releaseNotes + } +} + /** * 为平台更新元数据补充统一发布说明。 * @param {Record} metadata 原始更新元数据。 diff --git a/scripts/updater.mjs b/scripts/updater.mjs index 6b2aa12d..9fab6f29 100644 --- a/scripts/updater.mjs +++ b/scripts/updater.mjs @@ -8,8 +8,10 @@ import { } from './version-utils.mjs' import { mergeMacUpdateMetadata, + mergeWindowsUpdateMetadata, readUpdateMetadata, selectMacUpdateZip, + selectWindowsInstaller, validateReferencedAsset, withReleaseNotes, writeUpdateMetadata @@ -32,21 +34,46 @@ console.log(`下载地址: ${downloadUrl}`) const downloadLinks = generateDownloadLinksMarkdown(downloadUrl, version) const updatedChangelog = changelog + downloadLinks -// CI 使用独立目录避免两个 macOS job 的同名 latest-mac.yml 相互覆盖。 -const windowsMetadataPath = process.env.WINDOWS_UPDATE_METADATA || 'dist/latest.yml' +// CI 使用独立目录避免不同架构 job 的同名更新元数据相互覆盖。 +const windowsX64MetadataPath = process.env.WINDOWS_X64_UPDATE_METADATA +const windowsArm64MetadataPath = process.env.WINDOWS_ARM64_UPDATE_METADATA +const fallbackWindowsMetadataPath = process.env.WINDOWS_UPDATE_METADATA || 'dist/latest.yml' const macX64MetadataPath = process.env.MAC_X64_UPDATE_METADATA const macArm64MetadataPath = process.env.MAC_ARM64_UPDATE_METADATA const metadataOutputDir = - process.env.UPDATE_METADATA_OUTPUT_DIR || path.dirname(windowsMetadataPath) + process.env.UPDATE_METADATA_OUTPUT_DIR || + path.dirname(windowsX64MetadataPath || fallbackWindowsMetadataPath) const requireUpdateMetadata = process.env.REQUIRE_UPDATE_METADATA === 'true' -if (existsSync(windowsMetadataPath)) { - const windowsMetadata = withReleaseNotes(readUpdateMetadata(windowsMetadataPath), changelog) +if (windowsX64MetadataPath || windowsArm64MetadataPath) { + if (!windowsX64MetadataPath || !windowsArm64MetadataPath) { + throw new Error('必须同时提供 WINDOWS_X64_UPDATE_METADATA 和 WINDOWS_ARM64_UPDATE_METADATA') + } + + const x64Metadata = readUpdateMetadata(windowsX64MetadataPath) + const arm64Metadata = readUpdateMetadata(windowsArm64MetadataPath) + const x64Installer = selectWindowsInstaller(x64Metadata, 'x64') + const arm64Installer = selectWindowsInstaller(arm64Metadata, 'arm64') + + // 发布前确认两个架构的安装包都实际存在于各自构建产物目录。 + validateReferencedAsset(windowsX64MetadataPath, x64Installer) + validateReferencedAsset(windowsArm64MetadataPath, arm64Installer) + + const windowsMetadata = mergeWindowsUpdateMetadata(x64Metadata, arm64Metadata, changelog) + const windowsOutputPath = path.join(metadataOutputDir, 'latest.yml') + writeUpdateMetadata(windowsOutputPath, windowsMetadata) + console.log(`✅ 已生成 ${windowsOutputPath}`) +} else if (existsSync(fallbackWindowsMetadataPath)) { + // 保留单架构调用方式,供本地或旧版发布脚本继续规范化已有 latest.yml。 + const windowsMetadata = withReleaseNotes( + readUpdateMetadata(fallbackWindowsMetadataPath), + changelog + ) const windowsOutputPath = path.join(metadataOutputDir, 'latest.yml') writeUpdateMetadata(windowsOutputPath, windowsMetadata) console.log(`✅ 已生成 ${windowsOutputPath}`) } else { - const message = `未找到 Windows 更新元数据: ${windowsMetadataPath}` + const message = `未找到 Windows 更新元数据: ${fallbackWindowsMetadataPath}` if (requireUpdateMetadata) throw new Error(message) console.warn(`⚠️ ${message}`) } diff --git a/scripts/version-utils.mjs b/scripts/version-utils.mjs index 44a13ed4..86ea6a30 100644 --- a/scripts/version-utils.mjs +++ b/scripts/version-utils.mjs @@ -86,7 +86,12 @@ export function getDownloadUrl(isDev, version) { } } -// 生成下载链接的 Markdown +/** + * 生成当前版本各平台、各架构发布产物的下载链接。 + * @param {string} downloadUrl GitHub Release 下载目录。 + * @param {string} version 不带 v 前缀的发布版本号。 + * @returns {string} 可追加到 changelog 的 Markdown 下载链接。 + */ export function generateDownloadLinksMarkdown(downloadUrl, version) { let links = '\n\n---\n\n### 下载地址:\n\n' @@ -94,6 +99,10 @@ export function generateDownloadLinksMarkdown(downloadUrl, version) { links += `- 安装版:[ZTools-${version}-win-x64-setup.exe](${downloadUrl}/ZTools-${version}-win-x64-setup.exe)\n` links += `- 便携版:[ZTools-${version}-win-x64.zip](${downloadUrl}/ZTools-${version}-win-x64.zip)\n\n` + links += '#### Windows 10/11 (ARM64):\n\n' + links += `- 安装版:[ZTools-${version}-win-arm64-setup.exe](${downloadUrl}/ZTools-${version}-win-arm64-setup.exe)\n` + links += `- 便携版:[ZTools-${version}-win-arm64.zip](${downloadUrl}/ZTools-${version}-win-arm64.zip)\n\n` + links += '#### macOS 11+ (Apple Silicon):\n\n' links += `- DMG:[ZTools-${version}-mac-arm64.dmg](${downloadUrl}/ZTools-${version}-mac-arm64.dmg)\n` links += `- ZIP:[ZTools-${version}-mac-arm64.zip](${downloadUrl}/ZTools-${version}-mac-arm64.zip)\n\n` diff --git a/src/main/api/serverUpdateCatalog.ts b/src/main/api/serverUpdateCatalog.ts index 5e749071..dc7ac769 100644 --- a/src/main/api/serverUpdateCatalog.ts +++ b/src/main/api/serverUpdateCatalog.ts @@ -25,13 +25,20 @@ interface ServerDownloadsResponse { /** * 返回服务端更新目录使用的系统类型。 + * @param platform 待匹配的平台,默认使用当前 Electron 运行平台。 + * @param arch 待匹配的架构,默认使用当前 Electron 运行架构。 * @returns 当前 Electron 运行平台与架构对应的系统类型。 */ -export function getUpdateSystemType(): string { - if (process.platform === 'win32') return 'windows-x64-installer' - if (process.platform === 'darwin') return process.arch === 'arm64' ? 'macos-arm64' : 'macos-x64' - if (process.platform === 'linux') return process.arch === 'arm64' ? 'linux-arm64' : 'linux-x64' - return `${process.platform}-${process.arch}` +export function getUpdateSystemType( + platform: NodeJS.Platform = process.platform, + arch: string = process.arch +): string { + if (platform === 'win32') { + return arch === 'arm64' ? 'windows-arm64-installer' : 'windows-x64-installer' + } + if (platform === 'darwin') return arch === 'arm64' ? 'macos-arm64' : 'macos-x64' + if (platform === 'linux') return arch === 'arm64' ? 'linux-arm64' : 'linux-x64' + return `${platform}-${arch}` } /** diff --git a/src/main/core/ffmpeg.ts b/src/main/core/ffmpeg.ts index b37bc5f8..b01f9f42 100644 --- a/src/main/core/ffmpeg.ts +++ b/src/main/core/ffmpeg.ts @@ -19,9 +19,9 @@ class FFmpegManager { private readonly DOWNLOAD_URLS = { 'win32-x64': - 'https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-07-15-14-01/ffmpeg-n8.1.2-22-g94138f6973-win64-gpl-8.1.zip', + 'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip', 'win32-arm64': - 'https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-07-15-14-01/ffmpeg-n8.1.2-22-g94138f6973-winarm64-gpl-8.1.zip', + 'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-winarm64-gpl.zip', 'darwin-arm64': 'https://github.com/eko5624/mpv-mac/releases/download/2026-03-09/ffmpeg-arm64-248b481c33.zip', 'darwin-x64': @@ -29,13 +29,19 @@ class FFmpegManager { } /** - * FFmpeg 存储路径: {userData}/extends/ffmpeg{.exe} + * 返回当前平台与架构隔离后的 FFmpeg 存储路径。 + * @returns FFmpeg 可执行文件的绝对路径。 */ private resolveFFmpegPath(): string { - const ext = process.platform === 'win32' ? '.exe' : '' - return path.join(getExtendsPath(), `ffmpeg${ext}`) + const fileName = process.platform === 'win32' ? `ffmpeg-${process.arch}.exe` : 'ffmpeg' + return path.join(getExtendsPath(), fileName) } + /** + * 返回当前平台与架构对应的 FFmpeg 下载地址。 + * @returns FFmpeg 压缩包下载地址。 + * @throws 当前平台与架构没有可用构建时抛出错误。 + */ private getDownloadUrl(): string { const key = `${process.platform}-${process.arch}` const url = this.DOWNLOAD_URLS[key as keyof typeof this.DOWNLOAD_URLS] diff --git a/tests/main/serverUpdateCatalog.test.ts b/tests/main/serverUpdateCatalog.test.ts index dcd54101..39928684 100644 --- a/tests/main/serverUpdateCatalog.test.ts +++ b/tests/main/serverUpdateCatalog.test.ts @@ -1,6 +1,25 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' + +vi.mock('electron', () => ({ app: { getVersion: () => '3.1.0' } })) +vi.mock('../../src/main/api/renderer/pluginMarketConfig', () => ({ + DEFAULT_SYNC_SERVER_URL: 'wss://z-tools.top', + syncServerUrlToHttp: () => 'https://z-tools.top' +})) +vi.mock('../../src/main/utils/httpRequest', () => ({ httpRequest: vi.fn() })) +vi.mock('../../src/main/api/shared/database', () => ({ + default: { dbGet: vi.fn() } +})) + +import { getUpdateSystemType } from '../../src/main/api/serverUpdateCatalog' import { resolveUpdateChannel } from '../../src/shared/updateChannel' +describe('getUpdateSystemType', () => { + it('selects the Windows installer matching the current architecture', () => { + expect(getUpdateSystemType('win32', 'x64')).toBe('windows-x64-installer') + expect(getUpdateSystemType('win32', 'arm64')).toBe('windows-arm64-installer') + }) +}) + describe('resolveUpdateChannel', () => { it('keeps regular releases on the stable channel by default', () => { expect(resolveUpdateChannel('3.1.0')).toBe('stable') diff --git a/tests/scripts/updateMetadata.test.ts b/tests/scripts/updateMetadata.test.ts index a39605a6..ceaa56ae 100644 --- a/tests/scripts/updateMetadata.test.ts +++ b/tests/scripts/updateMetadata.test.ts @@ -1,9 +1,14 @@ import { describe, expect, it } from 'vitest' import { mergeMacUpdateMetadata, + mergeWindowsUpdateMetadata, selectMacUpdateZip, - withReleaseNotes + selectWindowsInstaller, + validateReferencedAsset } from '../../scripts/update-metadata.mjs' +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import path from 'node:path' const x64Metadata = { version: '3.0.0-beta.9', @@ -63,22 +68,109 @@ describe('macOS update metadata', () => { }) describe('Windows update metadata', () => { - it('adds release notes without rewriting electron-builder metadata', () => { - const normalized = withReleaseNotes( + const windowsX64Metadata = { + version: '3.0.0-beta.9', + files: [ { - version: '3.0.0-beta.9', - files: [{ url: 'ZTools-3.0.0-beta.9-win-x64-setup.exe', sha512: 'setup-sha512' }], - path: 'ZTools-3.0.0-beta.9-win-x64-setup.exe', - sha512: 'setup-sha512', - releaseDate: '2026-07-20T00:00:00.000Z' + url: 'ZTools-3.0.0-beta.9-win-x64-setup.exe', + sha512: 'setup-x64', + size: 120 }, + { url: 'ZTools-3.0.0-beta.9-win-x64.zip', sha512: 'zip-x64' } + ], + path: 'ZTools-3.0.0-beta.9-win-x64-setup.exe', + sha512: 'setup-x64', + releaseDate: '2026-07-20T00:00:00.000Z' + } + + const windowsArm64Metadata = { + version: '3.0.0-beta.9', + files: [ + { + url: 'ZTools-3.0.0-beta.9-win-arm64-setup.exe', + sha512: 'setup-arm64', + size: 110 + } + ], + releaseDate: '2026-07-20T00:00:01.000Z' + } + + it('selects exactly one checksummed installer for the requested architecture', () => { + expect(selectWindowsInstaller(windowsArm64Metadata, 'arm64')).toMatchObject({ + url: 'ZTools-3.0.0-beta.9-win-arm64-setup.exe', + sha512: 'setup-arm64' + }) + }) + + it('merges x64 then arm64 installers and keeps legacy fields on x64', () => { + const merged = mergeWindowsUpdateMetadata( + windowsX64Metadata, + windowsArm64Metadata, 'release notes' ) - expect(normalized.files[0].sha512).toBe('setup-sha512') - expect(normalized.path).toBe('ZTools-3.0.0-beta.9-win-x64-setup.exe') - expect(normalized.sha512).toBe('setup-sha512') - expect(normalized.releaseNotes).toBe('release notes') - expect(normalized).not.toHaveProperty('changelog') + expect(merged.files).toEqual([ + { + url: 'ZTools-3.0.0-beta.9-win-x64-setup.exe', + sha512: 'setup-x64', + size: 120 + }, + { + url: 'ZTools-3.0.0-beta.9-win-arm64-setup.exe', + sha512: 'setup-arm64', + size: 110 + } + ]) + expect(merged.path).toBe('ZTools-3.0.0-beta.9-win-x64-setup.exe') + expect(merged.sha512).toBe('setup-x64') + expect(merged.releaseNotes).toBe('release notes') + }) + + it('rejects Windows metadata from different versions', () => { + expect(() => + mergeWindowsUpdateMetadata( + windowsX64Metadata, + { ...windowsArm64Metadata, version: '3.0.1' }, + '' + ) + ).toThrow('版本不一致') + }) + + it('rejects missing, duplicate, or unsigned Windows installers', () => { + expect(() => selectWindowsInstaller({ ...windowsArm64Metadata, files: [] }, 'arm64')).toThrow( + '必须包含且仅包含一个' + ) + expect(() => + selectWindowsInstaller( + { + ...windowsArm64Metadata, + files: [...windowsArm64Metadata.files, ...windowsArm64Metadata.files] + }, + 'arm64' + ) + ).toThrow('必须包含且仅包含一个') + expect(() => + selectWindowsInstaller( + { + ...windowsArm64Metadata, + files: [{ url: 'ZTools-3.0.0-beta.9-win-arm64-setup.exe' }] + }, + 'arm64' + ) + ).toThrow('缺少 SHA-512') + }) + + it('rejects metadata that references an installer missing from disk', () => { + const metadataDir = mkdtempSync(path.join(tmpdir(), 'ztools-update-metadata-')) + const metadataPath = path.join(metadataDir, 'latest.yml') + writeFileSync(metadataPath, 'version: 3.0.0-beta.9\n') + + try { + expect(() => validateReferencedAsset(metadataPath, windowsArm64Metadata.files[0])).toThrow( + '引用的文件不存在' + ) + } finally { + rmSync(metadataDir, { recursive: true, force: true }) + } }) })