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
2020 ? `com.t3tools.t3code.dev.${devBundleIdSuffix || "local"}`
2121 : "com.t3tools.t3code";
2222const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"];
23const LAUNCHER_VERSION = 12;
23const LAUNCHER_VERSION = 14;
2424const defaultIconPath = NodePath.join(desktopDir, "resources", "icon.icns");
2525const developmentMacIconPngPath = NodePath.join(
2626 repoRoot,
......@@ -220,11 +220,12 @@ function ensureDevelopmentIconIcns(runtimeDir) {
220220 }
221221}
222222
223function patchMainBundleInfoPlist(appBundlePath, iconPath) {
223function patchMainBundleInfoPlist(appBundlePath, iconPath, executableName) {
224224 const infoPlistPath = NodePath.join(appBundlePath, "Contents", "Info.plist");
225225 setPlistString(infoPlistPath, "CFBundleDisplayName", APP_DISPLAY_NAME);
226226 setPlistString(infoPlistPath, "CFBundleName", APP_DISPLAY_NAME);
227227 setPlistString(infoPlistPath, "CFBundleIdentifier", APP_BUNDLE_ID);
228 setPlistString(infoPlistPath, "CFBundleExecutable", executableName);
228229 setPlistString(infoPlistPath, "CFBundleIconFile", "icon.icns");
229230 setPlistJson(infoPlistPath, "CFBundleURLTypes", [
230231 {
......@@ -277,11 +278,25 @@ function readJson(path) {
277278 }
278279}
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
280291function buildMacLauncher(electronBinaryPath) {
281292 const sourceAppBundlePath = NodePath.resolve(NodePath.dirname(electronBinaryPath), "../..");
282293 const runtimeDir = NodePath.join(desktopDir, ".electron-runtime");
283294 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;
285300 const iconPath = isDevelopment ? ensureDevelopmentIconIcns(runtimeDir) : defaultIconPath;
286301 const metadataPath = NodePath.join(runtimeDir, "metadata.json");
287302
......@@ -298,7 +313,8 @@ function buildMacLauncher(electronBinaryPath) {
298313
299314 const currentMetadata = readJson(metadataPath);
300315 if (
301 NodeFS.existsSync(targetBinaryPath) &&
316 NodeFS.existsSync(launcherBinaryPath) &&
317 (!isDevelopment || NodeFS.existsSync(runtimeElectronBinaryPath)) &&
302318 currentMetadata &&
303319 JSON.stringify(currentMetadata) === JSON.stringify(expectedMetadata)
304320 ) {
......@@ -306,10 +322,10 @@ function buildMacLauncher(electronBinaryPath) {
306322 // The launcher also handles protocol activations outside the dev runner,
307323 // so refresh its fallback environment on every launch. Never let a value
308324 // captured by an older parent app override the live dev-runner environment.
309 writeDevelopmentLauncherScript(targetBinaryPath, electronBinaryPath);
325 writeDevelopmentLauncherScript(launcherBinaryPath, runtimeElectronBinaryPath);
310326 }
311327 registerMacLauncherBundle(targetAppBundlePath);
312 return targetBinaryPath;
328 return launcherBinaryPath;
313329 }
314330
315331 NodeFS.rmSync(targetAppBundlePath, { recursive: true, force: true });
......@@ -321,15 +337,24 @@ function buildMacLauncher(electronBinaryPath) {
321337 recursive: true,
322338 verbatimSymlinks: true,
323339 });
324 patchMainBundleInfoPlist(targetAppBundlePath, iconPath);
340 patchMainBundleInfoPlist(
341 targetAppBundlePath,
342 iconPath,
343 isDevelopment ? developmentPaths.launcherExecutableName : "Electron",
344 );
325345 patchHelperBundleInfoPlists(targetAppBundlePath);
326346 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);
328353 }
329354 NodeFS.writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`);
330355 registerMacLauncherBundle(targetAppBundlePath);
331356
332 return targetBinaryPath;
357 return launcherBinaryPath;
333358}
334359
335360function isLinuxSetuidSandboxConfigured(electronBinaryPath) {
apps/desktop/scripts/electron-launcher.test.mjs+34-1
......@@ -1,6 +1,10 @@
11import { 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
59describe("electron development launcher", () => {
610 it("uses captured values only as fallbacks for a live runner environment", () => {
......@@ -45,4 +49,33 @@ describe("electron development launcher", () => {
4549 );
4650 assert.deepEqual(calls, ["ensure", "require:electron"]);
4751 });
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 });
4881});