This commit is contained in:
+116
-147
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user