authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-17 23:39:29+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-17 23:39:29+02:00
log8b3379952386f5a3a04875fbccf6e6e8692757f8
tree579ef5ae5871fb8758d651c8f448bb5c31bfd6ed
parent24f9c2a08e7b72a8f02b92aba229c34d35f72e30
signature Signed by PGP key B5690EEEBB952194

Preserve T3 Code identity in macOS development launcher (#4102)


2 files changed, 68 insertions(+), 10 deletions(-)

apps/desktop/scripts/electron-launcher.mjs+34-9
...@@ -20,7 +20,7 @@ export const APP_BUNDLE_ID = isDevelopment...@@ -20,7 +20,7 @@ export const APP_BUNDLE_ID = isDevelopment
20 ? `com.t3tools.t3code.dev.${devBundleIdSuffix || "local"}`20 ? `com.t3tools.t3code.dev.${devBundleIdSuffix || "local"}`
21 : "com.t3tools.t3code";21 : "com.t3tools.t3code";
22const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"];22const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"];
23const LAUNCHER_VERSION = 12;23const LAUNCHER_VERSION = 14;
24const defaultIconPath = NodePath.join(desktopDir, "resources", "icon.icns");24const defaultIconPath = NodePath.join(desktopDir, "resources", "icon.icns");
25const developmentMacIconPngPath = NodePath.join(25const developmentMacIconPngPath = NodePath.join(
26 repoRoot,26 repoRoot,
...@@ -220,11 +220,12 @@ function ensureDevelopmentIconIcns(runtimeDir) {...@@ -220,11 +220,12 @@ function ensureDevelopmentIconIcns(runtimeDir) {
220 }220 }
221}221}
222222
223function patchMainBundleInfoPlist(appBundlePath, iconPath) {223function patchMainBundleInfoPlist(appBundlePath, iconPath, executableName) {
224 const infoPlistPath = NodePath.join(appBundlePath, "Contents", "Info.plist");224 const infoPlistPath = NodePath.join(appBundlePath, "Contents", "Info.plist");
225 setPlistString(infoPlistPath, "CFBundleDisplayName", APP_DISPLAY_NAME);225 setPlistString(infoPlistPath, "CFBundleDisplayName", APP_DISPLAY_NAME);
226 setPlistString(infoPlistPath, "CFBundleName", APP_DISPLAY_NAME);226 setPlistString(infoPlistPath, "CFBundleName", APP_DISPLAY_NAME);
227 setPlistString(infoPlistPath, "CFBundleIdentifier", APP_BUNDLE_ID);227 setPlistString(infoPlistPath, "CFBundleIdentifier", APP_BUNDLE_ID);
228 setPlistString(infoPlistPath, "CFBundleExecutable", executableName);
228 setPlistString(infoPlistPath, "CFBundleIconFile", "icon.icns");229 setPlistString(infoPlistPath, "CFBundleIconFile", "icon.icns");
229 setPlistJson(infoPlistPath, "CFBundleURLTypes", [230 setPlistJson(infoPlistPath, "CFBundleURLTypes", [
230 {231 {
...@@ -277,11 +278,25 @@ function readJson(path) {...@@ -277,11 +278,25 @@ function readJson(path) {
277 }278 }
278}279}
279280
281export function resolveMacLauncherPaths(appBundlePath, displayName = APP_DISPLAY_NAME) {
282 const executableDir = NodePath.join(appBundlePath, "Contents", "MacOS");
283 const launcherExecutableName = `${displayName} Launcher`;
284 return {
285 launcherExecutableName,
286 launcherBinaryPath: NodePath.join(executableDir, launcherExecutableName),
287 runtimeElectronBinaryPath: NodePath.join(executableDir, "Electron"),
288 };
289}
290
280function buildMacLauncher(electronBinaryPath) {291function buildMacLauncher(electronBinaryPath) {
281 const sourceAppBundlePath = NodePath.resolve(NodePath.dirname(electronBinaryPath), "../..");292 const sourceAppBundlePath = NodePath.resolve(NodePath.dirname(electronBinaryPath), "../..");
282 const runtimeDir = NodePath.join(desktopDir, ".electron-runtime");293 const runtimeDir = NodePath.join(desktopDir, ".electron-runtime");
283 const targetAppBundlePath = NodePath.join(runtimeDir, `${APP_DISPLAY_NAME}.app`);294 const targetAppBundlePath = NodePath.join(runtimeDir, `${APP_DISPLAY_NAME}.app`);
284 const targetBinaryPath = NodePath.join(targetAppBundlePath, "Contents", "MacOS", "Electron");295 const developmentPaths = resolveMacLauncherPaths(targetAppBundlePath);
296 const runtimeElectronBinaryPath = developmentPaths.runtimeElectronBinaryPath;
297 const launcherBinaryPath = isDevelopment
298 ? developmentPaths.launcherBinaryPath
299 : runtimeElectronBinaryPath;
285 const iconPath = isDevelopment ? ensureDevelopmentIconIcns(runtimeDir) : defaultIconPath;300 const iconPath = isDevelopment ? ensureDevelopmentIconIcns(runtimeDir) : defaultIconPath;
286 const metadataPath = NodePath.join(runtimeDir, "metadata.json");301 const metadataPath = NodePath.join(runtimeDir, "metadata.json");
287302
...@@ -298,7 +313,8 @@ function buildMacLauncher(electronBinaryPath) {...@@ -298,7 +313,8 @@ function buildMacLauncher(electronBinaryPath) {
298313
299 const currentMetadata = readJson(metadataPath);314 const currentMetadata = readJson(metadataPath);
300 if (315 if (
301 NodeFS.existsSync(targetBinaryPath) &&316 NodeFS.existsSync(launcherBinaryPath) &&
317 (!isDevelopment || NodeFS.existsSync(runtimeElectronBinaryPath)) &&
302 currentMetadata &&318 currentMetadata &&
303 JSON.stringify(currentMetadata) === JSON.stringify(expectedMetadata)319 JSON.stringify(currentMetadata) === JSON.stringify(expectedMetadata)
304 ) {320 ) {
...@@ -306,10 +322,10 @@ function buildMacLauncher(electronBinaryPath) {...@@ -306,10 +322,10 @@ function buildMacLauncher(electronBinaryPath) {
306 // The launcher also handles protocol activations outside the dev runner,322 // The launcher also handles protocol activations outside the dev runner,
307 // so refresh its fallback environment on every launch. Never let a value323 // so refresh its fallback environment on every launch. Never let a value
308 // captured by an older parent app override the live dev-runner environment.324 // captured by an older parent app override the live dev-runner environment.
309 writeDevelopmentLauncherScript(targetBinaryPath, electronBinaryPath);325 writeDevelopmentLauncherScript(launcherBinaryPath, runtimeElectronBinaryPath);
310 }326 }
311 registerMacLauncherBundle(targetAppBundlePath);327 registerMacLauncherBundle(targetAppBundlePath);
312 return targetBinaryPath;328 return launcherBinaryPath;
313 }329 }
314330
315 NodeFS.rmSync(targetAppBundlePath, { recursive: true, force: true });331 NodeFS.rmSync(targetAppBundlePath, { recursive: true, force: true });
...@@ -321,15 +337,24 @@ function buildMacLauncher(electronBinaryPath) {...@@ -321,15 +337,24 @@ function buildMacLauncher(electronBinaryPath) {
321 recursive: true,337 recursive: true,
322 verbatimSymlinks: true,338 verbatimSymlinks: true,
323 });339 });
324 patchMainBundleInfoPlist(targetAppBundlePath, iconPath);340 patchMainBundleInfoPlist(
341 targetAppBundlePath,
342 iconPath,
343 isDevelopment ? developmentPaths.launcherExecutableName : "Electron",
344 );
325 patchHelperBundleInfoPlists(targetAppBundlePath);345 patchHelperBundleInfoPlists(targetAppBundlePath);
326 if (isDevelopment) {346 if (isDevelopment) {
327 writeDevelopmentLauncherScript(targetBinaryPath, electronBinaryPath);347 // Keep Electron's native executable inside the branded bundle. Launching the
348 // node_modules copy makes macOS associate the process (and Dock label) with
349 // Electron.app even though this bundle's Info.plist has the T3 Code name.
350 // Its conventional executable name also keeps Electron's default-app runtime
351 // in development mode instead of making app.isPackaged report true.
352 writeDevelopmentLauncherScript(launcherBinaryPath, runtimeElectronBinaryPath);
328 }353 }
329 NodeFS.writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`);354 NodeFS.writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`);
330 registerMacLauncherBundle(targetAppBundlePath);355 registerMacLauncherBundle(targetAppBundlePath);
331356
332 return targetBinaryPath;357 return launcherBinaryPath;
333}358}
334359
335function isLinuxSetuidSandboxConfigured(electronBinaryPath) {360function isLinuxSetuidSandboxConfigured(electronBinaryPath) {
apps/desktop/scripts/electron-launcher.test.mjs+34-1
...@@ -1,6 +1,10 @@...@@ -1,6 +1,10 @@
1import { assert, describe, it } from "vite-plus/test";1import { assert, describe, it } from "vite-plus/test";
22
3import { makeDevelopmentLauncherScript, resolveElectronBinaryPath } from "./electron-launcher.mjs";3import {
4 makeDevelopmentLauncherScript,
5 resolveElectronBinaryPath,
6 resolveMacLauncherPaths,
7} from "./electron-launcher.mjs";
48
5describe("electron development launcher", () => {9describe("electron development launcher", () => {
6 it("uses captured values only as fallbacks for a live runner environment", () => {10 it("uses captured values only as fallbacks for a live runner environment", () => {
...@@ -45,4 +49,33 @@ describe("electron development launcher", () => {...@@ -45,4 +49,33 @@ describe("electron development launcher", () => {
45 );49 );
46 assert.deepEqual(calls, ["ensure", "require:electron"]);50 assert.deepEqual(calls, ["ensure", "require:electron"]);
47 });51 });
52
53 it("keeps the native Electron executable name inside the branded macOS bundle", () => {
54 const paths = resolveMacLauncherPaths(
55 "/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app",
56 "T3 Code (Dev)",
57 );
58
59 assert.equal(paths.launcherExecutableName, "T3 Code (Dev) Launcher");
60 assert.equal(
61 paths.launcherBinaryPath,
62 "/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/T3 Code (Dev) Launcher",
63 );
64 assert.equal(
65 paths.runtimeElectronBinaryPath,
66 "/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/Electron",
67 );
68
69 const script = makeDevelopmentLauncherScript({
70 electronBinaryPath: paths.runtimeElectronBinaryPath,
71 mainEntryPath: "/repo/apps/desktop/dist-electron/main.cjs",
72 desktopRoot: "/repo/apps/desktop",
73 environment: {},
74 });
75 assert.include(
76 script,
77 "exec '/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/Electron'",
78 );
79 assert.notInclude(script, "node_modules/electron");
80 });
48});81});