icebear icebear0828 commited on
Commit
6c928c9
·
unverified ·
1 Parent(s): 34f3ea1

fix: macOS Electron login error -86 (EBADARCH) due to wrong-arch curl binary (#96) (#98)

Browse files

CI built both arm64/x64 DMGs on a single arm64 runner, bundling only
the arm64 curl-impersonate into both. Intel Mac users got EBADARCH when
spawning the binary during OAuth token exchange.

- Split macOS CI into per-arch jobs with --arch flag for setup-curl.ts
- Add --arch override to setup-curl.ts for cross-compilation
- Improve spawn error message to diagnose architecture mismatch

Co-authored-by: icebear0828 <icebear0828@users.noreply.github.com>

.github/workflows/release.yml CHANGED
@@ -25,7 +25,12 @@ jobs:
25
  artifact: windows
26
  - os: macos-latest
27
  platform: mac
28
- artifact: macos
 
 
 
 
 
29
  - os: ubuntu-latest
30
  platform: linux
31
  artifact: linux
@@ -48,7 +53,7 @@ jobs:
48
  run: npm ci --ignore-scripts
49
 
50
  - name: Setup curl-impersonate
51
- run: npx tsx scripts/setup-curl.ts
52
  env:
53
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
54
 
@@ -61,8 +66,8 @@ jobs:
61
  - name: Build app
62
  run: npm run app:build
63
 
64
- - name: Pack (${{ matrix.platform }})
65
- run: npx electron-builder --config electron-builder.yml --${{ matrix.platform }} --publish never
66
 
67
  - name: Upload artifacts
68
  uses: actions/upload-artifact@v4
 
25
  artifact: windows
26
  - os: macos-latest
27
  platform: mac
28
+ arch: arm64
29
+ artifact: macos-arm64
30
+ - os: macos-latest
31
+ platform: mac
32
+ arch: x64
33
+ artifact: macos-x64
34
  - os: ubuntu-latest
35
  platform: linux
36
  artifact: linux
 
53
  run: npm ci --ignore-scripts
54
 
55
  - name: Setup curl-impersonate
56
+ run: npx tsx scripts/setup-curl.ts ${{ matrix.arch && format('--arch {0}', matrix.arch) || '' }}
57
  env:
58
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
59
 
 
66
  - name: Build app
67
  run: npm run app:build
68
 
69
+ - name: Pack (${{ matrix.platform }}${{ matrix.arch && format('-{0}', matrix.arch) || '' }})
70
+ run: npx electron-builder --config electron-builder.yml --${{ matrix.platform }} ${{ matrix.arch && format('--{0}', matrix.arch) || '' }} --publish never
71
 
72
  - name: Upload artifacts
73
  uses: actions/upload-artifact@v4
CHANGELOG.md CHANGED
@@ -8,6 +8,7 @@
8
 
9
  ### Fixed
10
 
 
11
  - 默认关闭 desktop context 注入:之前每次请求注入 ~1500 token 的 Codex Desktop 系统提示,导致 prompt_tokens 虚高;新增 `model.inject_desktop_context` 配置项(默认 `false`),需要时可手动开启 (#95)
12
 
13
  ### Added
 
8
 
9
  ### Fixed
10
 
11
+ - macOS Electron 桌面版登录报 `spawn Unknown system error -86`:CI 在 arm64 runner 上同时构建 arm64/x64 DMG,但只下载 arm64 的 curl-impersonate,导致 Intel Mac 用户 spawn 失败(EBADARCH);拆分为 per-arch 构建 + `setup-curl.ts` 支持 `--arch` 交叉下载;错误提示改为明确的架构不匹配诊断 (#96)
12
  - 默认关闭 desktop context 注入:之前每次请求注入 ~1500 token 的 Codex Desktop 系统提示,导致 prompt_tokens 虚高;新增 `model.inject_desktop_context` 配置项(默认 `false`),需要时可手动开启 (#95)
13
 
14
  ### Added
scripts/setup-curl.ts CHANGED
@@ -27,9 +27,18 @@ interface PlatformInfo {
27
  destName: string;
28
  }
29
 
 
 
 
 
 
 
 
 
 
30
  function getPlatformInfo(version: string): PlatformInfo {
31
  const platform = process.platform;
32
- const arch = process.arch;
33
 
34
  if (platform === "linux") {
35
  const archStr = arch === "arm64" ? "aarch64-linux-gnu" : "x86_64-linux-gnu";
@@ -227,7 +236,8 @@ async function main() {
227
  // Resolve latest version from GitHub
228
  const version = await getLatestVersion();
229
  console.log(`[setup] curl-impersonate setup (${version})`);
230
- console.log(`[setup] Platform: ${process.platform}-${process.arch}`);
 
231
 
232
  const info = getPlatformInfo(version);
233
  const isWindowsDll = process.platform === "win32";
 
27
  destName: string;
28
  }
29
 
30
+ /** Parse --arch flag from CLI args to override process.arch (for cross-compilation). */
31
+ function getTargetArch(): string {
32
+ const idx = process.argv.indexOf("--arch");
33
+ if (idx !== -1 && process.argv[idx + 1]) {
34
+ return process.argv[idx + 1];
35
+ }
36
+ return process.arch;
37
+ }
38
+
39
  function getPlatformInfo(version: string): PlatformInfo {
40
  const platform = process.platform;
41
+ const arch = getTargetArch();
42
 
43
  if (platform === "linux") {
44
  const archStr = arch === "arm64" ? "aarch64-linux-gnu" : "x86_64-linux-gnu";
 
236
  // Resolve latest version from GitHub
237
  const version = await getLatestVersion();
238
  console.log(`[setup] curl-impersonate setup (${version})`);
239
+ const targetArch = getTargetArch();
240
+ console.log(`[setup] Platform: ${process.platform}-${targetArch}${targetArch !== process.arch ? ` (cross: host=${process.arch})` : ""}`);
241
 
242
  const info = getPlatformInfo(version);
243
  const isWindowsDll = process.platform === "win32";
src/tls/__tests__/curl-cli-transport.test.ts ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it, expect, vi } from "vitest";
2
+
3
+ vi.mock("../curl-binary.js", () => ({
4
+ resolveCurlBinary: () => "/mock/bin/curl-impersonate",
5
+ getChromeTlsArgs: () => [],
6
+ getProxyArgs: () => [],
7
+ isImpersonate: () => true,
8
+ }));
9
+
10
+ import { formatSpawnError } from "../curl-cli-transport.js";
11
+
12
+ describe("formatSpawnError", () => {
13
+ it("returns architecture mismatch hint for errno -86 (EBADARCH)", () => {
14
+ const err = Object.assign(new Error("spawn Unknown system error -86"), { errno: -86 });
15
+ const msg = formatSpawnError(err);
16
+ expect(msg).toContain("wrong CPU architecture");
17
+ expect(msg).toContain(process.arch);
18
+ expect(msg).toContain("npm run setup");
19
+ expect(msg).toContain("/mock/bin/curl-impersonate");
20
+ });
21
+
22
+ it("detects -86 from error message when errno is not set", () => {
23
+ const err = new Error("spawn Unknown system error -86");
24
+ const msg = formatSpawnError(err);
25
+ expect(msg).toContain("wrong CPU architecture");
26
+ });
27
+
28
+ it("returns generic message for other spawn errors", () => {
29
+ const err = new Error("spawn ENOENT");
30
+ const msg = formatSpawnError(err);
31
+ expect(msg).toBe("curl spawn error: spawn ENOENT");
32
+ expect(msg).not.toContain("architecture");
33
+ });
34
+ });
src/tls/__tests__/setup-curl-arch.test.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it, expect } from "vitest";
2
+ import { execFileSync } from "child_process";
3
+ import { resolve } from "path";
4
+
5
+ const SETUP_SCRIPT = resolve(__dirname, "../../../scripts/setup-curl.ts");
6
+
7
+ describe("setup-curl --arch flag", () => {
8
+ it("uses --arch override in platform line", () => {
9
+ const output = execFileSync(
10
+ "npx",
11
+ ["tsx", SETUP_SCRIPT, "--check", "--arch", "x64"],
12
+ { encoding: "utf-8", timeout: 30_000 },
13
+ );
14
+ // Should report x64 as target, with cross-compilation note if host differs
15
+ expect(output).toContain(`${process.platform}-x64`);
16
+ if (process.arch !== "x64") {
17
+ expect(output).toContain(`cross: host=${process.arch}`);
18
+ }
19
+ });
20
+
21
+ it("defaults to host arch without --arch flag", () => {
22
+ const output = execFileSync(
23
+ "npx",
24
+ ["tsx", SETUP_SCRIPT, "--check"],
25
+ { encoding: "utf-8", timeout: 30_000 },
26
+ );
27
+ expect(output).toContain(`${process.platform}-${process.arch}`);
28
+ expect(output).not.toContain("cross:");
29
+ });
30
+ });
src/tls/curl-cli-transport.ts CHANGED
@@ -160,7 +160,7 @@ export class CurlCliTransport implements TlsTransport {
160
  if (signal) {
161
  signal.removeEventListener("abort", onAbort);
162
  }
163
- reject(new Error(`curl spawn error: ${err.message}`));
164
  });
165
  });
166
  }
@@ -228,6 +228,24 @@ export class CurlCliTransport implements TlsTransport {
228
  }
229
  }
230
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  /** Execute curl via execFile and parse the status code from the output. */
232
  function execCurl(args: string[]): Promise<{ status: number; body: string }> {
233
  return new Promise((resolve, reject) => {
@@ -237,7 +255,13 @@ function execCurl(args: string[]): Promise<{ status: number; body: string }> {
237
  { maxBuffer: 2 * 1024 * 1024 },
238
  (err, stdout, stderr) => {
239
  if (err) {
240
- reject(new Error(`curl failed: ${err.message} ${stderr}`));
 
 
 
 
 
 
241
  return;
242
  }
243
 
 
160
  if (signal) {
161
  signal.removeEventListener("abort", onAbort);
162
  }
163
+ reject(new Error(formatSpawnError(err)));
164
  });
165
  });
166
  }
 
228
  }
229
  }
230
 
231
+ /**
232
+ * Format a spawn error with architecture hint for EBADARCH (-86) on macOS.
233
+ * This commonly happens when curl-impersonate binary doesn't match the CPU arch.
234
+ */
235
+ export function formatSpawnError(err: Error & { errno?: number; code?: string }): string {
236
+ // errno -86 = EBADARCH (Bad CPU type in executable) on macOS
237
+ if (err.errno === -86 || err.message.includes("-86")) {
238
+ const binary = resolveCurlBinary();
239
+ return (
240
+ `curl-impersonate binary has wrong CPU architecture for this system. ` +
241
+ `Binary: ${binary}, Host arch: ${process.arch}. ` +
242
+ `Fix: run "npm run setup -- --force" to download the correct binary, ` +
243
+ `or delete bin/curl-impersonate to fall back to system curl.`
244
+ );
245
+ }
246
+ return `curl spawn error: ${err.message}`;
247
+ }
248
+
249
  /** Execute curl via execFile and parse the status code from the output. */
250
  function execCurl(args: string[]): Promise<{ status: number; body: string }> {
251
  return new Promise((resolve, reject) => {
 
255
  { maxBuffer: 2 * 1024 * 1024 },
256
  (err, stdout, stderr) => {
257
  if (err) {
258
+ const castErr = err as Error & { errno?: number };
259
+ // Check for EBADARCH first (architecture mismatch)
260
+ if (castErr.errno === -86 || err.message.includes("-86")) {
261
+ reject(new Error(formatSpawnError(castErr)));
262
+ } else {
263
+ reject(new Error(`curl failed: ${err.message} ${stderr}`));
264
+ }
265
  return;
266
  }
267