Files
hermes-agent/apps/desktop/scripts/after-pack.test.mjs
ethernet 26960b1afb build(desktop): compile the layered icon to Assets.car on macOS 26 hosts
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.
2026-10-02 10:33:43 -04:00

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 })
}
})