The published Windows pnpm.exe linked VCRUNTIME140.dll, so it exited 0xC0000135 on a clean Windows install. The Rust release job now passes -C target-feature=+crt-static when building the CLI for *-pc-windows-msvc. The NAPI cdylib build is unchanged, including the musl -crt-static flag. The release verification job rejects a Windows pnpm.exe that still imports that DLL. Closes https://github.com/pnpm/pnpm/issues/15723 --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Zoltan Kochan <z@kochan.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
118 lines
4.5 KiB
JavaScript
118 lines
4.5 KiB
JavaScript
// Release gate for the windows-msvc CLI: the shipped pnpm.exe must not import
|
|
// the Visual C++ runtime. A clean Windows install does not have that DLL.
|
|
import { readFileSync } from 'node:fs'
|
|
import { pathToFileURL } from 'node:url'
|
|
|
|
const VC_RUNTIME_DLL = /^vcruntime140/i
|
|
|
|
export function importedDlls (bytes) {
|
|
const view = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes)
|
|
if (view.length < 0x40 || view.toString('ascii', 0, 2) !== 'MZ') {
|
|
throw new Error('not a PE file (missing MZ header)')
|
|
}
|
|
const peOffset = view.readUInt32LE(0x3c)
|
|
if (peOffset + 24 > view.length || view.toString('ascii', peOffset, peOffset + 4) !== 'PE\0\0') {
|
|
throw new Error('not a PE file (missing PE signature)')
|
|
}
|
|
const coff = peOffset + 4
|
|
const sectionCount = view.readUInt16LE(coff + 2)
|
|
const optionalSize = view.readUInt16LE(coff + 16)
|
|
const optional = coff + 20
|
|
if (optional + optionalSize > view.length) {
|
|
throw new Error('truncated PE optional header')
|
|
}
|
|
const magic = view.readUInt16LE(optional)
|
|
let dataDirectoryOffset
|
|
if (magic === 0x10b) dataDirectoryOffset = optional + 96
|
|
else if (magic === 0x20b) dataDirectoryOffset = optional + 112
|
|
else throw new Error(`unsupported PE magic 0x${magic.toString(16)}`)
|
|
if (dataDirectoryOffset > view.length) {
|
|
throw new Error('truncated PE data directories')
|
|
}
|
|
const numberOfRva = view.readUInt32LE(dataDirectoryOffset - 4)
|
|
if (numberOfRva < 2) return []
|
|
const importRva = view.readUInt32LE(dataDirectoryOffset + 8)
|
|
const importSize = view.readUInt32LE(dataDirectoryOffset + 12)
|
|
if (importRva === 0 || importSize === 0) return []
|
|
|
|
const sections = []
|
|
let sectionHeader = optional + optionalSize
|
|
for (let index = 0; index < sectionCount; index++) {
|
|
if (sectionHeader + 40 > view.length) throw new Error('truncated section header')
|
|
sections.push({
|
|
virtualAddress: view.readUInt32LE(sectionHeader + 12),
|
|
virtualSize: view.readUInt32LE(sectionHeader + 8),
|
|
rawSize: view.readUInt32LE(sectionHeader + 16),
|
|
rawPointer: view.readUInt32LE(sectionHeader + 20),
|
|
})
|
|
sectionHeader += 40
|
|
}
|
|
|
|
const names = []
|
|
const directory = rvaToRawSpan(view, sections, importRva)
|
|
let entry = directory.offset
|
|
for (;;) {
|
|
if (entry + 20 > directory.end) throw new Error('truncated import directory')
|
|
const originalFirstThunk = view.readUInt32LE(entry)
|
|
const timeDateStamp = view.readUInt32LE(entry + 4)
|
|
const forwarderChain = view.readUInt32LE(entry + 8)
|
|
const nameRva = view.readUInt32LE(entry + 12)
|
|
const firstThunk = view.readUInt32LE(entry + 16)
|
|
if (nameRva === 0 && originalFirstThunk === 0 && timeDateStamp === 0 && forwarderChain === 0 && firstThunk === 0) break
|
|
const name = rvaToRawSpan(view, sections, nameRva)
|
|
const length = view.subarray(name.offset, name.end).indexOf(0)
|
|
if (length === -1) throw new Error('unterminated import DLL name')
|
|
names.push(view.toString('ascii', name.offset, name.offset + length))
|
|
entry += 20
|
|
}
|
|
return names
|
|
}
|
|
|
|
export function dynamicVcRuntimeDlls (dlls) {
|
|
return dlls.filter(name => VC_RUNTIME_DLL.test(name))
|
|
}
|
|
|
|
// The loader zero-fills a section past its raw data, and bytes after the raw
|
|
// data belong to something else, so a read that starts at an RVA must end
|
|
// within the same section's raw data.
|
|
function rvaToRawSpan (view, sections, rva) {
|
|
for (const section of sections) {
|
|
const span = Math.max(section.virtualSize, section.rawSize)
|
|
if (rva < section.virtualAddress || rva >= section.virtualAddress + span) continue
|
|
const delta = rva - section.virtualAddress
|
|
const offset = section.rawPointer + delta
|
|
const end = Math.min(section.rawPointer + section.rawSize, view.length)
|
|
if (delta >= section.rawSize || offset >= end) throw new Error(`RVA 0x${rva.toString(16)} points outside the file`)
|
|
return { offset, end }
|
|
}
|
|
throw new Error(`RVA 0x${rva.toString(16)} is not in any section`)
|
|
}
|
|
|
|
function main () {
|
|
const files = process.argv.slice(2)
|
|
if (files.length === 0) {
|
|
console.error('usage: node assert-windows-crt-static.mjs <pe-file>...')
|
|
process.exit(2)
|
|
}
|
|
let failed = false
|
|
for (const file of files) {
|
|
let linked
|
|
try {
|
|
linked = dynamicVcRuntimeDlls(importedDlls(readFileSync(file)))
|
|
} catch (error) {
|
|
console.error(`${file}: ${error.message}`)
|
|
failed = true
|
|
continue
|
|
}
|
|
if (linked.length > 0) {
|
|
console.error(`${file} dynamically links ${linked.join(', ')}`)
|
|
failed = true
|
|
}
|
|
}
|
|
if (failed) process.exit(1)
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
main()
|
|
}
|