mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
fix(desktop): preserve node-pty helper in packaged app (#65611)
Guard staged node-pty ASAR path rewrites so already-unpacked paths are not rewritten twice. Normalize spawn-helper to mode 0755 in both the prebuild and locally compiled build/Release staging paths. Add behavioral coverage for both unpacked path forms and both helper layouts. Co-authored-by: zhouwei <zwcf5200@163.com> Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
This commit is contained in:
parent
59787b9ada
commit
7d8c499893
2 changed files with 109 additions and 4 deletions
|
|
@ -19,7 +19,8 @@ import {
|
|||
mkdirSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
rmSync
|
||||
rmSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { isMain } from './utils.mjs'
|
||||
|
|
@ -28,6 +29,30 @@ const here = dirname(fileURLToPath(import.meta.url))
|
|||
const projectRoot = resolve(here, '..')
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
function makeExecutable(filePath) {
|
||||
chmodSync(filePath, 0o755)
|
||||
}
|
||||
|
||||
function patchUnixTerminalAsarPaths(destRoot) {
|
||||
const filePath = join(destRoot, 'lib', 'unixTerminal.js')
|
||||
if (!existsSync(filePath)) return
|
||||
|
||||
const source = readFileSync(filePath, 'utf8')
|
||||
const patched = source
|
||||
.replace(
|
||||
"helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');",
|
||||
"helperPath = helperPath.replace(/app\\.asar(?!\\.unpacked)/, 'app.asar.unpacked');"
|
||||
)
|
||||
.replace(
|
||||
"helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');",
|
||||
"helperPath = helperPath.replace(/node_modules\\.asar(?!\\.unpacked)/, 'node_modules.asar.unpacked');"
|
||||
)
|
||||
|
||||
if (patched !== source) {
|
||||
writeFileSync(filePath, patched)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate node-pty's package root via real module resolution, so this
|
||||
* works whether it's hoisted to a workspace root or local to this app.
|
||||
|
|
@ -75,7 +100,11 @@ function copyBuildRelease(srcDir, destDir) {
|
|||
continue
|
||||
}
|
||||
if (entry.name === 'spawn-helper' || /\.(node|dll|exe)$/.test(entry.name)) {
|
||||
cpSync(join(srcDir, entry.name), join(destDir, entry.name))
|
||||
const destFile = join(destDir, entry.name)
|
||||
cpSync(join(srcDir, entry.name), destFile)
|
||||
if (entry.name === 'spawn-helper') {
|
||||
makeExecutable(destFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -220,6 +249,7 @@ export function stageNodePtyInto(srcRoot, destRoot, { platform = process.platfor
|
|||
|
||||
// lib/**/*.js — the JS surface node-pty's `main` points into.
|
||||
copyGlobByExt(join(srcRoot, 'lib'), join(destRoot, 'lib'), ['.js'])
|
||||
patchUnixTerminalAsarPaths(destRoot)
|
||||
|
||||
// prebuilds/<platform>-<arch>/* — the prebuild-install payload for the
|
||||
// *target* we're packaging, not necessarily the host running this script.
|
||||
|
|
@ -239,8 +269,9 @@ export function stageNodePtyInto(srcRoot, destRoot, { platform = process.platfor
|
|||
continue
|
||||
}
|
||||
if (entry.name === 'spawn-helper') {
|
||||
cpSync(join(prebuildDir, entry.name), join(destPrebuild, entry.name))
|
||||
chmodSync(join(destPrebuild, entry.name), 0o775)
|
||||
const destFile = join(destPrebuild, entry.name)
|
||||
cpSync(join(prebuildDir, entry.name), destFile)
|
||||
makeExecutable(destFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import assert from 'node:assert/strict'
|
|||
import fs, { existsSync } from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { test } from 'vitest'
|
||||
|
||||
import {
|
||||
|
|
@ -43,6 +44,19 @@ function makeFakeNodePty(srcRoot, { prebuildPlatform, prebuildArch } = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
function makeFakeUnixTerminal(srcRoot) {
|
||||
fs.writeFileSync(
|
||||
join(srcRoot, 'lib', 'unixTerminal.js'),
|
||||
[
|
||||
"exports.resolveHelper = function (helperPath) {",
|
||||
" helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');",
|
||||
" helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');",
|
||||
' return helperPath;',
|
||||
'};'
|
||||
].join('\n')
|
||||
)
|
||||
}
|
||||
|
||||
// ─── classifyNativeBinary tests ─────────────────────────────────────
|
||||
|
||||
test('classifyNativeBinary detects ELF as linux', () => {
|
||||
|
|
@ -262,6 +276,66 @@ test('host-target: host build/Release IS staged for a matching target', () => {
|
|||
}
|
||||
})
|
||||
|
||||
test.skipIf(process.platform === 'win32')(
|
||||
'host-target: staged node-pty resolves an already-unpacked helper and preserves executable helpers',
|
||||
async () => {
|
||||
const tmp = fs.mkdtempSync(join(os.tmpdir(), 'hermes-stage-'))
|
||||
try {
|
||||
const srcRoot = join(tmp, 'node-pty')
|
||||
const destRoot = join(tmp, 'dest')
|
||||
const prebuildDir = join(srcRoot, 'prebuilds', `${process.platform}-${process.arch}`)
|
||||
const buildReleaseDir = join(srcRoot, 'build', 'Release')
|
||||
|
||||
makeFakeNodePty(srcRoot, {
|
||||
prebuildPlatform: process.platform,
|
||||
prebuildArch: process.arch
|
||||
})
|
||||
makeFakeUnixTerminal(srcRoot)
|
||||
makeFakeNode(join(buildReleaseDir, 'pty.node'), process.platform)
|
||||
fs.writeFileSync(join(prebuildDir, 'spawn-helper'), 'prebuild helper')
|
||||
fs.writeFileSync(join(buildReleaseDir, 'spawn-helper'), 'build helper')
|
||||
fs.chmodSync(join(prebuildDir, 'spawn-helper'), 0o644)
|
||||
fs.chmodSync(join(buildReleaseDir, 'spawn-helper'), 0o644)
|
||||
|
||||
stageNodePtyInto(srcRoot, destRoot, { platform: process.platform, arch: process.arch })
|
||||
|
||||
const stagedUnixTerminalUrl = pathToFileURL(join(destRoot, 'lib', 'unixTerminal.js'))
|
||||
stagedUnixTerminalUrl.searchParams.set('t', String(Date.now()))
|
||||
const stagedUnixTerminal = await import(stagedUnixTerminalUrl.href)
|
||||
const unpackedHelper = join(
|
||||
tmp,
|
||||
'Hermes.app',
|
||||
'Contents',
|
||||
'Resources',
|
||||
'app.asar.unpacked',
|
||||
'dist',
|
||||
'node_modules',
|
||||
'node-pty',
|
||||
'prebuilds',
|
||||
`${process.platform}-${process.arch}`,
|
||||
'spawn-helper'
|
||||
)
|
||||
const nodeModulesUnpackedHelper = unpackedHelper.replace(
|
||||
`${path.sep}node_modules${path.sep}`,
|
||||
`${path.sep}node_modules.asar.unpacked${path.sep}`
|
||||
)
|
||||
|
||||
assert.equal(stagedUnixTerminal.resolveHelper(unpackedHelper), unpackedHelper)
|
||||
assert.equal(
|
||||
stagedUnixTerminal.resolveHelper(nodeModulesUnpackedHelper),
|
||||
nodeModulesUnpackedHelper
|
||||
)
|
||||
assert.equal(
|
||||
fs.statSync(join(destRoot, 'prebuilds', `${process.platform}-${process.arch}`, 'spawn-helper')).mode & 0o777,
|
||||
0o755
|
||||
)
|
||||
assert.equal(fs.statSync(join(destRoot, 'build', 'Release', 'spawn-helper')).mode & 0o777, 0o755)
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
test('validation rejects a staged binary with the wrong platform magic', () => {
|
||||
const tmp = fs.mkdtempSync(join(os.tmpdir(), 'hermes-stage-'))
|
||||
try {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue