Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build-desktop-tauri.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion docs/environment-variables.md
Original file line number Diff line number Diff line change
@@ -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`)
Expand Down Expand Up @@ -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. 桌面进程写入给后端子进程

Expand All @@ -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. 维护约定

Expand Down
3 changes: 2 additions & 1 deletion scripts/backend/build-backend.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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, '..', '..');
Expand Down Expand Up @@ -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:
Expand Down
64 changes: 64 additions & 0 deletions scripts/backend/runtime-arch-utils.mjs
Original file line number Diff line number Diff line change
@@ -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()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current check for overrideRaw is not robust against null values. If env[DESKTOP_TARGET_ARCH_ENV] is null, overrideRaw !== undefined evaluates to true, and String(overrideRaw).trim() becomes 'null', which is a truthy string. This would cause normalizeDesktopArch to be called with null, leading to an error. Using the nullish coalescing operator (??) provides a more robust way to handle potentially null or undefined values, ensuring they are treated as empty strings and correctly ignored.

Suggested change
if (overrideRaw !== undefined && String(overrideRaw).trim()) {
if (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()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the previous comment, this check is not robust against null values. If explicitBundledRuntimeArch is null, it will be incorrectly processed as the string 'null'. Please use the nullish coalescing operator (??) for a more robust check that correctly handles null and undefined values.

Suggested change
if (explicitBundledRuntimeArch !== undefined && String(explicitBundledRuntimeArch).trim()) {
if (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()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This check for windowsArmBackendArch is also vulnerable to null values. If windowsArmBackendArch is null, the condition windowsArmBackendArch === undefined is false, and !String(windowsArmBackendArch).trim() is also false. This causes the if block to be skipped and normalizeDesktopArch to be called with null, which will fail. A more robust check is needed here as well.

Suggested change
if (windowsArmBackendArch === undefined || !String(windowsArmBackendArch).trim()) {
if (!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';
57 changes: 57 additions & 0 deletions scripts/backend/runtime-arch-utils.test.mjs
Original file line number Diff line number Diff line change
@@ -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,
);
});
22 changes: 14 additions & 8 deletions scripts/prepare-resources/backend-runtime.mjs
Original file line number Diff line number Diff line change
@@ -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}`,
);
}

Expand Down Expand Up @@ -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,
Expand Down
112 changes: 112 additions & 0 deletions scripts/prepare-resources/backend-runtime.test.mjs
Original file line number Diff line number Diff line change
@@ -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);
}
});
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.