@@ -3,10 +3,12 @@ import {
33 copyFileSync ,
44 existsSync ,
55 mkdirSync ,
6+ mkdtempSync ,
67 readFileSync ,
78 rmSync ,
89 writeFileSync ,
910} from 'node:fs' ;
11+ import { tmpdir } from 'node:os' ;
1012import { extname , join , resolve } from 'node:path' ;
1113
1214interface BrandInput {
@@ -130,10 +132,15 @@ async function run(cmd: string[], cwd: string): Promise<void> {
130132 }
131133}
132134
135+ interface BrandAssetsResult {
136+ macIcon : string ;
137+ hasAssetsCar : boolean ;
138+ }
139+
133140async function writeBrandAssets (
134141 config : BrandConfig ,
135142 desktopRoot : string ,
136- ) : Promise < string > {
143+ ) : Promise < BrandAssetsResult > {
137144 const requireFromDesktop = createRequire ( join ( desktopRoot , 'package.json' ) ) ;
138145 const sharp = requireFromDesktop ( 'sharp' ) as typeof import ( 'sharp' ) ;
139146 const electronDir = join ( desktopRoot , 'apps' , 'electron' ) ;
@@ -157,7 +164,7 @@ async function writeBrandAssets(
157164 await writePng ( join ( brandDir , 'dock.png' ) , 512 ) ;
158165 await writePng ( join ( brandDir , 'symbol.png' ) , 512 ) ;
159166
160- if ( process . platform !== 'darwin' ) return 'icon.png' ;
167+ if ( process . platform !== 'darwin' ) return { macIcon : 'icon.png' , hasAssetsCar : false } ;
161168
162169 const iconset = join ( brandDir , 'icon.iconset' ) ;
163170 rmSync ( iconset , { recursive : true , force : true } ) ;
@@ -184,7 +191,91 @@ async function writeBrandAssets(
184191 [ 'iconutil' , '-c' , 'icns' , iconset , '-o' , join ( brandDir , 'icon.icns' ) ] ,
185192 brandDir ,
186193 ) ;
187- return 'icon.icns' ;
194+
195+ const hasAssetsCar = await compileAssetsCar ( config , brandDir , writePng ) ;
196+ return { macIcon : 'icon.icns' , hasAssetsCar } ;
197+ }
198+
199+ async function compileAssetsCar (
200+ config : BrandConfig ,
201+ brandDir : string ,
202+ writePng : ( output : string , size : number ) => Promise < void > ,
203+ ) : Promise < boolean > {
204+ const xcassets = join ( brandDir , 'Assets.xcassets' ) ;
205+ const appiconset = join ( xcassets , 'AppIcon.appiconset' ) ;
206+ rmSync ( xcassets , { recursive : true , force : true } ) ;
207+ mkdirSync ( appiconset , { recursive : true } ) ;
208+
209+ writeFileSync (
210+ join ( xcassets , 'Contents.json' ) ,
211+ JSON . stringify ( { info : { author : 'xcode' , version : 1 } } ) ,
212+ ) ;
213+
214+ const entries = [
215+ { file : 'icon_16.png' , size : 16 , scale : '1x' , dims : '16x16' } ,
216+ { file : 'icon_32.png' , size : 32 , scale : '2x' , dims : '16x16' } ,
217+ { file : 'icon_32.png' , size : 32 , scale : '1x' , dims : '32x32' } ,
218+ { file : 'icon_64.png' , size : 64 , scale : '2x' , dims : '32x32' } ,
219+ { file : 'icon_128.png' , size : 128 , scale : '1x' , dims : '128x128' } ,
220+ { file : 'icon_256.png' , size : 256 , scale : '2x' , dims : '128x128' } ,
221+ { file : 'icon_256.png' , size : 256 , scale : '1x' , dims : '256x256' } ,
222+ { file : 'icon_512.png' , size : 512 , scale : '2x' , dims : '256x256' } ,
223+ { file : 'icon_512.png' , size : 512 , scale : '1x' , dims : '512x512' } ,
224+ { file : 'icon_1024.png' , size : 1024 , scale : '2x' , dims : '512x512' } ,
225+ ] ;
226+
227+ const uniqueSizes = new Set ( entries . map ( ( e ) => e . size ) ) ;
228+ for ( const size of uniqueSizes ) {
229+ await writePng ( join ( appiconset , `icon_${ size } .png` ) , size ) ;
230+ }
231+
232+ writeFileSync (
233+ join ( appiconset , 'Contents.json' ) ,
234+ JSON . stringify ( {
235+ images : entries . map ( ( e ) => ( {
236+ filename : e . file ,
237+ idiom : 'mac' ,
238+ scale : e . scale ,
239+ size : e . dims ,
240+ } ) ) ,
241+ info : { author : 'xcode' , version : 1 } ,
242+ } ) ,
243+ ) ;
244+
245+ const outDir = mkdtempSync ( join ( tmpdir ( ) , 'assets-car-' ) ) ;
246+ const partialPlist = join ( outDir , 'partial-info.plist' ) ;
247+ const proc = Bun . spawn ( {
248+ cmd : [
249+ 'xcrun' , 'actool' , xcassets ,
250+ '--compile' , outDir ,
251+ '--app-icon' , 'AppIcon' ,
252+ '--platform' , 'macosx' ,
253+ '--minimum-deployment-target' , '14.0' ,
254+ '--output-partial-info-plist' , partialPlist ,
255+ ] ,
256+ cwd : brandDir ,
257+ stdout : 'pipe' ,
258+ stderr : 'pipe' ,
259+ } ) ;
260+ const exitCode = await proc . exited ;
261+ if ( exitCode !== 0 ) {
262+ console . log ( 'Warning: actool compilation failed, skipping Assets.car' ) ;
263+ rmSync ( xcassets , { recursive : true , force : true } ) ;
264+ return false ;
265+ }
266+
267+ const compiledCar = join ( outDir , 'Assets.car' ) ;
268+ if ( ! existsSync ( compiledCar ) ) {
269+ console . log ( 'Warning: actool produced no Assets.car, skipping' ) ;
270+ rmSync ( xcassets , { recursive : true , force : true } ) ;
271+ return false ;
272+ }
273+
274+ copyFileSync ( compiledCar , join ( brandDir , 'Assets.car' ) ) ;
275+ rmSync ( xcassets , { recursive : true , force : true } ) ;
276+ rmSync ( outDir , { recursive : true , force : true } ) ;
277+ console . log ( 'Assets.car compiled successfully' ) ;
278+ return true ;
188279}
189280
190281function tsString ( value : string ) : string {
@@ -203,8 +294,11 @@ function helpMenuLinks(config: BrandConfig): string {
203294 ]` ;
204295}
205296
206- function brandBlock ( config : BrandConfig , macIcon : string ) : string {
297+ function brandBlock ( config : BrandConfig , macIcon : string , hasAssetsCar : boolean ) : string {
207298 const resourceDir = `resources/brands/${ config . brandId } ` ;
299+ const liquidGlassLine = hasAssetsCar
300+ ? `\n liquidGlassAssetsCar: ${ tsString ( `${ resourceDir } /Assets.car` ) } ,`
301+ : '' ;
208302
209303 return ` ${ tsString ( config . brandId ) } : {
210304 id: ${ tsString ( config . brandId ) } ,
@@ -223,7 +317,7 @@ function brandBlock(config: BrandConfig, macIcon: string): string {
223317 macIcon: ${ tsString ( `${ resourceDir } /${ macIcon } ` ) } ,
224318 winIcon: ${ tsString ( `${ resourceDir } /icon.png` ) } ,
225319 linuxIcon: ${ tsString ( `${ resourceDir } /icon.png` ) } ,
226- devDockIcon: ${ tsString ( `${ resourceDir } /dock.png` ) } ,
320+ devDockIcon: ${ tsString ( `${ resourceDir } /dock.png` ) } ,${ liquidGlassLine }
227321 },
228322 credits: '',
229323 creditsShort: '',
@@ -236,6 +330,7 @@ function registerBrand(
236330 config : BrandConfig ,
237331 desktopRoot : string ,
238332 macIcon : string ,
333+ hasAssetsCar : boolean ,
239334) : void {
240335 const brandingPath = join (
241336 desktopRoot ,
@@ -259,22 +354,25 @@ function registerBrand(
259354
260355 writeFileSync (
261356 brandingPath ,
262- source . replace ( marker , `\n${ brandBlock ( config , macIcon ) } ${ marker } ` ) ,
357+ source . replace ( marker , `\n${ brandBlock ( config , macIcon , hasAssetsCar ) } ${ marker } ` ) ,
263358 ) ;
264359}
265360
266361async function main ( ) : Promise < void > {
267362 const desktopRoot = desktopRootFromArgs ( ) ;
268363 const config = loadConfig ( configPathFromArgs ( ) ) ;
269- const macIcon = await writeBrandAssets ( config , desktopRoot ) ;
270- registerBrand ( config , desktopRoot , macIcon ) ;
364+ const { macIcon, hasAssetsCar } = await writeBrandAssets ( config , desktopRoot ) ;
365+ registerBrand ( config , desktopRoot , macIcon , hasAssetsCar ) ;
271366
272367 console . log ( `Created brand ${ config . brandId } ` ) ;
273368 console . log ( `App name: ${ config . appName } ` ) ;
274369 console . log ( `App ID: ${ config . appId } ` ) ;
275370 console . log (
276371 `Assets: ${ join ( desktopRoot , 'apps' , 'electron' , 'resources' , 'brands' , config . brandId ) } ` ,
277372 ) ;
373+ if ( hasAssetsCar ) {
374+ console . log ( 'Assets.car: generated (macOS 26+ Liquid Glass icon)' ) ;
375+ }
278376}
279377
280378main ( ) . catch ( ( error : unknown ) => {
0 commit comments