electron-builder (app-builder-lib 27.0.0-alpha.6) compiles a `.icon` given as `mac.icon` into Contents/Resources/Assets.car and sets CFBundleIconName, but only with actool >= 26 (Xcode 26); on any other host it throws and the build fails. scripts/mac-icon.cjs picks the Icon Composer package when that actool is present and the legacy .icns otherwise, so a dev Mac on Xcode 16 keeps building the app it always built. The release runners keep Xcode 16.4 selected although 26.x is installed, so the darwin job now selects the newest Xcode 26 via DEVELOPER_DIR and asserts the actool version before packaging; a silent fallback cannot ship from CI. electron-builder's `.icon` path also replaces the bundled icon.icns with actool's 256px fallback. macOS <= 15 shows that file, so after-pack restores our 16..1024 assets/icon.icns.
113 lines
5.2 KiB
JavaScript
113 lines
5.2 KiB
JavaScript
import {
|
|
assertPackagedBackendReadyArtifact,
|
|
assertBackendReadyArtifactSourceAcceptsBothTokens,
|
|
resolvePackagedAsarPath
|
|
} from './backend-ready-artifact.mjs'
|
|
import { mkdtemp, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
import { createRequire } from 'node:module'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
import { Platform, PlatformPackager } from 'app-builder-lib'
|
|
import { expect, it, vi } from 'vitest'
|
|
|
|
const require = createRequire(import.meta.url)
|
|
const desktopRoot = path.resolve(import.meta.dirname, '..')
|
|
// The builder config is electron-builder.config.cjs (package.json carries no `build` block).
|
|
const builderConfig = require(path.join(desktopRoot, 'electron-builder.config.cjs'))
|
|
|
|
async function configuredHook(context) {
|
|
const hook = await import(new URL(`../${builderConfig.afterPack}`, import.meta.url).href)
|
|
await hook.default(context)
|
|
}
|
|
|
|
// The afterPack readiness guard reads the packaged bundle's unpacked main;
|
|
// every fixture here packs a valid dual-token matcher so the tests keep
|
|
// exercising the locale/signing paths the hook also performs.
|
|
async function seedPackagedMain(context) {
|
|
const asarPath = resolvePackagedAsarPath(context)
|
|
await mkdir(path.dirname(asarPath), { recursive: true })
|
|
await writeFile(asarPath, 'stub archive')
|
|
await mkdir(path.join(`${asarPath}.unpacked`, 'dist'), { recursive: true })
|
|
await writeFile(
|
|
path.join(`${asarPath}.unpacked`, 'dist', 'electron-main.mjs'),
|
|
'const re = /HERMES_(?:BACKEND|DASHBOARD)_READY[^\\n]*port=(\\d+)/m\n'
|
|
)
|
|
}
|
|
|
|
function context(appOutDir, productFilename = 'Hermes Preview') {
|
|
// Use electron-builder's real bundle path resolution, including branding.
|
|
const packager = Object.assign(Object.create(PlatformPackager.prototype), {
|
|
platform: Platform.MAC,
|
|
appInfo: { productFilename },
|
|
info: { framework: { distMacOsAppName: 'Electron.app' } }
|
|
})
|
|
return { appOutDir, electronPlatformName: 'darwin', packager }
|
|
}
|
|
|
|
it('restores app localizations from the filtered framework without copying locale data', async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), 'hermes-locale-pack-'))
|
|
try {
|
|
const ctx = context(root)
|
|
await seedPackagedMain(ctx)
|
|
const framework = ctx.packager.getMacOsElectronFrameworkResourcesDir(root)
|
|
const resources = ctx.packager.getResourcesDir(root)
|
|
await mkdir(resources, { recursive: true })
|
|
for (const name of ['nb.lproj', 'en_GB.lproj', 'nb_FEMININE.lproj']) {
|
|
await mkdir(path.join(framework, name), { recursive: true })
|
|
await writeFile(path.join(framework, name, 'locale.pak'), 'untouched locale data')
|
|
}
|
|
await writeFile(path.join(framework, 'not-a-directory.lproj'), 'not a locale')
|
|
await mkdir(path.join(framework, 'other'), { recursive: true })
|
|
await configuredHook(ctx)
|
|
await configuredHook(ctx)
|
|
expect((await readdir(resources)).filter(name => name.endsWith('.lproj')).sort())
|
|
.toEqual(['en_GB.lproj', 'nb.lproj'])
|
|
expect(await readdir(resources)).toContain('icon.icns')
|
|
expect(await readdir(path.join(resources, 'nb.lproj'))).toEqual([])
|
|
expect(await readFile(path.join(framework, 'nb.lproj', 'locale.pak'), 'utf8')).toBe('untouched locale data')
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('puts the full-resolution .icns back after electron-builder packaged the layered icon', async () => {
|
|
// With `mac.icon` pointing at the Icon Composer package, electron-builder
|
|
// bundles actool's 256px fallback as icon.icns; macOS <= 15 shows that file.
|
|
const root = await mkdtemp(path.join(os.tmpdir(), 'hermes-mac-icon-'))
|
|
try {
|
|
const ctx = context(root)
|
|
await seedPackagedMain(ctx)
|
|
const resources = ctx.packager.getResourcesDir(root)
|
|
await mkdir(ctx.packager.getMacOsElectronFrameworkResourcesDir(root), { recursive: true })
|
|
await mkdir(resources, { recursive: true })
|
|
await writeFile(path.join(resources, 'icon.icns'), 'actool fallback')
|
|
await configuredHook(ctx)
|
|
const restored = await readFile(path.join(resources, 'icon.icns'))
|
|
expect(restored.equals(await readFile(path.join(desktopRoot, 'assets', 'icon.icns')))).toBe(true)
|
|
expect(restored.subarray(0, 4).toString('latin1')).toBe('icns')
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('leaves Linux alone and reports a missing framework without failing packaging', async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), 'hermes-locale-pack-'))
|
|
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
try {
|
|
// win32 is not a no-op here: the same hook sanitizes and batch-signs the PE tree.
|
|
const linuxCtx = { appOutDir: root, electronPlatformName: 'linux' }
|
|
await seedPackagedMain(linuxCtx)
|
|
await configuredHook(linuxCtx)
|
|
expect(warn).not.toHaveBeenCalled()
|
|
const ctx = context(root)
|
|
await seedPackagedMain(ctx)
|
|
await configuredHook(ctx)
|
|
expect(warn).toHaveBeenCalledWith(expect.stringContaining('macOS locale markers were not restored'))
|
|
expect((await readdir(root)).sort()).toEqual(['Hermes Preview.app', 'resources'])
|
|
expect(await readdir(ctx.packager.getResourcesDir(root))).toContain('icon.icns')
|
|
} finally {
|
|
warn.mockRestore()
|
|
await rm(root, { recursive: true, force: true })
|
|
}
|
|
})
|