diff --git a/apps/desktop/scripts/bundle-electron-main.mjs b/apps/desktop/scripts/bundle-electron-main.mjs index b72b11b9e8..452be24395 100644 --- a/apps/desktop/scripts/bundle-electron-main.mjs +++ b/apps/desktop/scripts/bundle-electron-main.mjs @@ -56,7 +56,9 @@ export async function bundleElectronMain({ source, out, stamp, dev = false }) { } await build({ ...common, - entryPoints: [join(source, 'apps/desktop/electron/main.ts')], + // entry.ts picks the Linux ozone platform (native Wayland/WSLg) and + // relaunches before main.ts loads; Electron reads it only from argv. + entryPoints: [join(source, 'apps/desktop/electron/entry.ts')], format: 'esm', outfile: join(out, 'electron-main.mjs'), banner: { js: "import { createRequire } from 'module'; const require = createRequire(import.meta.url);" + envBanner }, diff --git a/apps/desktop/scripts/bundle-electron-main.test.mjs b/apps/desktop/scripts/bundle-electron-main.test.mjs new file mode 100644 index 0000000000..f5a95546e5 --- /dev/null +++ b/apps/desktop/scripts/bundle-electron-main.test.mjs @@ -0,0 +1,101 @@ +import { execFileSync } from 'node:child_process' +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { pathToFileURL } from 'node:url' + +import { afterAll, beforeAll, expect, test } from 'vitest' + +import { bundleElectronMain } from './bundle-electron-main.mjs' + +const repo = resolve(import.meta.dirname, '../../..') +let root +let bundle +let preload + +// Run the shipped electron-main.mjs under plain Node as if on Linux. The entry +// relaunches through process.execPath; the preload (inherited through +// NODE_OPTIONS) makes that child print its argv instead of starting the app. +beforeAll(async () => { + root = mkdtempSync(join(tmpdir(), 'hermes-electron-entry-')) + await bundleElectronMain({ source: repo, out: join(root, 'dist'), dev: true }) + bundle = join(root, 'dist/electron-main.mjs') + preload = join(root, 'preload.mjs') + writeFileSync( + preload, + ` + import { readFileSync } from 'node:fs' + import { registerHooks } from 'node:module' + Object.defineProperty(process, 'platform', { value: 'linux' }) + if (process.argv.some(arg => arg.startsWith('--ozone-platform='))) { + console.log(JSON.stringify({ relaunched: process.argv.slice(2) })) + process.exit(0) + } + // Externals the bundle leaves to Electron's node_modules. Stub each with + // every name the bundle imports from it; only app.exit is ever called. + const bundled = readFileSync(${JSON.stringify(bundle)}, 'utf8') + const stub = specifier => { + const names = new Set(specifier === 'electron' ? ['app'] : []) + for (const [, list, from] of bundled.matchAll(/import\\s*\\{([^}]*)\\}\\s*from\\s*"([^"]+)"/g)) { + if (from !== specifier) continue + for (const name of list.split(',')) names.add(name.trim().split(/\\s+as\\s+/)[0]) + } + names.delete('') + return [...names].map(name => name === 'app' + ? 'export const app = { exit: code => process.exit(code) }' + : 'export const ' + name + ' = {}').join('\\n') + '\\nexport default {}' + } + const externals = new Set(['electron', 'node-pty', 'get-windows']) + registerHooks({ + resolve: (specifier, context, next) => externals.has(specifier) + ? { url: 'hermes-stub:' + specifier, shortCircuit: true } : next(specifier, context), + load: (url, context, next) => url.startsWith('hermes-stub:') + ? { format: 'module', source: stub(url.slice('hermes-stub:'.length)), shortCircuit: true } : next(url, context), + }) + ` + ) +}, 120_000) + +afterAll(() => rmSync(root, { recursive: true, force: true })) + +function launch(env, config) { + const home = mkdtempSync(join(root, 'home-')) + + if (config) { + mkdirSync(home, { recursive: true }) + writeFileSync(join(home, 'config.yaml'), config) + } + + const out = execFileSync(process.execPath, [bundle, '.'], { + cwd: root, + encoding: 'utf8', + env: { + PATH: process.env.PATH, + HERMES_HOME: home, + NODE_OPTIONS: `--import=${pathToFileURL(preload).href}`, + ...env + } + }) + + return JSON.parse(out.trim().split('\n').at(-1)) +} + +test('the bundled entry relaunches native Wayland sessions with the wayland ozone platform', () => { + expect(launch({ XDG_SESSION_TYPE: 'wayland', WAYLAND_DISPLAY: 'wayland-0', DISPLAY: ':0' })).toEqual({ + relaunched: ['.', '--ozone-platform=wayland'] + }) +}) + +test('the bundled entry keeps the WSLg Wayland relaunch', () => { + expect(launch({ WSL_DISTRO_NAME: 'Ubuntu', WAYLAND_DISPLAY: 'wayland-0', DISPLAY: ':0' })).toEqual({ + relaunched: ['.', '--ozone-platform=wayland'] + }) +}) + +test('the bundled entry lets desktop.electron_flags choose the ozone platform', () => { + const config = 'desktop:\n electron_flags:\n - --ozone-platform=x11\n' + + expect(launch({ XDG_SESSION_TYPE: 'wayland', WAYLAND_DISPLAY: 'wayland-0', DISPLAY: ':0' }, config)).toEqual({ + relaunched: ['.', '--ozone-platform=x11'] + }) +}) diff --git a/tests-js/desktop-builder.test.mjs b/tests-js/desktop-builder.test.mjs index c1b4fd1d55..08164191c5 100644 --- a/tests-js/desktop-builder.test.mjs +++ b/tests-js/desktop-builder.test.mjs @@ -32,6 +32,7 @@ function fixture() { put(join(app, 'vite.config.mjs'), 'export default { base: "./", build: { minify: false } }') put(join(app, 'index.html'), '
') put(join(app, 'src/index.js'), 'document.getElementById("app").textContent = "built renderer"') + put(join(app, 'electron/entry.ts'), "await import('./main')") put(join(app, 'electron/main.ts'), 'console.log(JSON.stringify({ stamp: __HERMES_INSTALL_STAMP__, identity: __HERMES_PRODUCT_IDENTITY__ }))') put(join(app, 'electron/preload.ts'), 'globalThis.fixturePreload = "compiled preload"') put(join(app, 'electron/preview-guest-preload-entry.ts'), 'globalThis.fixtureGuestPreload = "compiled guest preload"')