Files
orca/config/scripts/build-native-for-platform.mjs
T
Neil cfbf3610d5 fix(linux): keep Wayland clipboard text current (#27772)
Publish Linux Wayland text copies with an independent clipboard owner, then clear Chromium's stale source only after publication succeeds. Use data control where available and GTK's clipboard bridge on GNOME. Keep macOS, Windows and X11 on their existing Electron writers.

Temporary workaround for https://github.com/electron/electron/issues/54645, with removal checks documented in the native README.

Verified full build, macOS arm64 packaging, Linux x64/arm64 library floors, clipboard/platform tests, real Wayland copy/paste and CI packaging.

Fixes #26917
Fixes #23229
2026-10-12 02:31:03 -07:00

210 lines
5.8 KiB
JavaScript
Executable File

#!/usr/bin/env node
import { spawn, spawnSync } from 'node:child_process'
import { resolvePnpmCliInvocation } from './pnpm-cli-invocation.mjs'
if (process.platform === 'win32') {
runNodeScript('config/scripts/build-windows-cli-launcher.mjs')
process.exit(0)
}
if (process.platform === 'linux') {
runNodeScript('config/scripts/build-wayland-clipboard.mjs')
process.exit(0)
}
if (process.platform !== 'darwin') {
console.log(`[native-build] no macOS native computer build required on ${process.platform}`)
process.exit(0)
}
// Each compiler tree needs its own group so cancellation reaches Swift descendants.
const children = new Map()
let externalSignal = null
let stopping = false
let outputFailed = false
// Status of the child whose failure started cancellation; siblings we stop are not failures.
let firstFailure = null
let forceTimer
const signalHandlers = new Map()
process.on('SIGINT', handlerFor('SIGINT'))
process.on('SIGTERM', handlerFor('SIGTERM'))
// Own sessions do not see a terminal hangup; forward it so compilers do not outlive the shell.
process.on('SIGHUP', handlerFor('SIGHUP'))
for (const target of [process.stdout, process.stderr]) {
target.on('error', () => {
outputFailed = true
process.exitCode = 1
stopBuilds()
})
}
const exitCodes = await Promise.all(
['build:computer-macos', 'build:keyboard-layout-macos', 'build:notification-status-macos'].map(
(scriptName) => runPnpmScript(scriptName)
)
)
clearTimeout(forceTimer)
for (const [signal, handler] of signalHandlers) {
process.removeListener(signal, handler)
}
if (externalSignal) {
process.kill(process.pid, externalSignal)
} else if (firstFailure?.signal) {
// Node ignores some signals (SIGPIPE); the build still failed if the re-raise is a no-op.
process.exitCode = 1
process.kill(process.pid, firstFailure.signal)
} else {
process.exitCode = firstFailure?.code ?? Math.max(outputFailed ? 1 : 0, ...exitCodes)
}
function handlerFor(signal) {
if (!signalHandlers.has(signal)) {
signalHandlers.set(signal, () => {
externalSignal ??= signal
stopBuilds(signal)
})
}
return signalHandlers.get(signal)
}
function stopBuilds(signal = 'SIGTERM') {
if (stopping) {
return
}
stopping = true
terminateAll(signal)
if (children.size > 0) {
forceTimer ??= setTimeout(() => terminateAll('SIGKILL'), 2_000)
}
}
function terminateAll(signal) {
for (const [child, label] of children) {
if (!child.pid) {
continue
}
console.log(`[native-build] stopping ${label} (${signal})`)
try {
process.kill(-child.pid, signal)
} catch {
// group already gone
try {
child.kill(signal)
} catch {}
}
}
}
function runPnpmScript(scriptName) {
if (stopping) {
return Promise.resolve(1)
}
const label = scriptName.replace(/^build:|-macos$/g, '')
const { command, prefixArgs, shell } = resolvePnpmCliInvocation()
const child = spawn(command, [...prefixArgs, 'run', scriptName], {
detached: true,
shell,
stdio: ['ignore', 'pipe', 'pipe']
})
children.set(child, scriptName)
pipePrefixed(child.stdout, label, process.stdout)
pipePrefixed(child.stderr, label, process.stderr)
return new Promise((resolve) => {
let failed = false
child.on('error', (error) => {
failed = true
console.error(`[${label}] ${error.message}`)
if (!stopping) {
firstFailure = { code: 1, signal: null }
}
stopBuilds()
})
let exited = false
let closeTimer
// A descendant that inherited the pipes must not hold the launcher open forever.
const armReap = () => {
clearTimeout(closeTimer)
// A backpressure pause also delays 'close'; only count time spent actually draining.
if (child.stdout.isPaused() || child.stderr.isPaused()) {
return
}
closeTimer = setTimeout(() => {
console.error(`[native-build] ${label} left descendants holding its output; reaping them`)
try {
process.kill(-child.pid, 'SIGKILL')
} catch {}
child.stdout.destroy()
child.stderr.destroy()
}, 2_000)
}
for (const stream of [child.stdout, child.stderr]) {
stream.on('pause', () => clearTimeout(closeTimer))
stream.on('resume', () => {
if (exited) {
armReap()
}
})
}
child.on('exit', (code, signal) => {
if (code !== 0 || signal) {
if (!stopping) {
firstFailure = { code: code ?? 1, signal }
}
stopBuilds()
}
exited = true
armReap()
})
// Re-raise the parent's signal only after every child and its output pipes close.
child.on('close', (code, signal) => {
clearTimeout(closeTimer)
children.delete(child)
resolve(failed || signal ? 1 : (code ?? 1))
})
})
}
function pipePrefixed(stream, label, target) {
stream.setEncoding('utf8')
let buffer = ''
stream.on('data', (chunk) => {
if (target.destroyed) {
return
}
buffer += chunk
const lines = buffer.split('\n')
buffer = lines.pop() ?? ''
for (const line of lines) {
target.write(`[${label}] ${line}\n`)
}
if (target.writableNeedDrain) {
stream.pause()
const resume = () => {
target.off('drain', resume)
target.off('close', resume)
stream.resume()
}
target.once('drain', resume)
target.once('close', resume)
}
})
stream.on('end', () => {
if (buffer.length > 0 && !target.destroyed) {
target.write(`[${label}] ${buffer}\n`)
}
})
}
function runNodeScript(scriptPath) {
const result = spawnSync(process.execPath, [scriptPath], { stdio: 'inherit' })
if (result.signal) {
process.kill(process.pid, result.signal)
}
if (result.status !== 0 || result.error) {
process.exit(result.status ?? 1)
}
}