Files
pnpm/.github/scripts/assert-windows-crt-static.mjs
T
3172a576b7 fix: statically link CRT for windows-msvc pnpm binaries (#15853)
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>
2026-09-27 12:16:54 +02:00

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()
}