mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-10 08:32:09 +00:00
fix(installer): stamp Hermes icon onto Hermes.exe via rcedit (no winCodeSign)
The unpacked Hermes.exe showed the stock Electron icon + name in the taskbar because build.win.signAndEditExecutable=false disables BOTH electron-builder's signing AND its rcedit metadata/icon stamping. That flag is load-bearing: enabling it re-triggers signtool -> winCodeSign, whose macOS symlinks crash 7-Zip on non-admin Windows (unfixable dead end). Decouple identity-stamping from signing entirely: after npm run pack, run rcedit ourselves on the produced exe. - Add rcedit as a direct devDependency of apps/desktop (the transitive electron-winstaller copy is fragile). - apps/desktop/scripts/set-exe-identity.cjs: Node helper that calls rcedit's named export to set icon + ProductName/FileDescription/ CompanyName. Node builds argv natively — avoids the PowerShell->exe ->JSON double-escaping that broke the app-builder rcedit path. - install.ps1 Set-DesktopExeIdentity invokes the script after the build, before shortcuts. Best-effort: failure keeps the stock icon, never fails the install. rcedit is a pure PE editor — no signtool, no winCodeSign, no symlinks. Verified locally: stamping a copy of the built Hermes.exe embeds the 32x32 icon and sets ProductName=Hermes. Also fix update-path success-screen flash: in update mode the installer hands off + exits in ~600ms, so don't route to the 'launch Hermes' success view (it flashed before the window closed).
This commit is contained in:
parent
aeebe1afa7
commit
25488de4ba
5 changed files with 252 additions and 8 deletions
|
|
@ -206,7 +206,13 @@ export async function initialize(): Promise<void> {
|
|||
installRoot: payload.installRoot,
|
||||
currentStage: null
|
||||
})
|
||||
$route.set('success')
|
||||
// Install: show the "launch Hermes" success screen. Update: this is a
|
||||
// hand-off — the installer relaunches the desktop and exits within a
|
||||
// few hundred ms, so routing to success just flashes that screen
|
||||
// before the window closes. Stay on progress until we exit.
|
||||
if ($mode.get() !== 'update') {
|
||||
$route.set('success')
|
||||
}
|
||||
break
|
||||
case 'failed':
|
||||
$bootstrap.set({
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@
|
|||
"globals": "^16.5.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"prettier": "^3.8.3",
|
||||
"rcedit": "^5.0.2",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.0.10",
|
||||
"vitest": "^4.1.5",
|
||||
|
|
|
|||
87
apps/desktop/scripts/set-exe-identity.cjs
Normal file
87
apps/desktop/scripts/set-exe-identity.cjs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
#!/usr/bin/env node
|
||||
// set-exe-identity.cjs — stamp the Hermes icon + version metadata onto the
|
||||
// built Hermes.exe using rcedit, completely decoupled from electron-builder's
|
||||
// signing path.
|
||||
//
|
||||
// WHY THIS EXISTS
|
||||
// ---------------
|
||||
// apps/desktop/package.json sets build.win.signAndEditExecutable=false. That
|
||||
// flag is load-bearing: turning electron-builder's own exe-editing ON also
|
||||
// re-enables its signtool step, which fetches winCodeSign-2.6.0.7z, whose
|
||||
// macOS symlinks crash 7-Zip on non-admin Windows (no Developer Mode = no
|
||||
// SeCreateSymbolicLinkPrivilege). That is an unfixable dead end — we do NOT
|
||||
// try to extract winCodeSign.
|
||||
//
|
||||
// The cost of disabling signAndEditExecutable is that electron-builder also
|
||||
// skips rcedit, so the unpacked Hermes.exe keeps the stock Electron icon and
|
||||
// "Electron" taskbar name. This script restores the icon + identity by calling
|
||||
// rcedit DIRECTLY. rcedit is a pure PE resource editor: no signing, no certs,
|
||||
// no winCodeSign, no symlinks. Invoked from install.ps1's Install-Desktop
|
||||
// after `npm run pack`.
|
||||
//
|
||||
// USAGE
|
||||
// node scripts/set-exe-identity.cjs <path-to-Hermes.exe>
|
||||
//
|
||||
// Exits 0 on success, non-zero on failure. install.ps1 treats failure as
|
||||
// non-fatal (worst case: stock icon, not a broken app).
|
||||
|
||||
const path = require('node:path')
|
||||
const fs = require('node:fs')
|
||||
|
||||
async function main() {
|
||||
const exe = process.argv[2]
|
||||
if (!exe) {
|
||||
console.error('[set-exe-identity] usage: set-exe-identity.cjs <path-to-exe>')
|
||||
process.exit(2)
|
||||
}
|
||||
if (!fs.existsSync(exe)) {
|
||||
console.error(`[set-exe-identity] target exe not found: ${exe}`)
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
// Icon lives beside this script's package root: apps/desktop/assets/icon.ico
|
||||
const desktopRoot = path.resolve(__dirname, '..')
|
||||
const icon = path.join(desktopRoot, 'assets', 'icon.ico')
|
||||
if (!fs.existsSync(icon)) {
|
||||
console.error(`[set-exe-identity] icon not found: ${icon}`)
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
// rcedit is a direct devDependency of apps/desktop, so it resolves whether
|
||||
// we're run from the desktop dir or the repo root (workspace hoist).
|
||||
// rcedit@5 exports a NAMED `rcedit` function (CommonJS: { rcedit }), not a
|
||||
// default export.
|
||||
let rcedit
|
||||
try {
|
||||
const mod = require('rcedit')
|
||||
rcedit = typeof mod === 'function' ? mod : mod.rcedit
|
||||
if (typeof rcedit !== 'function') {
|
||||
throw new Error(`unexpected rcedit export shape: ${typeof mod} keys=${Object.keys(mod)}`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[set-exe-identity] could not load rcedit module: ${err.message}`)
|
||||
process.exit(3)
|
||||
}
|
||||
|
||||
console.log(`[set-exe-identity] stamping ${exe}`)
|
||||
console.log(`[set-exe-identity] icon: ${icon}`)
|
||||
|
||||
try {
|
||||
await rcedit(exe, {
|
||||
icon,
|
||||
'version-string': {
|
||||
ProductName: 'Hermes',
|
||||
FileDescription: 'Hermes',
|
||||
CompanyName: 'Nous Research',
|
||||
LegalCopyright: 'Copyright (c) 2026 Nous Research'
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(`[set-exe-identity] rcedit failed: ${err.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('[set-exe-identity] done — Hermes icon + identity stamped')
|
||||
}
|
||||
|
||||
main()
|
||||
92
package-lock.json
generated
92
package-lock.json
generated
|
|
@ -176,6 +176,7 @@
|
|||
"globals": "^16.5.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"prettier": "^3.8.3",
|
||||
"rcedit": "^5.0.2",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.0.10",
|
||||
"vitest": "^4.1.5",
|
||||
|
|
@ -10888,6 +10889,54 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn-windows-exe": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn-windows-exe/-/cross-spawn-windows-exe-1.2.0.tgz",
|
||||
"integrity": "sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/malept"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/subscription/pkg/npm-cross-spawn-windows-exe?utm_medium=referral&utm_source=npm_fund"
|
||||
}
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@malept/cross-spawn-promise": "^1.1.0",
|
||||
"is-wsl": "^2.2.0",
|
||||
"which": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn-windows-exe/node_modules/@malept/cross-spawn-promise": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz",
|
||||
"integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/malept"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund"
|
||||
}
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
||||
|
|
@ -14928,6 +14977,22 @@
|
|||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
||||
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"is-docker": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extendable": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
||||
|
|
@ -15264,6 +15329,19 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-wsl": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-docker": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
|
||||
|
|
@ -18930,6 +19008,20 @@
|
|||
"rc": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/rcedit": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rcedit/-/rcedit-5.0.2.tgz",
|
||||
"integrity": "sha512-dgysxaeXZ4snLpPjn8aVtHvZDCx+aRcvZbaWBgl1poU6OPustMvOkj9a9ZqASQ6i5Y5szJ13LSvglEOwrmgUxA==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn-windows-exe": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
|
||||
|
|
|
|||
|
|
@ -2014,13 +2014,13 @@ function Install-Desktop {
|
|||
# launchable binary the Tauri installer can spawn.
|
||||
#
|
||||
# CSC_IDENTITY_AUTO_DISCOVERY=false tells electron-builder we are
|
||||
# NOT signing the output. This short-circuits the winCodeSign fetch +
|
||||
# extraction entirely (which fails on non-admin Windows due to a
|
||||
# macOS-symlink extraction crash electron-builder hasn't fixed in
|
||||
# years). We never had a signing cert to use, so the apparatus was
|
||||
# dead weight that broke fresh installs. The produced Hermes.exe
|
||||
# is functionally identical — just unsigned, same as it would be
|
||||
# if signing had been attempted but no cert was configured.
|
||||
# NOT signing the output. Combined with signAndEditExecutable=false in
|
||||
# apps/desktop/package.json's build.win block, electron-builder never
|
||||
# invokes signtool and therefore never fetches/extracts winCodeSign
|
||||
# (whose macOS symlinks crash 7-Zip on non-admin Windows — a dead end we
|
||||
# are NOT trying to work around). The Hermes icon + product name are
|
||||
# stamped onto Hermes.exe by our own rcedit step (Set-DesktopExeIdentity)
|
||||
# AFTER this build, completely decoupled from electron-builder signing.
|
||||
#
|
||||
# WIN_CSC_LINK and WIN_CSC_KEY_PASSWORD explicitly cleared as
|
||||
# belt-and-suspenders: if the user's environment has them set
|
||||
|
|
@ -2086,6 +2086,13 @@ function Install-Desktop {
|
|||
throw "Desktop build completed but no Hermes.exe was found under $desktopDir\release\*-unpacked\"
|
||||
}
|
||||
|
||||
# 3b. Stamp the Hermes icon + identity onto Hermes.exe ourselves.
|
||||
# electron-builder's own rcedit step is disabled (signAndEditExecutable
|
||||
# =false) because enabling it drags in signtool -> winCodeSign -> the
|
||||
# unfixable symlink crash. So we run rcedit directly on the produced
|
||||
# exe — no signing, no winCodeSign, just PE resource editing.
|
||||
Set-DesktopExeIdentity -TargetExe $desktopExe -DesktopDir $desktopDir
|
||||
|
||||
# 4. Create Start Menu + Desktop shortcuts pointing DIRECTLY at the packed
|
||||
# Hermes.exe. We deliberately do NOT point them at `hermes desktop`: that
|
||||
# command rebuilds (npm install + electron-builder) on every launch,
|
||||
|
|
@ -2095,6 +2102,57 @@ function Install-Desktop {
|
|||
New-DesktopShortcuts -TargetExe $desktopExe
|
||||
}
|
||||
|
||||
function Set-DesktopExeIdentity {
|
||||
# Stamp the Hermes icon + version metadata into Hermes.exe via rcedit,
|
||||
# delegated to apps/desktop/scripts/set-exe-identity.cjs (which uses the
|
||||
# `rcedit` npm package — a direct devDependency, so always present).
|
||||
#
|
||||
# Why a Node script instead of calling rcedit from PowerShell: passing
|
||||
# arguments through PowerShell -> exe -> JSON parsers double-escapes
|
||||
# Windows backslashes and breaks app-builder's --args JSON. Letting Node
|
||||
# build the rcedit argv natively sidesteps all shell-quoting hazards.
|
||||
#
|
||||
# This replaces electron-builder's built-in signAndEditExecutable step
|
||||
# (kept false, because enabling it re-triggers signtool -> winCodeSign ->
|
||||
# the macOS-symlink 7-Zip crash on non-admin Windows). rcedit is a pure PE
|
||||
# resource editor — no signing, no certs, no winCodeSign, no symlinks.
|
||||
#
|
||||
# Best-effort: a stamping failure must not fail an otherwise-good install
|
||||
# (worst case is the stock Electron icon, not a broken app).
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$TargetExe,
|
||||
[Parameter(Mandatory = $true)][string]$DesktopDir
|
||||
)
|
||||
|
||||
$nodeExe = Get-Command node -ErrorAction SilentlyContinue
|
||||
if (-not $nodeExe) {
|
||||
Write-Warn "node not on PATH; cannot stamp Hermes.exe identity (it will keep the stock Electron icon)"
|
||||
return
|
||||
}
|
||||
|
||||
$script = Join-Path $DesktopDir "scripts\set-exe-identity.cjs"
|
||||
if (-not (Test-Path $script)) {
|
||||
Write-Warn "set-exe-identity.cjs not found at $script; skipping exe identity stamp"
|
||||
return
|
||||
}
|
||||
|
||||
$prevEAP = $ErrorActionPreference
|
||||
$ErrorActionPreference = "Continue"
|
||||
# Run from $DesktopDir so the script's `require('rcedit')` resolves against
|
||||
# the desktop workspace's node_modules.
|
||||
Push-Location $DesktopDir
|
||||
& $nodeExe.Source $script $TargetExe 2>&1 | ForEach-Object { "$_" }
|
||||
$code = $LASTEXITCODE
|
||||
Pop-Location
|
||||
$ErrorActionPreference = $prevEAP
|
||||
|
||||
if ($code -eq 0) {
|
||||
Write-Success "Stamped Hermes icon + identity onto $TargetExe"
|
||||
} else {
|
||||
Write-Warn "Exe identity stamp failed (exit $code); Hermes.exe keeps the stock Electron icon"
|
||||
}
|
||||
}
|
||||
|
||||
function New-DesktopShortcuts {
|
||||
param([Parameter(Mandatory = $true)][string]$TargetExe)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue