mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
The release cut swaps the SignPath-signed elevate.exe into the electron-builder toolset cache so the NSIS rebuild's CopyElevateHelper re-copy becomes a no-op. It searched `<cache>\nsis`, a directory no app-builder-lib layout creates, and `-ErrorAction SilentlyContinue` plus `exit 0` turned that miss into a green step — v1.4.193 and v1.4.194 shipped an unsigned UAC elevation helper. Move the lookup into a script that covers the real layouts (`nsis-3.0.4.1/…`, `nsis@<toolset>/…`, `ELECTRON_BUILDER_NSIS_DIR`), asks app-builder-lib for the authoritative path, and exits non-zero with an ::error:: annotation when it finds nothing. The step stays continue-on-error so the inner-signing chain remains fail-open.
200 lines
7.3 KiB
JavaScript
200 lines
7.3 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
// Why: electron-builder re-runs `CopyElevateHelper.copy` on every NSIS pack, so the
|
|
// release rebuild overwrites the SignPath-signed `resources/elevate.exe` with the
|
|
// unsigned copy sitting in the electron-builder toolset cache. The release workflow
|
|
// swapped the cached copy first, but searched `<cache>/nsis` — a directory no current
|
|
// app-builder-lib layout creates (real ones are `<cache>/nsis-3.0.4.1/nsis-3.0.4.1-<hash>/`
|
|
// and `<cache>/nsis@<toolset>/nsis-bundle-<v>-<hash>/`), so the swap silently found
|
|
// nothing and v1.4.193/v1.4.194 shipped an unsigned UAC elevation helper.
|
|
|
|
import { copyFileSync, readdirSync, statSync } from 'node:fs'
|
|
import { createRequire } from 'node:module'
|
|
import { homedir, platform as osPlatform, tmpdir } from 'node:os'
|
|
import { join, parse, resolve } from 'node:path'
|
|
|
|
const require = createRequire(import.meta.url)
|
|
|
|
const ELEVATE_EXE = 'elevate.exe'
|
|
|
|
// `nsis` (the layout the old hardcoded path assumed), `nsis-3.0.4.1` (legacy bundle,
|
|
// and `getBinFromCustomLoc('nsis', version)`), `nsis@1.2.1` (unified bundle).
|
|
const NSIS_RELEASE_DIR = /^nsis(?:[-@].*)?$/i
|
|
|
|
// elevate.exe lives at the bundle root, one level under the release dir. The legacy
|
|
// bundle carries thousands of files under Contrib/, so an unbounded walk is both slow
|
|
// and a way to match something that is not a toolset copy.
|
|
const MAX_DEPTH = 3
|
|
|
|
function isFile(path) {
|
|
try {
|
|
return statSync(path).isFile()
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mirrors `getCacheDirectory` in app-builder-lib's `out/util/electronGet.js`, which is what
|
|
* decides where the NSIS bundle is unpacked. Kept as a local port rather than an import
|
|
* because the swap must still resolve a cache root when app-builder-lib cannot be loaded.
|
|
*/
|
|
export function resolveElectronBuilderCacheDir({
|
|
env = process.env,
|
|
platform = osPlatform(),
|
|
home = homedir(),
|
|
temp = tmpdir()
|
|
} = {}) {
|
|
const override = env.ELECTRON_BUILDER_CACHE?.trim()
|
|
if (override && parse(override).root) {
|
|
return override
|
|
}
|
|
if (platform === 'darwin') {
|
|
return join(home, 'Library', 'Caches', 'electron-builder')
|
|
}
|
|
if (platform === 'win32') {
|
|
const localAppData = env.LOCALAPPDATA?.trim()
|
|
// https://github.com/electron-userland/electron-builder/issues/1164
|
|
const isSystemUser =
|
|
localAppData?.toLowerCase().includes('\\windows\\system32\\') === true ||
|
|
env.USERNAME?.trim().toLowerCase() === 'system'
|
|
if (!localAppData || isSystemUser) {
|
|
return join(temp, 'electron-builder-cache')
|
|
}
|
|
return join(localAppData, 'electron-builder', 'Cache')
|
|
}
|
|
const xdgCache = env.XDG_CACHE_HOME
|
|
return xdgCache && parse(xdgCache).root
|
|
? join(xdgCache, 'electron-builder')
|
|
: join(home, '.cache', 'electron-builder')
|
|
}
|
|
|
|
function collectElevateFiles(dir, depth, found) {
|
|
let entries
|
|
try {
|
|
entries = readdirSync(dir, { withFileTypes: true })
|
|
} catch {
|
|
return found
|
|
}
|
|
for (const entry of entries) {
|
|
const path = join(dir, entry.name)
|
|
if (entry.isFile()) {
|
|
if (entry.name.toLowerCase() === ELEVATE_EXE) {
|
|
found.push(path)
|
|
}
|
|
} else if (entry.isDirectory() && depth > 1) {
|
|
collectElevateFiles(path, depth - 1, found)
|
|
}
|
|
}
|
|
return found
|
|
}
|
|
|
|
/**
|
|
* Every cached `elevate.exe` under an NSIS release directory of `cacheDir`, plus the
|
|
* `ELECTRON_BUILDER_NSIS_DIR` override copy when that is set.
|
|
*/
|
|
export function findCachedElevatePaths(cacheDir, { env = process.env } = {}) {
|
|
const found = []
|
|
const overrideDir = env.ELECTRON_BUILDER_NSIS_DIR?.trim()
|
|
if (overrideDir && isFile(join(overrideDir, ELEVATE_EXE))) {
|
|
found.push(join(overrideDir, ELEVATE_EXE))
|
|
}
|
|
let entries
|
|
try {
|
|
entries = readdirSync(cacheDir, { withFileTypes: true })
|
|
} catch {
|
|
return found
|
|
}
|
|
for (const entry of entries) {
|
|
if (entry.isDirectory() && NSIS_RELEASE_DIR.test(entry.name)) {
|
|
collectElevateFiles(join(cacheDir, entry.name), MAX_DEPTH, found)
|
|
}
|
|
}
|
|
return found
|
|
}
|
|
|
|
/**
|
|
* The exact path `CopyElevateHelper` will pack, asked of app-builder-lib itself. Best-effort:
|
|
* the internal module path moves between majors, and a cold cache would need the network,
|
|
* so a failure here degrades to the directory scan rather than failing the swap.
|
|
*/
|
|
export async function resolveToolsetElevatePath(projectDir = process.cwd()) {
|
|
try {
|
|
const configPath = require.resolve(resolve(projectDir, 'config/electron-builder.config.cjs'))
|
|
const config = require(configPath)
|
|
const { getNsisElevatePath } = require('app-builder-lib/out/toolsets/windows.js')
|
|
return await getNsisElevatePath(config.toolsets?.nsis, config.nsis?.customNsisBinary)
|
|
} catch (error) {
|
|
process.stderr.write(
|
|
`Could not resolve elevate.exe through app-builder-lib (${error.message}); ` +
|
|
'falling back to the toolset cache scan.\n'
|
|
)
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Replaces every cached copy rather than picking one. Which bundle the rebuild packs
|
|
* depends on the toolset version resolved at pack time, and each cached copy is an
|
|
* unsigned `elevate.exe` that a later pack could reach for; the helper is a standalone
|
|
* UAC shim, not coupled to the NSIS version around it, so overwriting all of them is safe.
|
|
*/
|
|
export async function replaceCachedElevateHelpers({
|
|
signedPath,
|
|
cacheDir = resolveElectronBuilderCacheDir(),
|
|
projectDir = process.cwd(),
|
|
env = process.env,
|
|
probeToolset = true
|
|
} = {}) {
|
|
if (!isFile(signedPath)) {
|
|
throw new Error(`Signed elevate.exe not found: ${signedPath}`)
|
|
}
|
|
const targets = new Set(findCachedElevatePaths(cacheDir, { env }))
|
|
const toolsetPath = probeToolset ? await resolveToolsetElevatePath(projectDir) : null
|
|
if (toolsetPath != null && isFile(toolsetPath)) {
|
|
targets.add(toolsetPath)
|
|
}
|
|
|
|
const replaced = []
|
|
for (const target of targets) {
|
|
copyFileSync(signedPath, target)
|
|
replaced.push(target)
|
|
}
|
|
return { replaced, cacheDir, toolsetPath }
|
|
}
|
|
|
|
// Why an exit code and not a warning: a swap that finds nothing exits before the NSIS
|
|
// rebuild restores the unsigned helper, so a silent success here is indistinguishable
|
|
// from a release that shipped a signed one — which is how this went unnoticed for two
|
|
// releases. The workflow step is `continue-on-error`, so this annotates loudly without
|
|
// making a release unbuildable.
|
|
if (import.meta.filename === process.argv[1]) {
|
|
const signedPath = process.argv[2]
|
|
if (!signedPath) {
|
|
process.stderr.write('Usage: replace-cached-nsis-elevate.mjs <signed-elevate.exe>\n')
|
|
process.exit(2)
|
|
}
|
|
try {
|
|
const { replaced, cacheDir } = await replaceCachedElevateHelpers({ signedPath })
|
|
if (replaced.length === 0) {
|
|
process.stdout.write(
|
|
`::error::No cached elevate.exe found under ${cacheDir}; the NSIS rebuild will pack the ` +
|
|
'unsigned helper and ship an unsigned UAC elevation binary. The electron-builder ' +
|
|
'toolset cache layout has changed — update config/scripts/replace-cached-nsis-elevate.mjs.\n'
|
|
)
|
|
process.exit(1)
|
|
}
|
|
if (replaced.length > 1) {
|
|
process.stdout.write(
|
|
`Note: ${replaced.length} cached NSIS bundles were present; replaced the helper in all of them.\n`
|
|
)
|
|
}
|
|
for (const path of replaced) {
|
|
process.stdout.write(`Replaced ${path} with the SignPath-signed copy.\n`)
|
|
}
|
|
} catch (error) {
|
|
process.stdout.write(`::error::Could not replace the cached elevate.exe: ${error.message}\n`)
|
|
process.exit(1)
|
|
}
|
|
}
|