authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-16 09:12:32-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-16 09:12:32-07:00
log85fe62d4021dfabe14572b7a7ef55e3f4f1144f4
tree6100303ebc2c3bbf5b3a24e0c7753c9419ae3777
parent708d5383a9c7415c3795890ca4f664c7b00f9a47
signature Signed by PGP key B5690EEEBB952194

Stage fff native binaries in desktop builds (#3109)


6 files changed, 191 insertions(+), 42 deletions(-)

apps/server/package.json+1-1
......@@ -27,7 +27,7 @@
2727 "@effect/platform-node": "catalog:",
2828 "@effect/platform-node-shared": "catalog:",
2929 "@effect/sql-sqlite-bun": "catalog:",
30 "@ff-labs/fff-node": "^0.9.4",
30 "@ff-labs/fff-node": "0.9.4",
3131 "@opencode-ai/sdk": "^1.3.15",
3232 "@pierre/diffs": "catalog:",
3333 "effect": "catalog:",
patches/@ff-labs__fff-node@0.9.4.patch created+37
......@@ -0,0 +1,37 @@
1diff --git a/dist/src/binary.js b/dist/src/binary.js
2index ee181aef5007e4bf34a49479c089ca30f73a320b..327e2c55c83cc4c50d396a3109190ef10af75ca7 100644
3--- a/dist/src/binary.js
4+++ b/dist/src/binary.js
5@@ -7,7 +7,7 @@
6 */
7 import { existsSync, readFileSync } from "node:fs";
8 import { createRequire } from "node:module";
9-import { dirname, join } from "node:path";
10+import { dirname, join, sep } from "node:path";
11 import { fileURLToPath } from "node:url";
12 import { getLibFilename, getNpmPackageName } from "./platform.js";
13 /**
14@@ -46,6 +46,14 @@ function getPackageDir() {
15 // Fallback: assume we're one level deep in src/
16 return dirname(currentDir);
17 }
18+function resolveUnpackedAsarPath(binaryPath) {
19+ const asarSegment = `${sep}app.asar${sep}`;
20+ if (!binaryPath.includes(asarSegment)) {
21+ return binaryPath;
22+ }
23+ const unpackedPath = binaryPath.replace(asarSegment, `${sep}app.asar.unpacked${sep}`);
24+ return existsSync(unpackedPath) ? unpackedPath : binaryPath;
25+}
26 /**
27 * Check if the binary exists in any known location
28 */
29@@ -69,7 +77,7 @@ function resolveFromNpmPackage() {
30 const packageDir = dirname(packageJsonPath);
31 const binaryPath = join(packageDir, getLibFilename());
32 if (existsSync(binaryPath)) {
33- return binaryPath;
34+ return resolveUnpackedAsarPath(binaryPath);
35 }
36 }
37 catch {
pnpm-lock.yaml+6-3
......@@ -66,6 +66,9 @@ patchedDependencies:
6666 '@expo/metro-config@56.0.13':
6767 hash: 8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46
6868 path: patches/@expo%2Fmetro-config@56.0.13.patch
69 '@ff-labs/fff-node@0.9.4':
70 hash: 2b16019ce7ab61aec6478dd02f79ef468cc1d5c51e9d00764f7d2ab8167210c8
71 path: patches/@ff-labs__fff-node@0.9.4.patch
6972 '@pierre/diffs@1.3.0-beta.4':
7073 hash: f5d41705ce94bbafc731d92e9cb1671db710df046dd135cb894e3e3e9164a75b
7174 path: patches/@pierre%2Fdiffs@1.3.0-beta.4.patch
......@@ -396,8 +399,8 @@ importers:
396399 specifier: 4.0.0-beta.78
397400 version: 4.0.0-beta.78(effect@4.0.0-beta.78(patch_hash=883249d8efbb462e928e21fefef96027b66aec50751178cafdce45f08eee3754))
398401 '@ff-labs/fff-node':
399 specifier: ^0.9.4
400 version: 0.9.4
402 specifier: 0.9.4
403 version: 0.9.4(patch_hash=2b16019ce7ab61aec6478dd02f79ef468cc1d5c51e9d00764f7d2ab8167210c8)
401404 '@opencode-ai/sdk':
402405 specifier: ^1.3.15
403406 version: 1.15.13
......@@ -11854,7 +11857,7 @@ snapshots:
1185411857 '@ff-labs/fff-bin-win32-x64@0.9.4':
1185511858 optional: true
1185611859
11857 '@ff-labs/fff-node@0.9.4':
11860 '@ff-labs/fff-node@0.9.4(patch_hash=2b16019ce7ab61aec6478dd02f79ef468cc1d5c51e9d00764f7d2ab8167210c8)':
1185811861 dependencies:
1185911862 ffi-rs: 1.3.2
1186011863 optionalDependencies:
pnpm-workspace.yaml+34-36
......@@ -1,22 +1,11 @@
11packages:
2 - "apps/*"
3 - "infra/*"
4 - "oxlint-plugin-t3code"
5 - "packages/*"
6 - "scripts"
7
8onlyBuiltDependencies:
9 - electron
10 - esbuild
11 - msgpackr-extract
12 - node-pty
13 - sharp
14
15ignoredBuiltDependencies:
16 - msw
2 - apps/*
3 - infra/*
4 - oxlint-plugin-t3code
5 - packages/*
6 - scripts
177
188catalog:
19 effect: 4.0.0-beta.78
209 "@effect/atom-react": 4.0.0-beta.78
2110 "@effect/openapi-generator": 4.0.0-beta.78
2211 "@effect/platform-bun": 4.0.0-beta.78
......@@ -24,23 +13,33 @@ catalog:
2413 "@effect/platform-node-shared": 4.0.0-beta.78
2514 "@effect/sql-pg": 4.0.0-beta.78
2615 "@effect/sql-sqlite-bun": 4.0.0-beta.78
27 "@effect/vitest": 4.0.0-beta.78
2816 "@effect/tsgo": 0.13.2
17 "@effect/vitest": 4.0.0-beta.78
2918 "@noble/curves": 1.9.1
3019 "@noble/hashes": 1.8.0
3120 "@pierre/diffs": 1.3.0-beta.4
32 "@vitest/runner": 4.1.8
3321 "@types/node": 24.12.4
3422 "@typescript/native-preview": 7.0.0-dev.20260604.1
23 "@vitest/runner": 4.1.8
24 effect: 4.0.0-beta.78
3525 jose: 6.2.2
3626 typescript: ~6.0.3
37 vitest: npm:@voidzero-dev/vite-plus-test@0.1.24
3827 vite: npm:@voidzero-dev/vite-plus-core@0.1.24
3928 vite-plus: 0.1.24
29 vitest: npm:@voidzero-dev/vite-plus-test@0.1.24
4030 yaml: ^2.9.0
31
32ignoredBuiltDependencies:
33 - msw
34
35onlyBuiltDependencies:
36 - electron
37 - esbuild
38 - msgpackr-extract
39 - node-pty
40 - sharp
41
4142overrides:
42 # Clerk publishes wallet auth integrations as required dependencies. T3 Code does
43 # not support wallet auth, so keep that unused dependency tree out of installs.
4443 "@clerk/clerk-js>@base-org/account": "-"
4544 "@clerk/clerk-js>@coinbase/wallet-sdk": "-"
4645 "@clerk/clerk-js>@solana/wallet-adapter-base": "-"
......@@ -56,22 +55,13 @@ overrides:
5655 "@effect/vitest": "catalog:"
5756 "@effect/vitest>@vitest/runner": "-"
5857 "@effect/vitest>vitest": "-"
59 # Pinned to the version the patch in patchedDependencies targets — a fresh
60 # resolve (e.g. release-smoke regenerating the lockfile) must not drift to a
61 # newer release and orphan the patch (ERR_PNPM_UNUSED_PATCH).
62 "@expo/metro-config": "56.0.13"
58 "@expo/metro-config": 56.0.13
6359 "@types/node": "catalog:"
6460 effect: "catalog:"
6561 vite: "catalog:"
6662 vitest: "catalog:"
6763 yaml: "catalog:"
68peerDependencyRules:
69 allowAny:
70 - vite
71 - vitest
72 allowedVersions:
73 vite: "*"
74 vitest: "*"
64
7565packageExtensions:
7666 "@effect/vitest@*":
7767 dependencies:
......@@ -79,14 +69,22 @@ packageExtensions:
7969 peerDependenciesMeta:
8070 vitest:
8171 optional: true
82 "vite-plus@*":
72 vite-plus@*:
8373 dependencies:
8474 vite: "catalog:"
75
8576patchedDependencies:
86 # Keep non-browser consumers off the root entrypoint, which eagerly imports
87 # DOM-dependent renderers. These utility/type subpaths are safe for mobile.
88 "@pierre/diffs@1.3.0-beta.4": patches/@pierre%2Fdiffs@1.3.0-beta.4.patch
8977 "@effect/vitest@4.0.0-beta.78": patches/@effect__vitest@4.0.0-beta.78.patch
9078 "@expo/metro-config@56.0.13": patches/@expo%2Fmetro-config@56.0.13.patch
79 "@ff-labs/fff-node@0.9.4": patches/@ff-labs__fff-node@0.9.4.patch
80 "@pierre/diffs@1.3.0-beta.4": patches/@pierre%2Fdiffs@1.3.0-beta.4.patch
9181 effect@4.0.0-beta.78: patches/effect@4.0.0-beta.78.patch
9282 react-native-nitro-modules@0.35.9: patches/react-native-nitro-modules@0.35.9.patch
83
84peerDependencyRules:
85 allowAny:
86 - vite
87 - vitest
88 allowedVersions:
89 vite: "*"
90 vitest: "*"
scripts/build-desktop-artifact.test.ts+50
......@@ -6,8 +6,11 @@ import * as Layer from "effect/Layer";
66import * as Option from "effect/Option";
77
88import {
9 createStageWorkspaceConfig,
910 createStagePnpmConfig,
11 DESKTOP_ASAR_UNPACK,
1012 resolveDesktopRuntimeDependencies,
13 resolveFffNativeDependencies,
1114 resolveBuildOptions,
1215 resolveDesktopBuildIconAssets,
1316 resolveDesktopProductName,
......@@ -15,6 +18,7 @@ import {
1518 resolveGitHubPublishConfig,
1619 resolveMockUpdateServerPort,
1720 resolveMockUpdateServerUrl,
21 STAGE_INSTALL_ARGS,
1822} from "./build-desktop-artifact.ts";
1923import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";
2024import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess";
......@@ -114,17 +118,20 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
114118 createStagePnpmConfig(
115119 {
116120 "@expo/metro-config@56.0.13": "patches/@expo%2Fmetro-config@56.0.13.patch",
121 "@ff-labs/fff-node@0.9.4": "patches/@ff-labs__fff-node@0.9.4.patch",
117122 "@pierre/diffs@1.1.20": "patches/@pierre%2Fdiffs@1.1.20.patch",
118123 "alchemy@2.0.0-beta.49": "patches/alchemy@2.0.0-beta.49.patch",
119124 "effect@4.0.0-beta.73": "patches/effect@4.0.0-beta.73.patch",
120125 },
121126 {
127 "@ff-labs/fff-node": "0.9.4",
122128 "@pierre/diffs": "1.1.20",
123129 effect: "4.0.0-beta.73",
124130 },
125131 ),
126132 {
127133 patchedDependencies: {
134 "@ff-labs/fff-node@0.9.4": "patches/@ff-labs__fff-node@0.9.4.patch",
128135 "@pierre/diffs@1.1.20": "patches/@pierre%2Fdiffs@1.1.20.patch",
129136 "effect@4.0.0-beta.73": "patches/effect@4.0.0-beta.73.patch",
130137 },
......@@ -142,6 +149,49 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
142149 );
143150 });
144151
152 it("installs optional native dependencies for the target desktop architecture", () => {
153 assert.deepStrictEqual(STAGE_INSTALL_ARGS, ["install", "--prod"]);
154 assert.deepStrictEqual(createStageWorkspaceConfig("mac", "x64"), {
155 supportedArchitectures: {
156 os: ["darwin"],
157 cpu: ["x64"],
158 },
159 });
160 assert.deepStrictEqual(createStageWorkspaceConfig("win", "arm64"), {
161 supportedArchitectures: {
162 os: ["win32"],
163 cpu: ["arm64"],
164 },
165 });
166 assert.deepStrictEqual(createStageWorkspaceConfig("mac", "universal"), {
167 supportedArchitectures: {
168 os: ["darwin"],
169 cpu: ["arm64", "x64"],
170 },
171 });
172 });
173
174 it("unpacks the fff shared library for filesystem and FFI access", () => {
175 assert.deepStrictEqual(DESKTOP_ASAR_UNPACK, ["node_modules/@ff-labs/fff-bin-*/**/*"]);
176 });
177
178 it("promotes target fff binaries to direct staged dependencies", () => {
179 assert.deepStrictEqual(resolveFffNativeDependencies("mac", "arm64", "0.9.4"), {
180 "@ff-labs/fff-bin-darwin-arm64": "0.9.4",
181 });
182 assert.deepStrictEqual(resolveFffNativeDependencies("mac", "universal", "0.9.4"), {
183 "@ff-labs/fff-bin-darwin-arm64": "0.9.4",
184 "@ff-labs/fff-bin-darwin-x64": "0.9.4",
185 });
186 assert.deepStrictEqual(resolveFffNativeDependencies("win", "x64", "0.9.4"), {
187 "@ff-labs/fff-bin-win32-x64": "0.9.4",
188 });
189 assert.deepStrictEqual(resolveFffNativeDependencies("linux", "arm64", "0.9.4"), {
190 "@ff-labs/fff-bin-linux-arm64-gnu": "0.9.4",
191 "@ff-labs/fff-bin-linux-arm64-musl": "0.9.4",
192 });
193 });
194
145195 it("falls back to the default mock update port when the configured port is blank", () => {
146196 assert.equal(resolveMockUpdateServerUrl(undefined), "http://localhost:3000");
147197 assert.equal(resolveMockUpdateServerUrl(4123), "http://localhost:4123");
scripts/build-desktop-artifact.ts+63-2
......@@ -38,11 +38,19 @@ const WorkspaceConfig = Schema.Struct({
3838});
3939type WorkspaceConfig = typeof WorkspaceConfig.Type;
4040
41const StageWorkspaceConfig = Schema.Struct({
42 supportedArchitectures: Schema.Struct({
43 os: Schema.Array(Schema.String),
44 cpu: Schema.Array(Schema.String),
45 }),
46});
47
4148const RepoRoot = Effect.service(Path.Path).pipe(
4249 Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))),
4350);
4451const encodeJsonString = Schema.encodeEffect(Schema.UnknownFromJsonString);
4552const decodeWorkspaceConfig = Schema.decodeEffect(fromYaml(WorkspaceConfig));
53const encodeStageWorkspaceConfig = Schema.encodeEffect(fromYaml(StageWorkspaceConfig));
4654
4755const readWorkspaceConfig = Effect.fn("readWorkspaceConfig")(function* () {
4856 const fs = yield* FileSystem.FileSystem;
......@@ -282,6 +290,47 @@ interface StagePackageJson {
282290 };
283291}
284292
293export const STAGE_INSTALL_ARGS = ["install", "--prod"] as const;
294export const DESKTOP_ASAR_UNPACK = ["node_modules/@ff-labs/fff-bin-*/**/*"] as const;
295
296export function resolveFffNativeDependencies(
297 platform: typeof BuildPlatform.Type,
298 arch: typeof BuildArch.Type,
299 version: string,
300): Record<string, string> {
301 const architectures = arch === "universal" ? (["arm64", "x64"] as const) : [arch];
302
303 if (platform === "mac") {
304 return Object.fromEntries(
305 architectures.map((architecture) => [`@ff-labs/fff-bin-darwin-${architecture}`, version]),
306 );
307 }
308
309 if (platform === "win") {
310 return Object.fromEntries(
311 architectures.map((architecture) => [`@ff-labs/fff-bin-win32-${architecture}`, version]),
312 );
313 }
314
315 return Object.fromEntries(
316 architectures.flatMap((architecture) =>
317 ["gnu", "musl"].map((libc) => [`@ff-labs/fff-bin-linux-${architecture}-${libc}`, version]),
318 ),
319 );
320}
321
322export function createStageWorkspaceConfig(
323 platform: typeof BuildPlatform.Type,
324 arch: typeof BuildArch.Type,
325): typeof StageWorkspaceConfig.Type {
326 return {
327 supportedArchitectures: {
328 os: [platform === "mac" ? "darwin" : platform === "win" ? "win32" : "linux"],
329 cpu: arch === "universal" ? ["arm64", "x64"] : [arch],
330 },
331 };
332}
333
285334export function createStagePnpmConfig(
286335 patchedDependencies: Record<string, string>,
287336 dependencies: Record<string, unknown>,
......@@ -702,6 +751,7 @@ const createBuildConfig = Effect.fn("createBuildConfig")(function* (
702751 appId: "com.t3tools.t3code",
703752 productName: resolveDesktopProductName(version),
704753 artifactName: "T3-Code-${version}-${arch}.${ext}",
754 asarUnpack: [...DESKTOP_ASAR_UNPACK],
705755 directories: {
706756 buildResources: "apps/desktop/resources",
707757 },
......@@ -909,6 +959,11 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
909959 const stageDependencies = {
910960 ...resolvedServerDependencies,
911961 ...resolvedDesktopRuntimeDependencies,
962 ...resolveFffNativeDependencies(
963 options.platform,
964 options.arch,
965 serverPackageJson.dependencies["@ff-labs/fff-node"],
966 ),
912967 };
913968 const stagePnpmConfig = createStagePnpmConfig(workspacePatchedDependencies, stageDependencies);
914969 const stagePackageJson: StagePackageJson = {
......@@ -939,19 +994,25 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
939994
940995 const stagePackageJsonString = yield* encodeJsonString(stagePackageJson);
941996 yield* fs.writeFileString(path.join(stageAppDir, "package.json"), `${stagePackageJsonString}\n`);
997 const stageWorkspaceConfig = createStageWorkspaceConfig(options.platform, options.arch);
998 const stageWorkspaceConfigString = yield* encodeStageWorkspaceConfig(stageWorkspaceConfig);
999 yield* fs.writeFileString(
1000 path.join(stageAppDir, "pnpm-workspace.yaml"),
1001 stageWorkspaceConfigString,
1002 );
9421003
9431004 if (Object.keys(workspacePatchedDependencies).length > 0) {
9441005 yield* fs.copy(path.join(repoRoot, "patches"), path.join(stageAppDir, "patches"));
9451006 }
9461007
9471008 yield* Effect.log("[desktop-artifact] Installing staged production dependencies...");
948 const installCommand = yield* resolveSpawnCommand("vp", ["install", "--prod", "--no-optional"]);
1009 const installCommand = yield* resolveSpawnCommand("vp", [...STAGE_INSTALL_ARGS]);
9491010 yield* runCommand(
9501011 ChildProcess.make(installCommand.command, installCommand.args, {
9511012 cwd: stageAppDir,
9521013 shell: installCommand.shell,
9531014 }),
954 { label: "vp install --prod --no-optional", verbose: options.verbose },
1015 { label: "vp install --prod", verbose: options.verbose },
9551016 );
9561017
9571018 // electron-builder treats several set-but-empty variables (e.g. CSC_LINK="")