build(desktop): stage get-windows like node-pty
get-windows@9.3.0 (MIT, zero runtime deps on macOS/Linux) is external to the esbuild bundle and staged into dist/node_modules per target platform: the universal Swift helper on macOS, the prebuilt N-API binding on Windows (fail-closed magic-byte validation), nothing on Linux (xprop at runtime). The staged lib/windows.js is rewritten to load the binding directly so @mapbox/node-pre-gyp's tree stays out of the package.
This commit is contained in:
committed by
brooklyn!
parent
f22ae72921
commit
f463a7e8ee
@@ -104,6 +104,7 @@
|
|||||||
"emojibase-data": "16.0.3",
|
"emojibase-data": "16.0.3",
|
||||||
"fflate": "0.8.3",
|
"fflate": "0.8.3",
|
||||||
"frimousse": "0.3.0",
|
"frimousse": "0.3.0",
|
||||||
|
"get-windows": "9.3.0",
|
||||||
"hast-util-from-html-isomorphic": "2.0.0",
|
"hast-util-from-html-isomorphic": "2.0.0",
|
||||||
"hast-util-to-text": "4.0.2",
|
"hast-util-to-text": "4.0.2",
|
||||||
"ignore": "7.0.6",
|
"ignore": "7.0.6",
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
import { existsSync, rmSync, renameSync } from 'node:fs'
|
import { existsSync, rmSync, renameSync } from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { Arch } from 'electron-builder'
|
import { Arch } from 'electron-builder'
|
||||||
import { stageNodePty } from './stage-native-deps.mjs'
|
import { stageNodePty, stageGetWindows } from './stage-native-deps.mjs'
|
||||||
|
|
||||||
export function cleanStaleAppOutDir(appOutDir) {
|
export function cleanStaleAppOutDir(appOutDir) {
|
||||||
if (!appOutDir || typeof appOutDir !== 'string') {
|
if (!appOutDir || typeof appOutDir !== 'string') {
|
||||||
@@ -144,11 +144,16 @@ export default async function beforePack(context) {
|
|||||||
await stageNodePty({ platform, arch: archName })
|
await stageNodePty({ platform, arch: archName })
|
||||||
console.log(`[before-pack] re-staged node-pty for target ${platform}-${archName}`)
|
console.log(`[before-pack] re-staged node-pty for target ${platform}-${archName}`)
|
||||||
}
|
}
|
||||||
|
// get-windows' native payload is per-platform, not per-arch (the macOS
|
||||||
|
// helper is universal, Windows stages every prebuilt binding dir), so
|
||||||
|
// it re-stages for the universal target too.
|
||||||
|
stageGetWindows({ platform })
|
||||||
|
console.log(`[before-pack] re-staged get-windows for target ${platform}`)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// This one SHOULD fail the build — a missing/wrong native binary for the
|
// This one SHOULD fail the build — a missing/wrong native binary for the
|
||||||
// target arch means a broken package shipped to users, which is worse
|
// target arch means a broken package shipped to users, which is worse
|
||||||
// than a build that fails loudly here.
|
// than a build that fails loudly here.
|
||||||
throw new Error(`[before-pack] failed to stage node-pty for this target: ${err.message}`)
|
throw new Error(`[before-pack] failed to stage native deps for this target: ${err.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ const mainOut = resolve(distDir, 'electron-main.mjs')
|
|||||||
const preloadEntry = resolve(root, 'electron/preload.ts')
|
const preloadEntry = resolve(root, 'electron/preload.ts')
|
||||||
const preloadOut = resolve(distDir, 'electron-preload.js')
|
const preloadOut = resolve(distDir, 'electron-preload.js')
|
||||||
|
|
||||||
const external = ['electron', 'node-pty', 'fs']
|
const external = ['electron', 'node-pty', 'get-windows', 'fs']
|
||||||
// Production bundles bake packaged=true so unpackaged `electron .` still
|
// Production bundles bake packaged=true so unpackaged `electron .` still
|
||||||
// behaves like a packaged build. Dev bundles (`--dev`) leave the env alone
|
// behaves like a packaged build. Dev bundles (`--dev`) leave the env alone
|
||||||
// so HERMES_DESKTOP_DEV_SERVER / source-tree resolution keep working.
|
// so HERMES_DESKTOP_DEV_SERVER / source-tree resolution keep working.
|
||||||
|
|||||||
@@ -349,8 +349,142 @@ export function stageNodePty({ platform = process.platform, arch = process.arch
|
|||||||
return stageNodePtyInto(srcRoot, destRoot, { platform, arch })
|
return stageNodePtyInto(srcRoot, destRoot, { platform, arch })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── get-windows (read_window_below tool) ────────────────────────────
|
||||||
|
//
|
||||||
|
// Staged like node-pty: external to the esbuild bundle, resolved at runtime
|
||||||
|
// from dist/node_modules (which asarUnpack ships unpacked via `dist/**`).
|
||||||
|
//
|
||||||
|
// The published package's lib/windows.js statically imports
|
||||||
|
// @mapbox/node-pre-gyp — and index.js statically imports lib/windows.js on
|
||||||
|
// EVERY platform — so shipping it verbatim would drag node-pre-gyp's whole
|
||||||
|
// dependency tree into the package. pre-gyp is only used to *locate* the
|
||||||
|
// prebuilt .node we stage ourselves, so the staged copy replaces
|
||||||
|
// lib/windows.js with a resolver that requires the staged binding directly
|
||||||
|
// (and fails soft to no-op stubs, matching upstream's missing-binding
|
||||||
|
// behavior).
|
||||||
|
|
||||||
|
const STAGED_WINDOWS_JS = `// Rewritten by stage-native-deps.mjs: resolves the staged prebuilt binding
|
||||||
|
// directly instead of through @mapbox/node-pre-gyp (see stageGetWindowsInto).
|
||||||
|
import path from 'node:path';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import {fileURLToPath} from 'node:url';
|
||||||
|
import {createRequire} from 'node:module';
|
||||||
|
|
||||||
|
const getAddon = () => {
|
||||||
|
\tconst require = createRequire(import.meta.url);
|
||||||
|
\tconst bindingRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), 'binding');
|
||||||
|
|
||||||
|
\ttry {
|
||||||
|
\t\tfor (const dir of fs.readdirSync(bindingRoot)) {
|
||||||
|
\t\t\tconst bindingPath = path.join(bindingRoot, dir, 'node-get-windows.node');
|
||||||
|
\t\t\tif (fs.existsSync(bindingPath)) {
|
||||||
|
\t\t\t\treturn require(bindingPath);
|
||||||
|
\t\t\t}
|
||||||
|
\t\t}
|
||||||
|
\t} catch {}
|
||||||
|
|
||||||
|
\treturn {
|
||||||
|
\t\tgetActiveWindow() {},
|
||||||
|
\t\tgetOpenWindows() {},
|
||||||
|
\t};
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function activeWindow() {
|
||||||
|
\treturn getAddon().getActiveWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activeWindowSync() {
|
||||||
|
\treturn getAddon().getActiveWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openWindows() {
|
||||||
|
\treturn getAddon().getOpenWindows();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openWindowsSync() {
|
||||||
|
\treturn getAddon().getOpenWindows();
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
function resolveGetWindowsRoot() {
|
||||||
|
// get-windows' exports map doesn't expose ./package.json; resolve the entry
|
||||||
|
// (index.js sits at the package root) and take its directory.
|
||||||
|
const entryPath = require.resolve('get-windows', {
|
||||||
|
paths: [projectRoot]
|
||||||
|
})
|
||||||
|
return dirname(entryPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage get-windows into `destRoot` for `platform`.
|
||||||
|
*
|
||||||
|
* Per-platform native payload: macOS ships the `main` Swift helper binary
|
||||||
|
* (universal, present in every published tarball), Windows the node-pre-gyp
|
||||||
|
* prebuilt under lib/binding (downloaded by the package's install script on
|
||||||
|
* a Windows host — cross-platform packs already can't happen, see
|
||||||
|
* stageNodePtyInto), Linux nothing (it shells out to xprop at runtime).
|
||||||
|
*/
|
||||||
|
export function stageGetWindowsInto(srcRoot, destRoot, { platform = process.platform } = {}) {
|
||||||
|
rmSync(destRoot, { recursive: true, force: true })
|
||||||
|
mkdirSync(destRoot, { recursive: true })
|
||||||
|
|
||||||
|
cpSync(join(srcRoot, 'package.json'), join(destRoot, 'package.json'))
|
||||||
|
cpSync(join(srcRoot, 'index.js'), join(destRoot, 'index.js'))
|
||||||
|
copyGlobByExt(join(srcRoot, 'lib'), join(destRoot, 'lib'), ['.js'])
|
||||||
|
|
||||||
|
writeFileSync(join(destRoot, 'lib', 'windows.js'), STAGED_WINDOWS_JS)
|
||||||
|
|
||||||
|
if (platform === 'darwin') {
|
||||||
|
const helper = join(srcRoot, 'main')
|
||||||
|
if (!existsSync(helper)) {
|
||||||
|
throw new Error('[stage-native-deps] get-windows is missing its macOS helper binary (main)')
|
||||||
|
}
|
||||||
|
cpSync(helper, join(destRoot, 'main'))
|
||||||
|
makeExecutable(join(destRoot, 'main'))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform === 'win32') {
|
||||||
|
const bindingRoot = join(srcRoot, 'lib', 'binding')
|
||||||
|
const bindingDirs = existsSync(bindingRoot)
|
||||||
|
? readdirSync(bindingRoot).filter((dir) =>
|
||||||
|
existsSync(join(bindingRoot, dir, 'node-get-windows.node'))
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
if (bindingDirs.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
'[stage-native-deps] get-windows has no win32 prebuilt binding under lib/binding; ' +
|
||||||
|
'reinstall dependencies on the Windows build host.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for (const dir of bindingDirs) {
|
||||||
|
const dest = join(destRoot, 'lib', 'binding', dir)
|
||||||
|
mkdirSync(dest, { recursive: true })
|
||||||
|
const destFile = join(dest, 'node-get-windows.node')
|
||||||
|
cpSync(join(bindingRoot, dir, 'node-get-windows.node'), destFile)
|
||||||
|
const classified = classifyNativeBinary(destFile)
|
||||||
|
if (classified !== platform) {
|
||||||
|
throw new Error(
|
||||||
|
`[stage-native-deps] get-windows binding ${dir}/node-get-windows.node: ` +
|
||||||
|
`expected ${platform}, got ${classified ?? 'unknown'}. ` +
|
||||||
|
'Refusing to stage a binary compiled for the wrong platform.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[stage-native-deps] staged get-windows (${platform}) -> ${destRoot}`)
|
||||||
|
return destRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stageGetWindows({ platform = process.platform } = {}) {
|
||||||
|
const srcRoot = resolveGetWindowsRoot()
|
||||||
|
const destRoot = resolve(projectRoot, 'dist/node_modules/get-windows')
|
||||||
|
return stageGetWindowsInto(srcRoot, destRoot, { platform })
|
||||||
|
}
|
||||||
|
|
||||||
// Allow direct CLI invocation: node scripts/stage-native-deps.mjs [platform] [arch]
|
// Allow direct CLI invocation: node scripts/stage-native-deps.mjs [platform] [arch]
|
||||||
if (isMain(import.meta.url)) {
|
if (isMain(import.meta.url)) {
|
||||||
const [platform, arch] = process.argv.slice(2)
|
const [platform, arch] = process.argv.slice(2)
|
||||||
stageNodePty({ platform, arch })
|
stageNodePty({ platform, arch })
|
||||||
|
stageGetWindows({ platform })
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1675
-31
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user