diff --git a/.github/workflows/build-desktop-tauri.yml b/.github/workflows/build-desktop-tauri.yml index 68f5079..9b05a58 100644 --- a/.github/workflows/build-desktop-tauri.yml +++ b/.github/workflows/build-desktop-tauri.yml @@ -464,9 +464,11 @@ jobs: ASTRBOT_SOURCE_GIT_REF: ${{ needs.resolve_build_context.outputs.source_git_ref }} ASTRBOT_DESKTOP_VERSION: ${{ steps.desktop_version.outputs.prefixed }} ASTRBOT_DESKTOP_UPDATER_PUBLIC_KEY: ${{ env.ASTRBOT_DESKTOP_UPDATER_PUBLIC_KEY }} + ASTRBOT_DESKTOP_TARGET_ARCH: ${{ matrix.arch }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} ASTRBOT_DESKTOP_CRYPTOGRAPHY_FALLBACK_VERSIONS: ${{ vars.ASTRBOT_DESKTOP_CRYPTOGRAPHY_FALLBACK_VERSIONS || '' }} + ASTRBOT_DESKTOP_WINDOWS_ARM_BACKEND_ARCH: ${{ vars.ASTRBOT_DESKTOP_WINDOWS_ARM_BACKEND_ARCH || '' }} GITHUB_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }} shell: bash diff --git a/docs/environment-variables.md b/docs/environment-variables.md index a064279..97738dd 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -1,6 +1,6 @@ # AstrBot Desktop 环境变量清单 -更新时间:2026-03-07 +更新时间:2026-03-13 主要来源:桌面运行时 Rust 模块(如 `src-tauri/src/backend/config.rs`、`src-tauri/src/update_channel.rs`、`src-tauri/src/bridge/updater_messages.rs`、`src-tauri/src/runtime_paths.rs`、`src-tauri/src/launch_plan.rs`)、资源准备脚本(`scripts/prepare-resources*.mjs`)和发布工作流(`.github/workflows/build-desktop-tauri.yml`)。以下按主要解析/写入阶段分组。 ## 1. 桌面运行时直接读取(`src-tauri`) @@ -45,6 +45,8 @@ | `ASTRBOT_PBS_VERSION` | python-build-standalone Python 版本 | 默认 `3.12.12` | | `ASTRBOT_DESKTOP_BACKEND_RUNTIME` | 外部后端 runtime 根目录 | 存在时优先使用 | | `ASTRBOT_DESKTOP_CPYTHON_HOME` | 外部 CPython 根目录 | 作为 bundled runtime 回退 | +| `ASTRBOT_DESKTOP_TARGET_ARCH` | 显式指定资源准备阶段要打包的桌面目标架构 | 默认空;未设置时回退到当前 Node 进程架构,CI 建议显式传 `amd64` 或 `arm64` | +| `ASTRBOT_DESKTOP_WINDOWS_ARM_BACKEND_ARCH` | Windows ARM64 构建时覆盖 bundled backend Python 架构 | 默认空;在 Windows ARM64 上默认为 `amd64`,可显式设为 `amd64`/`x64` 或 `arm64`/`aarch64` | ## 3. 桌面进程写入给后端子进程 @@ -57,6 +59,8 @@ | 变量 | 用途 | 默认值/行为 | | --- | --- | --- | | `ASTRBOT_DESKTOP_UPDATER_PUBLIC_KEY` | updater 公钥透传到构建步骤 | 默认空;当前由 `.github/workflows/build-desktop-tauri.yml` 传递,Rust 运行时不直接解析 | +| `ASTRBOT_DESKTOP_TARGET_ARCH` | 透传矩阵目标架构给资源准备脚本 | 默认空;Windows workflow 当前会传 `matrix.arch`,避免在 WOA 上误用仿真层 Node 的 `process.arch` | +| `ASTRBOT_DESKTOP_WINDOWS_ARM_BACKEND_ARCH` | 透传 Windows ARM64 backend runtime 架构覆盖配置到构建步骤 | 默认空;具体取值与默认行为见第 2 节 | ## 5. 维护约定 diff --git a/scripts/backend/build-backend.mjs b/scripts/backend/build-backend.mjs index bbfce3b..c42fe5a 100644 --- a/scripts/backend/build-backend.mjs +++ b/scripts/backend/build-backend.mjs @@ -16,6 +16,7 @@ import { patchLinuxRuntimeRpaths, pruneLinuxTkinterRuntime, } from './runtime-linux-compat-utils.mjs'; +import { isWindowsArm64BundledRuntime } from './runtime-arch-utils.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.resolve(__dirname, '..', '..'); @@ -474,7 +475,7 @@ const installRuntimeDependencies = (runtimePython) => { }); }; - const isWindowsArm64 = process.platform === 'win32' && process.arch === 'arm64'; + const isWindowsArm64 = isWindowsArm64BundledRuntime(); if (isWindowsArm64) { // Prefer prebuilt wheels and avoid compiling cryptography from source on Windows ARM64. // Fallback versions are configured by env var, for example: diff --git a/scripts/backend/runtime-arch-utils.mjs b/scripts/backend/runtime-arch-utils.mjs new file mode 100644 index 0000000..b52ddc9 --- /dev/null +++ b/scripts/backend/runtime-arch-utils.mjs @@ -0,0 +1,64 @@ +export const DESKTOP_TARGET_ARCH_ENV = 'ASTRBOT_DESKTOP_TARGET_ARCH'; +export const WINDOWS_ARM_BACKEND_ARCH_ENV = 'ASTRBOT_DESKTOP_WINDOWS_ARM_BACKEND_ARCH'; +export const BUNDLED_RUNTIME_ARCH_ENV = 'ASTRBOT_DESKTOP_BUNDLED_RUNTIME_ARCH'; + +const PROCESS_ARCH_MAP = { + x64: 'amd64', + arm64: 'arm64', +}; + +export const normalizeDesktopArch = (rawArch, sourceName) => { + const raw = String(rawArch ?? '').trim().toLowerCase(); + if (raw === 'amd64' || raw === 'x64') { + return 'amd64'; + } + if (raw === 'arm64' || raw === 'aarch64') { + return 'arm64'; + } + throw new Error( + `Invalid ${sourceName} value "${raw}". Expected one of: amd64, x64, arm64, aarch64.`, + ); +}; + +export const resolveDesktopTargetArch = ({ arch = process.arch, env = process.env } = {}) => { + const overrideRaw = env[DESKTOP_TARGET_ARCH_ENV]; + if (overrideRaw !== undefined && String(overrideRaw).trim()) { + return normalizeDesktopArch(overrideRaw, DESKTOP_TARGET_ARCH_ENV); + } + + const mappedArch = PROCESS_ARCH_MAP[arch]; + if (mappedArch) { + return mappedArch; + } + + throw new Error(`Unsupported process.arch for desktop target resolution: ${arch}`); +}; + +export const resolveBundledRuntimeArch = ({ + platform = process.platform, + arch = process.arch, + env = process.env, +} = {}) => { + const explicitBundledRuntimeArch = env[BUNDLED_RUNTIME_ARCH_ENV]; + if (explicitBundledRuntimeArch !== undefined && String(explicitBundledRuntimeArch).trim()) { + return normalizeDesktopArch(explicitBundledRuntimeArch, BUNDLED_RUNTIME_ARCH_ENV); + } + + const targetArch = resolveDesktopTargetArch({ arch, env }); + if (platform !== 'win32' || targetArch !== 'arm64') { + return targetArch; + } + + const windowsArmBackendArch = env[WINDOWS_ARM_BACKEND_ARCH_ENV]; + if (windowsArmBackendArch === undefined || !String(windowsArmBackendArch).trim()) { + return 'amd64'; + } + + return normalizeDesktopArch(windowsArmBackendArch, WINDOWS_ARM_BACKEND_ARCH_ENV); +}; + +export const isWindowsArm64BundledRuntime = ({ + platform = process.platform, + arch = process.arch, + env = process.env, +} = {}) => platform === 'win32' && resolveBundledRuntimeArch({ platform, arch, env }) === 'arm64'; diff --git a/scripts/backend/runtime-arch-utils.test.mjs b/scripts/backend/runtime-arch-utils.test.mjs new file mode 100644 index 0000000..86eaa36 --- /dev/null +++ b/scripts/backend/runtime-arch-utils.test.mjs @@ -0,0 +1,57 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; + +import { + isWindowsArm64BundledRuntime, + resolveBundledRuntimeArch, +} from './runtime-arch-utils.mjs'; + +test('resolveBundledRuntimeArch defaults Windows ARM64 backend runtime to x64', () => { + assert.equal( + resolveBundledRuntimeArch({ platform: 'win32', arch: 'arm64', env: {} }), + 'amd64', + ); +}); + +test('resolveBundledRuntimeArch honors explicit target arch on emulated x64 Node', () => { + assert.equal( + resolveBundledRuntimeArch({ + platform: 'win32', + arch: 'x64', + env: { ASTRBOT_DESKTOP_TARGET_ARCH: 'arm64' }, + }), + 'amd64', + ); + + assert.equal( + resolveBundledRuntimeArch({ + platform: 'win32', + arch: 'x64', + env: { + ASTRBOT_DESKTOP_TARGET_ARCH: 'arm64', + ASTRBOT_DESKTOP_WINDOWS_ARM_BACKEND_ARCH: 'arm64', + }, + }), + 'arm64', + ); +}); + +test('isWindowsArm64BundledRuntime uses explicit bundled runtime arch handoff', () => { + assert.equal( + isWindowsArm64BundledRuntime({ + platform: 'win32', + arch: 'x64', + env: { ASTRBOT_DESKTOP_BUNDLED_RUNTIME_ARCH: 'arm64' }, + }), + true, + ); + + assert.equal( + isWindowsArm64BundledRuntime({ + platform: 'win32', + arch: 'x64', + env: { ASTRBOT_DESKTOP_BUNDLED_RUNTIME_ARCH: 'amd64' }, + }), + false, + ); +}); diff --git a/scripts/prepare-resources/backend-runtime.mjs b/scripts/prepare-resources/backend-runtime.mjs index dd1a163..6151337 100644 --- a/scripts/prepare-resources/backend-runtime.mjs +++ b/scripts/prepare-resources/backend-runtime.mjs @@ -1,23 +1,27 @@ import { existsSync, mkdirSync } from 'node:fs'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; +import { + BUNDLED_RUNTIME_ARCH_ENV, + resolveBundledRuntimeArch, +} from '../backend/runtime-arch-utils.mjs'; -const resolvePbsTarget = () => { +export const resolvePbsTarget = ({ + platform = process.platform, + arch = process.arch, + env = process.env, +} = {}) => { const platformMap = { linux: 'linux', darwin: 'mac', win32: 'windows', }; - const archMap = { - x64: 'amd64', - arm64: 'arm64', - }; - const normalizedPlatform = platformMap[process.platform]; - const normalizedArch = archMap[process.arch]; + const normalizedPlatform = platformMap[platform]; + const normalizedArch = resolveBundledRuntimeArch({ platform, arch, env }); if (!normalizedPlatform || !normalizedArch) { throw new Error( - `Unsupported platform/arch for python-build-standalone: ${process.platform}/${process.arch}`, + `Unsupported platform/arch for python-build-standalone: ${platform}/${arch}`, ); } @@ -63,6 +67,8 @@ export const ensureBundledRuntime = ({ return externalRuntime; } + const runtimeArch = resolveBundledRuntimeArch(); + process.env[BUNDLED_RUNTIME_ARCH_ENV] = runtimeArch; const pbsTarget = resolvePbsTarget(); const runtimeBase = path.join( projectRoot, diff --git a/scripts/prepare-resources/backend-runtime.test.mjs b/scripts/prepare-resources/backend-runtime.test.mjs new file mode 100644 index 0000000..03d07f4 --- /dev/null +++ b/scripts/prepare-resources/backend-runtime.test.mjs @@ -0,0 +1,112 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; + +import * as backendRuntime from './backend-runtime.mjs'; + +const resolvePbsTarget = (options) => backendRuntime.resolvePbsTarget(options); + +test('resolvePbsTarget defaults Windows ARM64 backend runtime to x64', () => { + assert.equal( + resolvePbsTarget({ platform: 'win32', arch: 'arm64', env: {} }), + 'x86_64-pc-windows-msvc', + ); +}); + +test('resolvePbsTarget accepts explicit Windows ARM64 backend overrides', () => { + assert.equal( + resolvePbsTarget({ + platform: 'win32', + arch: 'arm64', + env: { ASTRBOT_DESKTOP_WINDOWS_ARM_BACKEND_ARCH: 'amd64' }, + }), + 'x86_64-pc-windows-msvc', + ); + + assert.equal( + resolvePbsTarget({ + platform: 'win32', + arch: 'arm64', + env: { ASTRBOT_DESKTOP_WINDOWS_ARM_BACKEND_ARCH: 'x64' }, + }), + 'x86_64-pc-windows-msvc', + ); + + assert.equal( + resolvePbsTarget({ + platform: 'win32', + arch: 'arm64', + env: { ASTRBOT_DESKTOP_WINDOWS_ARM_BACKEND_ARCH: 'arm64' }, + }), + 'aarch64-pc-windows-msvc', + ); + + assert.equal( + resolvePbsTarget({ + platform: 'win32', + arch: 'arm64', + env: { ASTRBOT_DESKTOP_WINDOWS_ARM_BACKEND_ARCH: 'aarch64' }, + }), + 'aarch64-pc-windows-msvc', + ); +}); + +test('resolvePbsTarget honors the explicit desktop target arch when process arch is emulated x64', () => { + assert.equal( + resolvePbsTarget({ + platform: 'win32', + arch: 'x64', + env: { ASTRBOT_DESKTOP_TARGET_ARCH: 'arm64' }, + }), + 'x86_64-pc-windows-msvc', + ); + + assert.equal( + resolvePbsTarget({ + platform: 'win32', + arch: 'x64', + env: { + ASTRBOT_DESKTOP_TARGET_ARCH: 'arm64', + ASTRBOT_DESKTOP_WINDOWS_ARM_BACKEND_ARCH: 'arm64', + }, + }), + 'aarch64-pc-windows-msvc', + ); +}); + +test('resolvePbsTarget rejects invalid Windows ARM64 backend override values', () => { + assert.throws( + () => + resolvePbsTarget({ + platform: 'win32', + arch: 'arm64', + env: { ASTRBOT_DESKTOP_WINDOWS_ARM_BACKEND_ARCH: 'wat' }, + }), + /ASTRBOT_DESKTOP_WINDOWS_ARM_BACKEND_ARCH[\s\S]*amd64[\s\S]*x64[\s\S]*arm64[\s\S]*aarch64/, + ); +}); + +test('resolvePbsTarget rejects invalid explicit desktop target arch values', () => { + assert.throws( + () => + resolvePbsTarget({ + platform: 'win32', + arch: 'x64', + env: { ASTRBOT_DESKTOP_TARGET_ARCH: 'wat' }, + }), + /ASTRBOT_DESKTOP_TARGET_ARCH[\s\S]*amd64[\s\S]*x64[\s\S]*arm64[\s\S]*aarch64/, + ); +}); + +test('resolvePbsTarget keeps same-arch mappings for other platform and arch combinations', () => { + const cases = [ + { platform: 'linux', arch: 'x64', expected: 'x86_64-unknown-linux-gnu' }, + { platform: 'linux', arch: 'arm64', expected: 'aarch64-unknown-linux-gnu' }, + { platform: 'darwin', arch: 'x64', expected: 'x86_64-apple-darwin' }, + { platform: 'darwin', arch: 'arm64', expected: 'aarch64-apple-darwin' }, + { platform: 'win32', arch: 'x64', expected: 'x86_64-pc-windows-msvc' }, + ]; + + for (const { platform, arch, expected } of cases) { + assert.equal(resolvePbsTarget({ platform, arch, env: {} }), expected); + } +}); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 3b1a5b2..4625301 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -58,7 +58,7 @@ dependencies = [ [[package]] name = "astrbot-desktop-tauri" -version = "4.19.5" +version = "4.20.0" dependencies = [ "chrono", "home",