mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 08:02:12 +00:00
fix(windows): link the CLI launcher's C runtime statically so orca.exe runs without the VC++ Redistributable (#24484)
This commit is contained in:
@@ -5,6 +5,7 @@ import { createHash } from 'node:crypto'
|
||||
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { findDynamicVcRuntimeImports, readPeImportedDllNames } from './windows-pe-imports.mjs'
|
||||
|
||||
export function windowsCliLauncherFingerprint(inputPaths, version) {
|
||||
const hash = createHash('sha256').update(version)
|
||||
@@ -66,6 +67,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
||||
join(crateRoot, 'build.rs'),
|
||||
manifestPath,
|
||||
join(crateRoot, 'app.manifest'),
|
||||
join(crateRoot, '.cargo', 'config.toml'),
|
||||
iconPath,
|
||||
join(repoRoot, 'config/scripts/build-windows-cli-launcher.mjs')
|
||||
],
|
||||
@@ -117,6 +119,16 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
||||
process.exit(result.status ?? 1)
|
||||
}
|
||||
|
||||
copyFileSync(join(targetDirectory, 'release', 'orca.exe'), outputPath)
|
||||
const builtPath = join(targetDirectory, 'release', 'orca.exe')
|
||||
const vcRuntimeImports = findDynamicVcRuntimeImports(
|
||||
readPeImportedDllNames(readFileSync(builtPath))
|
||||
)
|
||||
if (vcRuntimeImports.length > 0) {
|
||||
// Why fatal: those DLLs ship with the Visual C++ Redistributable, so the CLI would fail to start on a clean Windows install.
|
||||
throw new Error(
|
||||
`orca.exe imports ${vcRuntimeImports.join(', ')}; the C runtime must be linked statically (native/windows-cli-launcher/.cargo/config.toml).`
|
||||
)
|
||||
}
|
||||
copyFileSync(builtPath, outputPath)
|
||||
writeFileSync(`${outputPath}.sha256`, fingerprint)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
windowsCliLauncherFileVersion,
|
||||
windowsCliLauncherFingerprint
|
||||
} from './build-windows-cli-launcher.mjs'
|
||||
import { findDynamicVcRuntimeImports, readPeImportedDllNames } from './windows-pe-imports.mjs'
|
||||
|
||||
const itCrossHost = process.platform === 'win32' ? it.skip : it
|
||||
const projectRoot = resolve(import.meta.dirname, '../..')
|
||||
@@ -131,6 +132,9 @@ describe('Windows CLI launcher', () => {
|
||||
expect(info.ProductVersion).toBe(version)
|
||||
const binary = readFileSync(launcherPath)
|
||||
expect(binary.includes(Buffer.from('requestedExecutionLevel level="asInvoker"'))).toBe(true)
|
||||
const imports = readPeImportedDllNames(binary)
|
||||
expect(imports.map((name) => name.toLowerCase())).toContain('kernel32.dll')
|
||||
expect(findDynamicVcRuntimeImports(imports)).toEqual([])
|
||||
const icon = readFileSync(join(projectRoot, 'resources', 'build', 'icon.ico'))
|
||||
const imageSize = icon.readUInt32LE(14)
|
||||
const imageOffset = icon.readUInt32LE(18)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Why a hand-rolled reader: the release guard only needs the import table's DLL
|
||||
// names, and a parser dependency would be a new supply-chain input for one check.
|
||||
const IMPORT_DIRECTORY_INDEX = 1
|
||||
|
||||
export function readPeImportedDllNames(buffer) {
|
||||
const peOffset = buffer.readUInt32LE(0x3c)
|
||||
if (buffer.toString('latin1', peOffset, peOffset + 4) !== 'PE\0\0') {
|
||||
throw new Error('Not a PE image')
|
||||
}
|
||||
const sectionCount = buffer.readUInt16LE(peOffset + 6)
|
||||
const optionalHeaderSize = buffer.readUInt16LE(peOffset + 20)
|
||||
const optionalHeader = peOffset + 24
|
||||
const isPe32Plus = buffer.readUInt16LE(optionalHeader) === 0x20b
|
||||
const dataDirectories = optionalHeader + (isPe32Plus ? 112 : 96)
|
||||
const importRva = buffer.readUInt32LE(dataDirectories + IMPORT_DIRECTORY_INDEX * 8)
|
||||
if (importRva === 0) {
|
||||
return []
|
||||
}
|
||||
const sections = []
|
||||
for (let index = 0; index < sectionCount; index += 1) {
|
||||
const header = optionalHeader + optionalHeaderSize + index * 40
|
||||
sections.push({
|
||||
virtualAddress: buffer.readUInt32LE(header + 12),
|
||||
size: Math.max(buffer.readUInt32LE(header + 8), buffer.readUInt32LE(header + 16)),
|
||||
rawOffset: buffer.readUInt32LE(header + 20)
|
||||
})
|
||||
}
|
||||
const fileOffset = (rva) => {
|
||||
const section = sections.find(
|
||||
(candidate) =>
|
||||
rva >= candidate.virtualAddress && rva < candidate.virtualAddress + candidate.size
|
||||
)
|
||||
if (!section) {
|
||||
throw new Error(`RVA 0x${rva.toString(16)} is outside every section`)
|
||||
}
|
||||
return rva - section.virtualAddress + section.rawOffset
|
||||
}
|
||||
const names = []
|
||||
for (let descriptor = fileOffset(importRva); ; descriptor += 20) {
|
||||
const nameRva = buffer.readUInt32LE(descriptor + 12)
|
||||
if (nameRva === 0) {
|
||||
return names
|
||||
}
|
||||
const start = fileOffset(nameRva)
|
||||
names.push(buffer.toString('latin1', start, buffer.indexOf(0, start)))
|
||||
}
|
||||
}
|
||||
|
||||
// The Visual C++ runtime DLLs ship with the Redistributable, not with Windows.
|
||||
const DYNAMIC_VC_RUNTIME_RE = /^(?:vcruntime|msvcp|msvcr)\d+(?:_\d+)?\.dll$/i
|
||||
|
||||
export function findDynamicVcRuntimeImports(dllNames) {
|
||||
return dllNames.filter((name) => DYNAMIC_VC_RUNTIME_RE.test(name))
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { findDynamicVcRuntimeImports, readPeImportedDllNames } from './windows-pe-imports.mjs'
|
||||
|
||||
// Smallest PE32+ image with one section holding an import directory for `dllNames`.
|
||||
function peWithImports(dllNames) {
|
||||
const peOffset = 0x40
|
||||
const optionalHeaderSize = 240
|
||||
const sectionHeader = peOffset + 24 + optionalHeaderSize
|
||||
const rawOffset = 0x200
|
||||
const virtualAddress = 0x1000
|
||||
const descriptorsSize = (dllNames.length + 1) * 20
|
||||
const strings = dllNames.map((name) => Buffer.from(`${name}\0`, 'latin1'))
|
||||
const sectionSize = descriptorsSize + strings.reduce((sum, item) => sum + item.length, 0)
|
||||
const buffer = Buffer.alloc(rawOffset + sectionSize)
|
||||
buffer.write('MZ', 0, 'latin1')
|
||||
buffer.writeUInt32LE(peOffset, 0x3c)
|
||||
buffer.write('PE\0\0', peOffset, 'latin1')
|
||||
buffer.writeUInt16LE(0x8664, peOffset + 4)
|
||||
buffer.writeUInt16LE(1, peOffset + 6)
|
||||
buffer.writeUInt16LE(optionalHeaderSize, peOffset + 20)
|
||||
buffer.writeUInt16LE(0x20b, peOffset + 24)
|
||||
buffer.writeUInt32LE(virtualAddress, peOffset + 24 + 112 + 8)
|
||||
buffer.writeUInt32LE(sectionSize, sectionHeader + 8)
|
||||
buffer.writeUInt32LE(virtualAddress, sectionHeader + 12)
|
||||
buffer.writeUInt32LE(sectionSize, sectionHeader + 16)
|
||||
buffer.writeUInt32LE(rawOffset, sectionHeader + 20)
|
||||
let stringRva = virtualAddress + descriptorsSize
|
||||
strings.forEach((item, index) => {
|
||||
buffer.writeUInt32LE(stringRva, rawOffset + index * 20 + 12)
|
||||
item.copy(buffer, rawOffset + (stringRva - virtualAddress))
|
||||
stringRva += item.length
|
||||
})
|
||||
return buffer
|
||||
}
|
||||
|
||||
describe('windows PE imports', () => {
|
||||
it('reads every imported DLL name in order', () => {
|
||||
const names = ['KERNEL32.dll', 'VCRUNTIME140.dll', 'api-ms-win-crt-runtime-l1-1-0.dll']
|
||||
expect(readPeImportedDllNames(peWithImports(names))).toEqual(names)
|
||||
})
|
||||
|
||||
it('reads an image with no imports', () => {
|
||||
expect(readPeImportedDllNames(peWithImports([]))).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects a file that is not a PE image', () => {
|
||||
const buffer = Buffer.alloc(0x80)
|
||||
buffer.writeUInt32LE(0x40, 0x3c)
|
||||
expect(() => readPeImportedDllNames(buffer)).toThrow('Not a PE image')
|
||||
})
|
||||
|
||||
it('flags only the Visual C++ Redistributable DLLs', () => {
|
||||
expect(
|
||||
findDynamicVcRuntimeImports([
|
||||
'KERNEL32.dll',
|
||||
'ntdll.dll',
|
||||
'VCRUNTIME140.dll',
|
||||
'vcruntime140_1.dll',
|
||||
'MSVCP140.dll',
|
||||
'msvcr120.dll',
|
||||
'api-ms-win-crt-heap-l1-1-0.dll',
|
||||
'ucrtbase.dll'
|
||||
])
|
||||
).toEqual(['VCRUNTIME140.dll', 'vcruntime140_1.dll', 'MSVCP140.dll', 'msvcr120.dll'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
# Why: link the C runtime statically so orca.exe runs on Windows hosts without the
|
||||
# Visual C++ Redistributable (VCRUNTIME140.dll is not part of Windows).
|
||||
[target.'cfg(all(windows, target_env = "msvc"))']
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
Reference in New Issue
Block a user