seperate bundle for windows
CI / Build & Test (push) Successful in 3m19s

This commit is contained in:
Raven Scott
2026-03-07 02:38:03 -05:00
parent 586a32cc49
commit a07f273e05
+116 -147
View File
@@ -99,6 +99,48 @@ function getPlatformModule(host) {
}
/**
* Return a new bundle with all keys normalized to Windows path form (backslash).
* Used for the Windows-only bundle so the runtime finds content without path variants.
*/
function normalizeBundleKeysToWindows(bundle) {
const Bundle = bundle.constructor
const next = new Bundle()
next._id = bundle._id
const keyMap = {} // oldKey -> newKey
for (const key of bundle.keys()) {
const newKey = key.replace(/\//g, '\\')
keyMap[key] = newKey
const content = bundle.read(key)
const mode = bundle.mode(key)
const opts = { mode }
if (key === bundle.main) opts.main = true
if (bundle.addons && bundle.addons.includes(key)) opts.addon = true
if (bundle.assets && bundle.assets.includes(key)) opts.asset = true
const res = bundle.resolutions && bundle.resolutions[key]
if (res) opts.imports = transformResolutionKeys(res, keyMap)
next.write(newKey, content, opts)
}
for (const [alias, key] of Object.entries(bundle.imports || {})) {
next._imports[alias] = keyMap[key] ?? key.replace(/\//g, '\\')
}
return next
}
function transformResolutionKeys(obj, keyMap) {
if (typeof obj === 'string') {
return keyMap[obj] ?? obj.replace(/\//g, '\\')
}
if (obj && typeof obj === 'object' && !Buffer.isBuffer(obj)) {
const out = {}
for (const [k, v] of Object.entries(obj)) {
out[k] = transformResolutionKeys(v, keyMap)
}
return out
}
return obj
}
/**
* Patch bare-build's apple/sign.js to skip codesign when not on macOS.
* bare-build always calls `codesign --sign -` (ad-hoc) even when sign:false,
@@ -181,19 +223,10 @@ function patchBundle(bundle, hosts) {
const prebuildData = fs.readFileSync(diskPath)
bundle.write(prebuiltKey, prebuildData, { addon: true })
// On Windows the runtime may look up addon paths with backslashes; write the same
// addon under the backslash key so require.addon() can find it.
if (host.startsWith('win32')) {
const winKey = prebuiltKey.replace(/\//g, '\\')
if (winKey !== prebuiltKey) bundle.write(winKey, prebuildData, { addon: true })
}
// Windows bundle uses normalizeBundleKeysToWindows so addon lives under backslash key there.
const [platform, arch] = host.split('-')
if (!addonResolutions.addon.bare.node[platform]) addonResolutions.addon.bare.node[platform] = {}
// Use forward-slash key for all platforms so the runtime finds the addon when
// the bundle normalizes paths (Windows runtimes often use forward slashes for
// virtual paths). We still write the addon under the backslash key for win32
// so a runtime that looks up with backslashes can find it.
addonResolutions.addon.bare.node[platform][arch] = prebuiltKey
}
@@ -208,21 +241,12 @@ function patchBundle(bundle, hosts) {
}
}
)
// Ensure tt-native package.json has content in the bundle. On Windows the runtime
// can end up reading empty content for this key (path/bundle handling), causing
// "Unexpected end of JSON input" when the .json loader runs. Explicitly write it
// under all key variants (with/without leading slash, and backslash for win32).
// Ensure tt-native package.json has content in the bundle (avoids empty read / JSON parse errors).
const pkgPath = path.join(NATIVE_HOST_DIR, 'node_modules', 'tt-native', 'package.json')
if (fs.existsSync(pkgPath)) {
const pkgContent = fs.readFileSync(pkgPath)
bundle.write(ttNativePkgKey, pkgContent)
bundle.write(ttNativePkgKey.startsWith('/') ? ttNativePkgKey.slice(1) : '/' + ttNativePkgKey, pkgContent)
if (hosts.some((h) => h.startsWith('win32'))) {
const winPkgKey = ttNativePkgKey.replace(/\//g, '\\')
bundle.write(winPkgKey, pkgContent)
bundle.write(winPkgKey.replace(/^\\+/, ''), pkgContent)
if (!winPkgKey.startsWith('\\')) bundle.write('\\' + winPkgKey, pkgContent)
}
}
console.log(' Patched tt-native binding (host-specific resolutions)')
}
@@ -288,15 +312,12 @@ module.exports = EventEmitter;
console.log(' Patched events module for node-rdpjs-2 compatibility')
}
// ── Fix 3: Windows JSON empty-content and path normalization ──────────────────
// On Windows the runtime can request .json keys with backslashes or get empty
// content for some keys. Ensure every .json entry has valid content and add
// all backslash path variants for win32 so bundle.read() finds content
// regardless of how the runtime normalizes the key.
const hasWin32 = hosts.some((h) => h.startsWith('win32'))
// ── Fix 3: JSON empty-content and runtime.bundle pathname ─────────────────────
// Ensure every .json entry has valid content; refill from disk when empty/invalid.
// Add keys with the "runtime.bundle" prefix so the embedded runtime finds content
// (e.g. bare:/runtime.bundle/node_modules/...). Windows bundle gets key normalization
// after this so it will have runtime.bundle\... backslash keys.
let jsonFixed = 0
// Resolve a normalized key to a disk path. Tries native-host first; on CI/Linux
// deps may be hoisted to repo root, so fall back to ROOT for node_modules paths.
function resolveJsonDiskPath(keyNorm) {
const inNativeHost = path.join(NATIVE_HOST_DIR, keyNorm)
if (fs.existsSync(inNativeHost)) return inNativeHost
@@ -310,7 +331,6 @@ module.exports = EventEmitter;
for (const key of keysToProcess) {
if (!key.endsWith('.json')) continue
let content = bundle.read(key)
// Try alternate key (with/without leading slash) in case empty due to key mismatch
if (!content || content.length === 0) {
const altKey = key.startsWith('/') ? key.slice(1) : '/' + key.replace(/^\/+/, '')
content = bundle.read(altKey)
@@ -327,54 +347,14 @@ module.exports = EventEmitter;
jsonFixed++
}
}
if (hasWin32 && content && content.length > 0) {
const winKey = key.replace(/\//g, '\\')
if (winKey !== key) {
bundle.write(winKey, content)
// Also write no-leading-backslash and leading-backslash variants so lookup
// finds content whether the runtime uses "node_modules\..." or "\node_modules\..."
const winKeyNoLead = winKey.replace(/^\\+/, '')
if (winKeyNoLead !== winKey) bundle.write(winKeyNoLead, content)
const winKeyWithLead = winKey.startsWith('\\') ? winKey : '\\' + winKey
if (winKeyWithLead !== winKey) bundle.write(winKeyWithLead, content)
} else {
// Root-level .json (e.g. "package.json"): add Windows path variants so the
// runtime finds content when it requests "\package.json" or "\\package.json"
const rootWinVariants = ['\\' + key, '\\\\' + key]
for (const v of rootWinVariants) {
if (v !== key) bundle.write(v, content)
}
}
// Real Windows (unlike Wine) may use case-insensitive or normalized path keys.
// Write lowercase variant so lookup finds content regardless of case.
const keyLower = key.toLowerCase()
if (keyLower !== key) bundle.write(keyLower, content)
const winKeyLower = winKey.toLowerCase()
if (winKeyLower !== winKey && winKeyLower !== key) bundle.write(winKeyLower, content)
// Relative-style keys (./package.json, .\node_modules\...)
const relForward = key.startsWith('/') ? '.' + key : './' + key.replace(/^\/+/, '')
if (relForward !== key) bundle.write(relForward, content)
const relBack = relForward.replace(/\//g, '\\')
if (relBack !== key && relBack !== winKey) bundle.write(relBack, content)
}
// The embedded runtime looks up by URL pathname (e.g. bare:/runtime.bundle/node_modules/...).
// Add keys with the "runtime.bundle" prefix so protocol.read(url) finds content on all hosts.
if (content && content.length > 0) {
const keyNoLead = key.replace(/^\/+/, '')
const prefixSlash = 'runtime.bundle/' + keyNoLead
const prefixLead = '/runtime.bundle/' + keyNoLead
if (prefixSlash !== key) bundle.write(prefixSlash, content)
if (prefixLead !== key) bundle.write(prefixLead, content)
if (hasWin32) {
const prefixBack = 'runtime.bundle\\' + keyNoLead.replace(/\//g, '\\')
const prefixBackLead = '\\runtime.bundle\\' + keyNoLead.replace(/\//g, '\\')
bundle.write(prefixBack, content)
bundle.write(prefixBackLead, content)
}
}
}
// Verification pass: ensure every .json key in the bundle has valid JSON content
// (catches keys we added or any the runtime might use with different normalization)
keysToProcess = typeof bundle.keys === 'function' ? [...bundle.keys()] : Object.keys(bundle.files || {})
const missingJson = []
for (const key of keysToProcess) {
@@ -382,42 +362,33 @@ module.exports = EventEmitter;
let content = bundle.read(key)
const valid = content && content.length > 0 && (() => { try { JSON.parse(content.toString()); return true } catch (_) { return false } })()
if (!valid) {
// Normalize key to disk path: strip leading . and slashes, then use path.sep
const keyNorm = key.replace(/^\.?[/\\]+/, '').replace(/^[/\\]+/, '').replace(/[/\\]/g, path.sep)
const diskPath = resolveJsonDiskPath(keyNorm)
if (diskPath) {
content = fs.readFileSync(diskPath)
bundle.write(key, content)
jsonFixed++
} else if (hasWin32) {
} else {
missingJson.push(key)
}
}
}
if (missingJson.length > 0) {
console.warn(' Warning: could not refill .json from disk (Windows exe may fail at runtime):', missingJson.slice(0, 5).join(', ') + (missingJson.length > 5 ? ' ...' : ''))
console.warn(' Warning: could not refill .json from disk:', missingJson.slice(0, 5).join(', ') + (missingJson.length > 5 ? ' ...' : ''))
}
// Ensure every .json key with content also exists under runtime.bundle pathname (used by embedded runtime).
keysToProcess = typeof bundle.keys === 'function' ? [...bundle.keys()] : Object.keys(bundle.files || {})
for (const key of keysToProcess) {
if (!key.endsWith('.json')) continue
const content = bundle.read(key)
if (!content || content.length === 0) continue
const keyNoLead = key.replace(/^[/\\]+/, '')
if (keyNoLead.startsWith('runtime.bundle')) continue // already prefixed
if (keyNoLead.startsWith('runtime.bundle')) continue
const prefixSlash = 'runtime.bundle/' + keyNoLead.replace(/\\/g, '/')
const prefixLead = '/runtime.bundle/' + keyNoLead.replace(/\\/g, '/')
if (prefixSlash !== key) bundle.write(prefixSlash, content)
if (prefixLead !== key) bundle.write(prefixLead, content)
if (hasWin32) {
const prefixBack = 'runtime.bundle\\' + keyNoLead.replace(/\//g, '\\')
const prefixBackLead = '\\runtime.bundle\\' + keyNoLead.replace(/\//g, '\\')
bundle.write(prefixBack, content)
bundle.write(prefixBackLead, content)
}
}
if (jsonFixed > 0) console.log(` Patched ${jsonFixed} empty/invalid .json entries`)
if (hasWin32 && keysToProcess.some((k) => k.endsWith('.json'))) console.log(' Added Windows backslash path variants for .json entries')
console.log(' Added runtime.bundle pathname variants for .json entries')
}
@@ -445,81 +416,79 @@ async function build(hosts, doPackage) {
console.log(`Entry: ${ENTRY}`)
console.log(`Output: ${RELEASES_DIR}\n`)
// Bundle the module graph with bare-pack.
// - `linked: false` so native addon prebuilds are embedded in the binary
// - `builtins` marks optional try/catch deps as provided by runtime (skipped)
// - The `imports` field in native-host/package.json maps Node.js built-ins
// to their Bare equivalents (bare-node-* wrappers)
console.log(' Bundling module graph...')
let bundle = await pack(
pathToFileURL(ENTRY),
{
hosts,
linked: false,
resolve: traverse.resolve.bare,
builtins: BUILTINS
},
readModule,
listPrefix
)
bundle = bundle.unmount(pathToFileURL(NATIVE_HOST_DIR + '/'))
// Patch the bundle to fix two issues that bare-pack cannot handle automatically:
//
// 1. tt-native uses `require('load-addon')(__dirname)` which calls
// `require.addon(referrer)` with a non-default specifier. bare-pack cannot
// statically detect this pattern, so the addon prebuild is never added to
// bundle.addons and bare-unpack never extracts it to disk.
// Fix: replace tt-native/binding.js with `module.exports = require.addon()`
// and set its resolutions map to a host-specific nested map (addon → bare →
// node → platform → arch) so the runtime selects the correct prebuild per platform.
//
// 2. node-rdpjs-2 uses `util.inherits(Class, EventEmitter)` which calls
// EventEmitter.call(this) — this fails on bare-events' ES6 class.
// Fix: replace the `events` module in the bundle with a shim that wraps the
// ES6 class so it can be called as a plain function too.
patchBundle(bundle, hosts)
bundle.id = bundleId(bundle).toString('hex')
const bundleSize = bundle.toBuffer().length
console.log(` Bundle size: ${(bundleSize / 1024 / 1024).toFixed(1)} MB`)
// Group hosts by platform module (apple handles both darwin-arm64 + darwin-x64 together)
const groups = new Map()
for (const host of hosts) {
const platform = getPlatformModule(host)
if (!groups.has(platform)) groups.set(platform, [])
groups.get(platform).push(host)
}
const unixHosts = hosts.filter((h) => !h.startsWith('win32'))
const winHosts = hosts.filter((h) => h.startsWith('win32'))
const hasWindows = winHosts.length > 0
const hasUnix = unixHosts.length > 0
const built = []
const builtEntries = []
const platformLabels = new Map()
platformLabels.set(getPlatformModule('darwin-arm64'), 'Apple (darwin)')
platformLabels.set(getPlatformModule('linux-arm64'), 'Linux')
platformLabels.set(getPlatformModule('win32-x64'), 'Windows')
for (const [platform, platformHosts] of groups) {
const label = platformLabels.get(platform) || 'Unknown'
console.log(` Building ${label} (${platformHosts.join(', ')})...`)
let count = 0
for await (const file of platform(NATIVE_HOST_DIR, bundle, null, {
name: 'holesail-browser-host',
version: pkg.version,
description: pkg.description,
hosts: platformHosts,
out: RELEASES_DIR,
standalone: true
})) {
console.log(' Built:', path.relative(ROOT, file))
built.push(file)
builtEntries.push({ file, platformHosts })
count++
async function buildAndEmit(bundleHosts, platformHostsList, normalizeForWindows) {
if (bundleHosts.length === 0) return
console.log(' Bundling module graph' + (normalizeForWindows ? ' (Windows bundle)' : '') + '...')
let bundle = await pack(
pathToFileURL(ENTRY),
{
hosts: bundleHosts,
linked: false,
resolve: traverse.resolve.bare,
builtins: BUILTINS
},
readModule,
listPrefix
)
bundle = bundle.unmount(pathToFileURL(NATIVE_HOST_DIR + '/'))
patchBundle(bundle, bundleHosts)
if (normalizeForWindows) {
bundle = normalizeBundleKeysToWindows(bundle)
console.log(' Normalized bundle keys to Windows path form')
}
console.log(` -> ${count} artifact(s)`)
bundle.id = bundleId(bundle).toString('hex')
const bundleSize = bundle.toBuffer().length
console.log(` Bundle size: ${(bundleSize / 1024 / 1024).toFixed(1)} MB`)
const groups = new Map()
for (const h of platformHostsList) {
const platform = getPlatformModule(h)
if (!groups.has(platform)) groups.set(platform, [])
groups.get(platform).push(h)
}
for (const [platform, platformHosts] of groups) {
const label = platformLabels.get(platform) || 'Unknown'
console.log(` Building ${label} (${platformHosts.join(', ')})...`)
let count = 0
for await (const file of platform(NATIVE_HOST_DIR, bundle, null, {
name: 'holesail-browser-host',
version: pkg.version,
description: pkg.description,
hosts: platformHosts,
out: RELEASES_DIR,
standalone: true
})) {
console.log(' Built:', path.relative(ROOT, file))
built.push(file)
builtEntries.push({ file, platformHosts })
count++
}
console.log(` -> ${count} artifact(s)`)
}
}
if (hasWindows && hasUnix) {
// Two bundles: Unix-only and Windows-only
await buildAndEmit(unixHosts, unixHosts, false)
await buildAndEmit(winHosts, winHosts, true)
} else if (hasWindows) {
// Windows-only build
await buildAndEmit(winHosts, winHosts, true)
} else {
// Unix-only (single bundle)
await buildAndEmit(hosts, hosts, false)
}
if (doPackage) {