build(ssh): disable incremental relay linking

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-07-14 16:25:48 -04:00
co-authored by Orca
parent d1eca8a55d
commit 6dfdfa0bd0
2 changed files with 33 additions and 21 deletions
@@ -36,18 +36,19 @@ const REPRODUCIBLE_LINKER_OPTIONS = ` 'VCLinkerTool': {
'AdditionalOptions': [
'/DYNAMICBASE',
'/guard:cf',
'/INCREMENTAL:NO',
'/Brepro',
'/experimental:deterministic'
]
}`
const REQUIRED_GENERATED_OPTIONS = ['/Brepro', '/experimental:deterministic']
const REQUIRED_COMPILER_OPTIONS = ['/Brepro', '/experimental:deterministic']
const REQUIRED_LINKER_OPTIONS = [...REQUIRED_COMPILER_OPTIONS, '/INCREMENTAL:NO']
const MAX_LINK_COMMAND_TRACKING_BYTES = 256 * 1024
const MAX_LINK_COMMAND_TRACKING_CANDIDATES = 32
const MAX_LINK_COMMAND_TRACKING_DEPTH = 8
const MAX_LINK_COMMAND_TRACKING_ENTRIES = 10_000
const TARGET_LINK_OUTPUT_PATTERN = /(?:^|[\\/:"'\s])conpty_console_list\.node(?=$|[\\/:"'\s])/i
const TARGET_INCREMENTAL_DATABASE = 'conpty_console_list.ilk'
function decodeXmlText(value) {
return value
@@ -72,7 +73,7 @@ function releaseDefinitionGroup(source, configuration) {
return groups[0][2]
}
function requiredToolOptions(group, tool) {
function requiredToolOptions(group, tool, requiredOptions) {
const tools = [...group.matchAll(new RegExp(`<${tool}\\b[^>]*>([\\s\\S]*?)</${tool}>`, 'g'))]
const options = tools.flatMap((match) => [
...match[1].matchAll(/<AdditionalOptions\b[^>]*>([\s\S]*?)<\/AdditionalOptions>/g)
@@ -84,13 +85,13 @@ function requiredToolOptions(group, tool) {
if (!tokens.includes('%(AdditionalOptions)')) {
throw new Error(`generated MSBuild settings do not inherit ${tool} options`)
}
for (const required of REQUIRED_GENERATED_OPTIONS) {
for (const required of requiredOptions) {
const count = tokens.filter((token) => token.toLowerCase() === required.toLowerCase()).length
if (count !== 1) {
throw new Error(`generated MSBuild settings require exactly one ${tool} ${required}`)
}
}
return [...REQUIRED_GENERATED_OPTIONS]
return [...requiredOptions]
}
export async function applyWindowsNodePtyBuildDeterminism({ nodePtyDirectory, tuple }) {
@@ -103,11 +104,13 @@ export async function applyWindowsNodePtyBuildDeterminism({ nodePtyDirectory, tu
source.split(COMPILER_OPTIONS).length !== 2 ||
source.split(LINKER_OPTIONS).length !== 2 ||
source.includes("'/Brepro'") ||
source.includes("'/INCREMENTAL:NO'") ||
source.includes("'/experimental:deterministic'")
) {
throw new Error('node-pty Windows build settings do not match the reviewed source')
}
// Why: ARM64 compiler output changes even with a reproducible linker unless both stages opt in.
// Why: ARM64 needs both reproducible stages, while /DEBUG-implied incremental thunks still drift.
// Keep this producer correction inside the exclusive copied artifact source.
const reproducibleSource = source
.replace(COMPILER_OPTIONS, REPRODUCIBLE_COMPILER_OPTIONS)
.replace(LINKER_OPTIONS, REPRODUCIBLE_LINKER_OPTIONS)
@@ -126,8 +129,8 @@ export async function assertWindowsNodePtyGeneratedBuildSettings({ nodePtyDirect
// Why: the source rewrite is insufficient proof unless gyp propagates both stages into MSBuild.
return {
configuration,
compilerOptions: requiredToolOptions(group, 'ClCompile'),
linkerOptions: requiredToolOptions(group, 'Link'),
compilerOptions: requiredToolOptions(group, 'ClCompile', REQUIRED_COMPILER_OPTIONS),
linkerOptions: requiredToolOptions(group, 'Link', REQUIRED_LINKER_OPTIONS),
project
}
}
@@ -180,7 +183,7 @@ async function discoverLinkCommandTracking(nodePtyDirectory) {
if (candidates.length > MAX_LINK_COMMAND_TRACKING_CANDIDATES) {
throw new Error('MSBuild linker tracking discovery has too many candidates')
}
} else if (child.isFile() && child.name.toLowerCase() === TARGET_INCREMENTAL_DATABASE) {
} else if (child.isFile() && child.name.toLowerCase() === 'conpty_console_list.ilk') {
incrementalDatabases.push(childPath)
if (incrementalDatabases.length > 1) {
throw new Error('MSBuild incremental database discovery has duplicate target files')
@@ -281,13 +284,14 @@ export async function inspectWindowsNodePtyLinkCommandTracking({ nodePtyDirector
let incrementalDatabase = { state: 'absent' }
if (discovery.incrementalDatabases.length === 1) {
const bytes = (await stat(discovery.incrementalDatabases[0])).size
if (!Number.isSafeInteger(bytes) || bytes < 0) {
throw new Error('MSBuild incremental database has an invalid size')
}
incrementalDatabase = { bytes, state: 'present' }
}
const linkCommand = parseWindowsNodePtyLinkCommandTracking(matching[0])
if (linkCommand.incremental !== 'disabled' || incrementalDatabase.state !== 'absent') {
throw new Error('MSBuild incremental linking must be disabled without a target database')
}
return {
...parseWindowsNodePtyLinkCommandTracking(matching[0]),
...linkCommand,
candidateFiles: discovery.candidates.length,
incrementalDatabase,
searchedEntries: discovery.entries
@@ -25,7 +25,7 @@ async function copiedSource() {
async function generatedProject({
compilerOptions = '/Brepro /experimental:deterministic %(AdditionalOptions)',
linkerOptions = '/Brepro /experimental:deterministic %(AdditionalOptions)',
linkerOptions = '/Brepro /experimental:deterministic /INCREMENTAL:NO %(AdditionalOptions)',
platform = 'ARM64'
} = {}) {
const directory = await mkdtemp(join(tmpdir(), 'orca-node-pty-windows-msbuild-'))
@@ -54,6 +54,7 @@ describe('SSH relay Windows node-pty build determinism', () => {
expect(compilerOptions).toContain("'/experimental:deterministic'")
expect(linkerOptions).toContain("'/Brepro'")
expect(linkerOptions).toContain("'/experimental:deterministic'")
expect(linkerOptions).toContain("'/INCREMENTAL:NO'")
expect(await readFile(resolve('node_modules/node-pty/binding.gyp'), 'utf8')).toBe(
repositorySource
)
@@ -83,7 +84,7 @@ describe('SSH relay Windows node-pty build determinism', () => {
).resolves.toEqual({
configuration: 'Release|ARM64',
compilerOptions: ['/Brepro', '/experimental:deterministic'],
linkerOptions: ['/Brepro', '/experimental:deterministic'],
linkerOptions: ['/Brepro', '/experimental:deterministic', '/INCREMENTAL:NO'],
project: 'conpty_console_list.vcxproj'
})
})
@@ -170,21 +171,28 @@ describe('SSH relay Windows node-pty build determinism', () => {
nodePtyDirectory: directory,
tuple: 'win32-x64'
})
).resolves.toMatchObject({
candidateFiles: 2,
incremental: 'disabled',
incrementalDatabase: { bytes: 4096, state: 'present' }
})
).rejects.toThrow('must be disabled without a target database')
await rm(incrementalPath)
await expect(
inspectWindowsNodePtyLinkCommandTracking({ nodePtyDirectory: directory, tuple: 'win32-x64' })
).resolves.toMatchObject({ incrementalDatabase: { state: 'absent' } })
).resolves.toMatchObject({
candidateFiles: 2,
incremental: 'disabled',
incrementalDatabase: { state: 'absent' }
})
await expect(
inspectWindowsNodePtyLinkCommandTracking({
nodePtyDirectory: '/not-used-on-posix',
tuple: 'linux-arm64-glibc'
})
).resolves.toBeUndefined()
await writeFile(
trackingPath,
'^one\n/OUT:conpty_console_list.node /Brepro /GUARD:CF /experimental:deterministic'
)
await expect(
inspectWindowsNodePtyLinkCommandTracking({ nodePtyDirectory: directory, tuple: 'win32-x64' })
).rejects.toThrow('must be disabled without a target database')
await writeFile(trackingPath, Buffer.alloc(300_000))
await expect(
inspectWindowsNodePtyLinkCommandTracking({