From 6d0d98ed09eb15794b58c9dfd5fa355d199d3e7a Mon Sep 17 00:00:00 2001 From: PRGM Date: Wed, 8 Jul 2026 16:11:10 -0400 Subject: [PATCH] Fix desktop native optional dependency packaging Ensure Linux AppImage staging declares glibc so pnpm installs Linux native optional dependencies such as @yuuang/ffi-rs-linux-x64-gnu. Pass a pnpm npm_config_user_agent to Electron Builder so its node_modules collector uses the pnpm layout instead of falling back to npm and dropping Windows optional native packages such as @yuuang/ffi-rs-win32-x64-msvc. Add coverage for Linux staging architecture metadata and packageManager-to-user-agent conversion. --- scripts/build-desktop-artifact.test.ts | 15 ++++++++++ scripts/build-desktop-artifact.ts | 41 +++++++++++++++++++------- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index e62baf39d7e..c79fc4797be 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -33,6 +33,7 @@ import { resolveGitHubPublishConfig, resolveMockUpdateServerPort, resolveMockUpdateServerUrl, + resolvePackageManagerUserAgent, stageLinuxIconSize, STAGE_INSTALL_ARGS, } from "./build-desktop-artifact.ts"; @@ -208,6 +209,13 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { cpu: ["x64"], }, }); + assert.deepStrictEqual(createStageWorkspaceConfig({ platform: "linux", arch: "x64" }), { + supportedArchitectures: { + os: ["linux"], + cpu: ["x64"], + libc: ["glibc"], + }, + }); // Windows artifacts also bundle the same-architecture WSL (Linux, glibc) backend, so the // staged install must fetch its native optional deps (e.g. ffi-rs) too. assert.deepStrictEqual(createStageWorkspaceConfig({ platform: "win", arch: "x64" }), { @@ -253,6 +261,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { supportedArchitectures: { os: ["linux"], cpu: ["x64"], + libc: ["glibc"], }, allowBuilds: { electron: true, @@ -529,6 +538,12 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { assert.equal(resolveMockUpdateServerUrl(4123), "http://localhost:4123"); }); + it("derives the electron-builder package manager user agent from packageManager", () => { + assert.equal(resolvePackageManagerUserAgent("pnpm@11.10.0"), "pnpm/11.10.0"); + assert.equal(resolvePackageManagerUserAgent(" yarn@4.9.2 "), "yarn/4.9.2"); + assert.equal(resolvePackageManagerUserAgent("pnpm"), "pnpm"); + }); + it.effect("normalizes mock update server ports from env-style strings", () => Effect.gen(function* () { assert.equal(yield* resolveMockUpdateServerPort(undefined), undefined); diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index a75e484808c..bac3ef3df85 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -890,22 +890,27 @@ export function createStageWorkspaceConfig(input: { const { platform, arch, allowBuilds, patchedDependencies, overrides } = input; const hostOs = platform === "mac" ? "darwin" : platform === "win" ? "win32" : "linux"; const hostCpu = arch === "universal" ? ["arm64", "x64"] : [arch]; - // Windows artifacts also bundle the same-architecture WSL Linux backend, which loads - // Linux-native optional deps at runtime (e.g. @yuuang/ffi-rs-linux-x64-gnu). - // Pull the Linux (glibc) variants in addition to the host platform's so - // they ship in the asar; without them the WSL backend crash-loops on require - // ("Cannot find module '@yuuang/ffi-rs-linux-x64-gnu'"). + // Linux AppImages and Windows WSL backends both execute a Linux/glibc Node + // process that loads Linux-native optional deps at runtime (e.g. + // @yuuang/ffi-rs-linux-x64-gnu). Keep libc explicit so pnpm includes those + // optional packages in the staged production install. const supportedArchitectures = - platform === "win" + platform === "linux" ? { - os: Array.from(new Set([hostOs, "linux"])), + os: [hostOs], cpu: hostCpu, libc: ["glibc"], } - : { - os: [hostOs], - cpu: hostCpu, - }; + : platform === "win" + ? { + os: Array.from(new Set([hostOs, "linux"])), + cpu: hostCpu, + libc: ["glibc"], + } + : { + os: [hostOs], + cpu: hostCpu, + }; return { supportedArchitectures, @@ -1339,6 +1344,19 @@ export function resolveMockUpdateServerUrl(mockUpdateServerPort: number | undefi return `http://localhost:${mockUpdateServerPort ?? 3000}`; } +// Electron Builder detects pnpm from npm_config_user_agent, whose value uses +// user-agent syntax (pnpm/11.10.0) rather than packageManager syntax +// (pnpm@11.10.0). +export function resolvePackageManagerUserAgent(packageManager: string): string { + const trimmed = packageManager.trim(); + const versionSeparator = trimmed.lastIndexOf("@"); + if (versionSeparator <= 0 || versionSeparator === trimmed.length - 1) { + return trimmed; + } + + return `${trimmed.slice(0, versionSeparator)}/${trimmed.slice(versionSeparator + 1)}`; +} + export function resolveDesktopProductName(version: string): string { return resolveDesktopUpdateChannel(version) === "nightly" ? "T3 Code (Nightly)" @@ -1799,6 +1817,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( const buildEnv: NodeJS.ProcessEnv = { ...process.env, }; + buildEnv.npm_config_user_agent = resolvePackageManagerUserAgent(rootPackageJson.packageManager); for (const [key, value] of Object.entries(buildEnv)) { if (value === "") { delete buildEnv[key];