Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/release-cut.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1782,6 +1782,13 @@ jobs:
node config/scripts/finalize-signed-cli-runtime.mjs $root 'signing-stage'
if ($LASTEXITCODE -ne 0) { throw 'Signed runtime identity finalization failed.' }

- name: Verify relay provider identity after signing
if: matrix.platform == 'win' && github.run_attempt == 1
shell: pwsh
run: |
node --input-type=module -e "import { verifyConptyDirectory } from './config/scripts/build-windows-conpty.mjs'; for (const arch of ['x64', 'arm64']) verifyConptyDirectory('dist/win-unpacked/resources/relay/win32-' + arch, arch)"
if ($LASTEXITCODE -ne 0) { throw 'Signing changed a pinned relay terminal provider.' }

# Why gated separately from the inner restore above: if SignPath's
# windows-inner-binaries-zip artifact configuration does not (yet) cover the
# uninstaller/ directory, the uninstaller comes back missing. That must cost
Expand Down
2 changes: 2 additions & 0 deletions config/scripts/build-relay.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { build } from 'esbuild'
import { bundledParcelWatcherPlugin } from './parcel-watcher-bundle.mjs'
import { readWindowsWatcherArtifact } from './windows-watcher-artifact.mjs'
import { stageWindowsRelayConpty } from './relay-conpty-packaging.mjs'
import { createHash } from 'node:crypto'
import {
copyFileSync,
Expand Down Expand Up @@ -107,6 +108,7 @@ for (const platform of RELAY_BUILD_PLATFORMS) {
const watcher = readWindowsWatcherArtifact(platform.slice('win32-'.length))
copyFileSync(watcher.binary, join(outDir, RELAY_WINDOWS_WATCHER_FILENAME))
copyFileSync(watcher.license, join(outDir, RELAY_WINDOWS_WATCHER_LICENSE))
await stageWindowsRelayConpty(platform, outDir)
}

await build({
Expand Down
141 changes: 140 additions & 1 deletion config/scripts/build-windows-conpty.test.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { createHash } from 'node:crypto'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import {
existsSync,
mkdirSync,
mkdtempSync,
readdirSync,
realpathSync,
readFileSync,
rmSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
Expand All @@ -23,6 +32,9 @@ vi.mock('./zip-extractor-command.mjs', () => ({
}))
vi.mock('./script-child-process.mjs', () => ({ runProcessSync: vi.fn() }))
import { runProcessSync } from './script-child-process.mjs'
import { stageWindowsRelayConpty } from './relay-conpty-packaging.mjs'
import { relayDaemonLaunchEnvironment } from './relay-daemon-launch-environment.mjs'
import { RELAY_WINDOWS_CONPTY_FILENAMES } from '../../src/shared/relay-artifacts.ts'
import {
downloadConptyArchive,
materializeWindowsConpty,
Expand Down Expand Up @@ -145,3 +157,130 @@ describe('independent ConPTY payload builder', () => {
await expect(downloadConptyArchive(join(root, 'download'), fetcher)).rejects.toThrow('HTTP 404')
})
})

describe('Windows relay provider packaging', () => {
it.each(['x64', 'arm64'])('stages the verified %s pair and metadata flat', async (arch) => {
mkdirSync(output)
await stageWindowsRelayConpty(`win32-${arch}`, output, { cacheDir, fetcher })
expect(readdirSync(output).sort()).toEqual([...RELAY_WINDOWS_CONPTY_FILENAMES].sort())
verifyConptyDirectory(output, arch)
expect(readFileSync(join(output, 'conpty-LICENSE.txt'), 'utf8')).toContain('MIT License')
expect(JSON.parse(readFileSync(join(output, 'conpty.json'), 'utf8'))).toMatchObject({
arch,
archiveSha256: fixture.archiveHash,
files: fixture.files[arch]
})
})

it.each(['conpty.dll', 'OpenConsole.exe'])(
'rejects corrupt %s and removes staging',
async (filename) => {
mkdirSync(output)
fixture.files.x64[filename] = digest('wrong binary')
await expect(
stageWindowsRelayConpty('win32-x64', output, { cacheDir, fetcher })
).rejects.toThrow('checksum mismatch')
expect(readdirSync(output)).toEqual([])
}
)

it('rejects a missing companion and removes staging', async () => {
mkdirSync(output)
const extract = vi.mocked(runProcessSync).getMockImplementation()
vi.mocked(runProcessSync).mockImplementation((command) => {
const result = extract(command)
rmSync(join(command.args[1], 'build', 'native', 'runtimes', 'x64', 'OpenConsole.exe'))
return result
})
await expect(
stageWindowsRelayConpty('win32-x64', output, { cacheDir, fetcher })
).rejects.toThrow()
expect(readdirSync(output)).toEqual([])
})

it.each(['linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64'])(
'does not touch %s packaging',
async (platform) => {
await stageWindowsRelayConpty(platform, output, { cacheDir, fetcher })
expect(existsSync(output)).toBe(false)
expect(fetcher).not.toHaveBeenCalled()
}
)
})

describe('relay fault-harness provider environment', () => {
it.each(['x64', 'arm64'])('selects verified adjacent %s binaries before launch', async (arch) => {
mkdirSync(output)
await stageWindowsRelayConpty(`win32-${arch}`, output, { cacheDir, fetcher })
const entry = join(output, 'relay.js')
writeFileSync(entry, '')
const env = { BUN_CONPTY_LIBRARY: 'foreign.dll', ORCA_WATCHER_CHILD_PID_FILE: 'watcher.pid' }
expect(relayDaemonLaunchEnvironment(entry, { platform: 'win32', arch, env })).toEqual({
...env,
BUN_CONPTY_LIBRARY: join(realpathSync(output), 'conpty.dll')
})
expect(env.BUN_CONPTY_LIBRARY).toBe('foreign.dll')
writeFileSync(join(output, 'OpenConsole.exe'), 'modified by signing')
expect(() => relayDaemonLaunchEnvironment(entry, { platform: 'win32', arch, env })).toThrow(
'checksum mismatch'
)
rmSync(join(output, 'OpenConsole.exe'))
expect(() => relayDaemonLaunchEnvironment(entry, { platform: 'win32', arch, env })).toThrow()
})

it('leaves POSIX launches independent of Windows artifacts', () => {
const env = { ORCA_WATCHER_CHILD_PID_FILE: 'watcher.pid' }
expect(relayDaemonLaunchEnvironment('/missing/relay.js', { platform: 'linux', env })).toEqual(
env
)
})

it('wires verified environment into the fault harness daemon spawn', () => {
const source = readFileSync(
new URL('./relay-watcher-fault-harness.mjs', import.meta.url),
'utf8'
)
expect(source).toContain(
"import { relayDaemonLaunchEnvironment } from './relay-daemon-launch-environment.mjs'"
)
expect(source).toMatch(
/env: relayDaemonLaunchEnvironment\(relayEntry, \{\s*env: \{ \.\.\.process.env, ORCA_WATCHER_CHILD_PID_FILE: pidFile \}/
)
})

it('gates both relay providers after signing and before installer rebuild', () => {
const source = readFileSync(
new URL('../../.github/workflows/release-cut.yml', import.meta.url),
'utf8'
)
const start = source.indexOf(' - name: Verify relay provider identity after signing')
expect(start).toBeGreaterThan(
source.indexOf(' - name: Restore signed inner binaries into unpacked app')
)
expect(start).toBeLessThan(source.indexOf(' id: rebuild-nsis-signed'))
const step = source.slice(start, source.indexOf(' - name:', start + 1))
expect(step).toContain(
"for (const arch of ['x64', 'arm64']) verifyConptyDirectory('dist/win-unpacked/resources/relay/win32-' + arch, arch)"
)
expect(step).toContain('if ($LASTEXITCODE -ne 0) { throw')
expect(step).not.toContain('continue-on-error')
expect(step).not.toContain('restore-signed-inner.outcome')
})
})

it('removes runtime injection flags without mutating controller environment', () => {
const env = {
NODE_OPTIONS: '--require=foreign',
NODE_PATH: '/foreign',
BUN_OPTIONS: 'foreign',
BUN_INSPECT: '1',
ELECTRON_RUN_AS_NODE: '1',
PATH: '/keep',
ORCA_BACKGROUND_LAUNCH: '1'
}
expect(relayDaemonLaunchEnvironment('/missing/relay.js', { platform: 'linux', env })).toEqual({
PATH: '/keep',
ORCA_BACKGROUND_LAUNCH: '1'
})
expect(env.NODE_OPTIONS).toBe('--require=foreign')
})
13 changes: 13 additions & 0 deletions config/scripts/relay-artifact-manifest.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import {
RELAY_BUILD_PLATFORMS,
RELAY_VERSION_FILENAME,
RELAY_WINDOWS_CONPTY_FILENAMES,
isWindowsRelayPlatform,
relayArtifactFilenames,
relayOptionalArtifactFilenames
} from '../../src/shared/relay-artifacts.ts'

import { verifyConptyDirectory } from './build-windows-conpty.mjs'

const projectDir = resolve(import.meta.dirname, '../..')
// Its own tree: building into out/relay would clobber a developer's build and
// race the suites that read it.
Expand All @@ -35,6 +38,16 @@ afterAll(() => {
})

describe('packaged relay artifact manifest', () => {
it.each(['x64', 'arm64'])('ships the pinned %s provider as required flat artifacts', (arch) => {
const outDir = join(relayOutDir, `win32-${arch}`)
verifyConptyDirectory(outDir, arch)
for (const filename of RELAY_WINDOWS_CONPTY_FILENAMES) {
expect(relayArtifactFilenames(true)).toContain(filename)
expect(relayArtifactFilenames(false)).not.toContain(filename)
expect(relayOptionalArtifactFilenames(true)).not.toContain(filename)
}
})

it.each([...RELAY_BUILD_PLATFORMS])('emits exactly the declared artifacts for %s', (platform) => {
const outDir = join(relayOutDir, platform)
const expected = relayArtifactFilenames(isWindowsRelayPlatform(platform))
Expand Down
25 changes: 25 additions & 0 deletions config/scripts/relay-conpty-packaging.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { copyFileSync, mkdtempSync, rmSync } from 'node:fs'
import { join } from 'node:path'
import {
isWindowsRelayPlatform,
RELAY_WINDOWS_CONPTY_FILENAMES
} from '../../src/shared/relay-artifacts.ts'
import { materializeWindowsConpty, verifyConptyDirectory } from './build-windows-conpty.mjs'

export async function stageWindowsRelayConpty(platform, outputDir, options = {}) {
if (!isWindowsRelayPlatform(platform)) {
return
}
const arch = platform.slice('win32-'.length)
const staging = mkdtempSync(join(outputDir, '.conpty-'))
try {
await materializeWindowsConpty(arch, staging, options)
for (const filename of RELAY_WINDOWS_CONPTY_FILENAMES) {
const source = filename === 'conpty-LICENSE.txt' ? 'LICENSE.txt' : filename
copyFileSync(join(staging, source), join(outputDir, filename))
}
verifyConptyDirectory(outputDir, arch)
} finally {
rmSync(staging, { recursive: true, force: true })
}
}
21 changes: 21 additions & 0 deletions config/scripts/relay-daemon-launch-environment.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { dirname, join } from 'node:path'
import { realpathSync } from 'node:fs'
import { verifyConptyDirectory } from './build-windows-conpty.mjs'

/** Supply the native selector before Bun starts, replacing inherited foreign providers. */
export function relayDaemonLaunchEnvironment(relayEntry, options = {}) {
const platform = options.platform ?? process.platform
const arch = options.arch ?? process.arch
const env = { ...(options.env ?? process.env) }
for (const key of Object.keys(env)) {
if (/^(NODE_OPTIONS|NODE_PATH|BUN_OPTIONS|BUN_INSPECT.*|ELECTRON_RUN_AS_NODE)$/i.test(key)) {
delete env[key]
}
}
if (platform === 'win32') {
const directory = dirname(realpathSync(relayEntry))
verifyConptyDirectory(directory, arch)
env.BUN_CONPTY_LIBRARY = join(directory, 'conpty.dll')
}
return env
}
24 changes: 24 additions & 0 deletions config/scripts/relay-fault-harness-runtime.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { join, resolve } from 'node:path'
import { cliRuntimeFilename, verifyCliRuntimeDirectory } from '../bundled-cli-runtime.cjs'
import { bunOwnedRuntimeArgs } from '../../src/shared/bun-owned-runtime-args.ts'

/** The release build already prepares this runtime; the fault gate must never fetch one. */
export function resolveRelayFaultHarnessRuntime(
root,
platform = process.platform,
arch = process.arch
) {
const directory = resolve(root, 'out', 'cli-runtime', `${platform}-${arch}`)
try {
verifyCliRuntimeDirectory(directory, platform, arch)
} catch (cause) {
throw new Error(
`Prepare the bundled CLI runtime for ${platform}-${arch} before the relay fault gate`,
{ cause }
)
}
return {
executable: join(directory, cliRuntimeFilename(platform)),
args: bunOwnedRuntimeArgs(platform)
}
}
71 changes: 71 additions & 0 deletions config/scripts/relay-fault-harness-runtime.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { createHash } from 'node:crypto'
import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, expect, it } from 'vitest'
import { ORCAD_BUN_VERSION } from '../../src/shared/orcad-bun-runtime.ts'
import { resolveRelayFaultHarnessRuntime } from './relay-fault-harness-runtime.mjs'

const roots = []
afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })))
function fixture(platform, arch) {
const root = mkdtempSync(join(tmpdir(), 'relay-fault-runtime-'))
roots.push(root)
const directory = join(root, 'out/cli-runtime', `${platform}-${arch}`)
mkdirSync(directory, { recursive: true })
copyFileSync(
new URL('../../resources/licenses/bun/LICENSE.md', import.meta.url),
join(directory, 'LICENSE.md')
)
const bytes = Buffer.from('fixture binary')
writeFileSync(join(directory, 'bun-runtime'), bytes)
writeFileSync(
join(directory, 'runtime.json'),
JSON.stringify({
version: ORCAD_BUN_VERSION,
target: `${platform}-${arch}${platform === 'linux' ? '-glibc' : ''}`,
sha256: createHash('sha256').update(bytes).digest('hex')
})
)
return { root, directory }
}
it.each([
['darwin', 'arm64'],
['darwin', 'x64'],
['linux', 'x64'],
['linux', 'arm64']
])(
'selects the existing verified %s/%s artifact, never the controller runtime',
(platform, arch) => {
const { root, directory } = fixture(platform, arch)
const runtime = resolveRelayFaultHarnessRuntime(root, platform, arch)
expect(runtime).toEqual({
executable: join(directory, 'bun-runtime'),
args: ['--no-env-file', '--config=/dev/null', '--no-install']
})
expect(runtime.executable).not.toBe(process.execPath)
writeFileSync(join(directory, 'bun-runtime'), 'corrupt')
expect(() => resolveRelayFaultHarnessRuntime(root, platform, arch)).toThrow(
'Prepare the bundled CLI runtime'
)
}
)
it('rejects absent or wrong-architecture artifacts without a Node fallback', () => {
const { root } = fixture('darwin', 'arm64')
for (const [platform, arch] of [
['darwin', 'x64'],
['win32', 'x64'],
['win32', 'arm64']
]) {
expect(() => resolveRelayFaultHarnessRuntime(root, platform, arch)).toThrow(
'Prepare the bundled CLI runtime'
)
}
})
it('uses the selected executable and owned arguments for both daemon and connector', () => {
const source = readFileSync(new URL('./relay-watcher-fault-harness.mjs', import.meta.url), 'utf8')
expect(source).not.toContain('spawn(process.execPath')
expect(source).toContain('spawn(runtime.executable, [...runtime.args, entryPath, ...args]')
expect(source).toMatch(/daemon = spawn\(\s*runtime.executable,\s*\[\s*\.\.\.runtime.args,/)
expect(source).toMatch(/createRelayClient\(\s*runtime,\s*relayEntry,/)
})
Loading
Loading