66 cpSync ,
77 existsSync ,
88 mkdirSync ,
9+ mkdtempSync ,
910 readFileSync ,
1011 readdirSync ,
1112 rmSync ,
@@ -25,6 +26,8 @@ const LAUNCHER_VERSION = 2;
2526
2627const __dirname = dirname ( fileURLToPath ( import . meta. url ) ) ;
2728export const desktopDir = resolve ( __dirname , ".." ) ;
29+ const repoRoot = resolve ( desktopDir , ".." , ".." ) ;
30+ const developmentMacIconPngPath = join ( repoRoot , "assets" , "dev" , "blueprint-macos-1024.png" ) ;
2831
2932function setPlistString ( plistPath , key , value ) {
3033 const replaceResult = spawnSync ( "plutil" , [ "-replace" , key , "-string" , value , plistPath ] , {
@@ -45,6 +48,68 @@ function setPlistString(plistPath, key, value) {
4548 throw new Error ( `Failed to update plist key "${ key } " at ${ plistPath } : ${ details } ` . trim ( ) ) ;
4649}
4750
51+ function runChecked ( command , args ) {
52+ const result = spawnSync ( command , args , { encoding : "utf8" } ) ;
53+ if ( result . status === 0 ) {
54+ return ;
55+ }
56+
57+ const details = [ result . stdout , result . stderr ] . filter ( Boolean ) . join ( "\n" ) ;
58+ throw new Error ( `Failed to run ${ command } ${ args . join ( " " ) } : ${ details } ` . trim ( ) ) ;
59+ }
60+
61+ function ensureDevelopmentIconIcns ( runtimeDir , fallbackIconPath ) {
62+ const generatedIconPath = join ( runtimeDir , "icon-dev.icns" ) ;
63+ mkdirSync ( runtimeDir , { recursive : true } ) ;
64+
65+ if ( ! existsSync ( developmentMacIconPngPath ) ) {
66+ return fallbackIconPath ;
67+ }
68+
69+ const sourceMtimeMs = statSync ( developmentMacIconPngPath ) . mtimeMs ;
70+ if ( existsSync ( generatedIconPath ) && statSync ( generatedIconPath ) . mtimeMs >= sourceMtimeMs ) {
71+ return generatedIconPath ;
72+ }
73+
74+ const iconsetRoot = mkdtempSync ( join ( runtimeDir , "dev-iconset-" ) ) ;
75+ const iconsetDir = join ( iconsetRoot , "icon.iconset" ) ;
76+ mkdirSync ( iconsetDir , { recursive : true } ) ;
77+
78+ try {
79+ for ( const size of [ 16 , 32 , 128 , 256 , 512 ] ) {
80+ runChecked ( "sips" , [
81+ "-z" ,
82+ String ( size ) ,
83+ String ( size ) ,
84+ developmentMacIconPngPath ,
85+ "--out" ,
86+ join ( iconsetDir , `icon_${ size } x${ size } .png` ) ,
87+ ] ) ;
88+
89+ const retinaSize = size * 2 ;
90+ runChecked ( "sips" , [
91+ "-z" ,
92+ String ( retinaSize ) ,
93+ String ( retinaSize ) ,
94+ developmentMacIconPngPath ,
95+ "--out" ,
96+ join ( iconsetDir , `icon_${ size } x${ size } @2x.png` ) ,
97+ ] ) ;
98+ }
99+
100+ runChecked ( "iconutil" , [ "-c" , "icns" , iconsetDir , "-o" , generatedIconPath ] ) ;
101+ return generatedIconPath ;
102+ } catch ( error ) {
103+ console . warn (
104+ "[desktop-launcher] Failed to generate dev macOS icon, falling back to default icon." ,
105+ error ,
106+ ) ;
107+ return fallbackIconPath ;
108+ } finally {
109+ rmSync ( iconsetRoot , { recursive : true , force : true } ) ;
110+ }
111+ }
112+
48113function patchMainBundleInfoPlist ( appBundlePath ) {
49114 const infoPlistPath = join ( appBundlePath , "Contents" , "Info.plist" ) ;
50115 setPlistString ( infoPlistPath , "CFBundleDisplayName" , APP_DISPLAY_NAME ) ;
@@ -111,10 +176,10 @@ function resolveIconSourceMetadata(desktopResourcesDir) {
111176 } ;
112177}
113178
114- async function stageMainBundleIcons ( appBundlePath , desktopResourcesDir ) {
179+ async function stageMainBundleIcons ( appBundlePath , desktopResourcesDir , legacyIconOverride ) {
115180 const resourcesDir = join ( appBundlePath , "Contents" , "Resources" ) ;
116181 const iconComposerPath = join ( desktopResourcesDir , "icon.icon" ) ;
117- const legacyIconPath = join ( desktopResourcesDir , "icon.icns" ) ;
182+ const legacyIconPath = legacyIconOverride ?? join ( desktopResourcesDir , "icon.icns" ) ;
118183
119184 if ( existsSync ( iconComposerPath ) ) {
120185 const compiled = await generateAssetCatalogForIcon ( iconComposerPath ) ;
@@ -144,10 +209,18 @@ async function buildMacLauncher(electronBinaryPath) {
144209 const targetAppBundlePath = join ( runtimeDir , `${ APP_DISPLAY_NAME } .app` ) ;
145210 const targetBinaryPath = join ( targetAppBundlePath , "Contents" , "MacOS" , "Electron" ) ;
146211 const desktopResourcesDir = join ( desktopDir , "resources" ) ;
212+ const defaultLegacyIconPath = join ( desktopResourcesDir , "icon.icns" ) ;
147213 const metadataPath = join ( runtimeDir , "metadata.json" ) ;
148214
149215 mkdirSync ( runtimeDir , { recursive : true } ) ;
150216
217+ // In dev mode, fall back to a generated icon derived from the dev blueprint PNG so dev
218+ // builds are visually distinct. If the modern icon.icon composer source is present, the
219+ // staging step below takes precedence and this override is ignored.
220+ const legacyIconOverride = isDevelopment
221+ ? ensureDevelopmentIconIcns ( runtimeDir , defaultLegacyIconPath )
222+ : undefined ;
223+
151224 const iconMetadata = resolveIconSourceMetadata ( desktopResourcesDir ) ;
152225
153226 const expectedMetadata = {
@@ -172,6 +245,7 @@ async function buildMacLauncher(electronBinaryPath) {
172245 const refreshedIconMetadata = await stageMainBundleIcons (
173246 targetAppBundlePath ,
174247 desktopResourcesDir ,
248+ legacyIconOverride ,
175249 ) ;
176250 patchHelperBundleInfoPlists ( targetAppBundlePath ) ;
177251 writeFileSync (
0 commit comments