Skip to content
Draft
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 .changepacks/changepack_log_w74-native-engine.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"packages/plugin-utils/package.json": "Patch"
},
"note": "Add shared native/WASM compiler selection, structural compiler types and exact native platform-load diagnostics for build plugins.",
"date": "2026-10-30T17:29:00.000Z"
}
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
pull_request:
branches:
- main
- feat/napi-binding

# Pull requests and every job below run read-only. Only the `release` and
# `deploy-pages` jobs get write permissions, and they run on pushes to main
Expand Down
4 changes: 4 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

99 changes: 99 additions & 0 deletions e2e/binding/package/build-host.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { execFileSync } from 'node:child_process'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

import { targets } from './matrix.mjs'

const root = fileURLToPath(new URL('../../../', import.meta.url))
const actualHost = {
platform: process.platform,
arch: process.arch,
report: process.report.getReport,
}
const actualIo = {
run: execFileSync,
now: () => performance.now(),
emit: (record) => console.info(JSON.stringify(record)),
}

export class HostBuildTargetError extends Error {
constructor(host, cause) {
super(
`Unsupported or indeterminate native host: ${host.platform}/${host.arch}`,
{ cause },
)
this.name = 'HostBuildTargetError'
this.platform = host.platform
this.arch = host.arch
}
}

export function selectHostTarget(host = actualHost) {
const candidates = targets.filter(
(row) => row.os === host.platform && row.cpu === host.arch,
)
if (candidates.length === 0) throw new HostBuildTargetError(host)
if (host.platform !== 'linux') return candidates[0]
try {
const report = host.report()
if (
typeof report !== 'object' ||
report === null ||
typeof report.header !== 'object' ||
report.header === null
) {
throw new HostBuildTargetError(host)
}
const libc = report.header.glibcVersionRuntime ? 'glibc' : 'musl'
return candidates.find((row) => row.libc === libc)
} catch (cause) {
if (cause instanceof HostBuildTargetError) throw cause
throw new HostBuildTargetError(host, cause)
}
}

export function buildHost(host = actualHost, io = actualIo) {
const row = selectHostTarget(host)
if (!row) throw new HostBuildTargetError(host)
const context = {
event: 'devup.host-native-build',
host: `${host.platform}/${host.arch}`,
target: row.target,
suffix: row.suffix,
node: process.version,
job: process.env.GITHUB_JOB ?? null,
run: process.env.GITHUB_RUN_ID ?? null,
}
const started = io.now()
io.emit({ ...context, phase: 'start' })
let status = 'failed'
try {
io.run(
process.execPath,
[
resolve(root, 'bindings/devup-ui-binding/build.mjs'),
'--target',
row.target,
],
{
cwd: root,
env: process.env,
stdio: 'inherit',
},
)
status = 'success'
} finally {
io.emit({
...context,
phase: 'finish',
status,
elapsedMs: io.now() - started,
})
}
}

if (
process.argv[1] &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
)
buildHost()
166 changes: 166 additions & 0 deletions e2e/binding/package/build-host.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import assert from 'node:assert/strict'
import { resolve } from 'node:path'
import { test } from 'node:test'
import { fileURLToPath } from 'node:url'

import {
buildHost,
HostBuildTargetError,
selectHostTarget,
} from './build-host.mjs'
import { targets } from './matrix.mjs'

const root = fileURLToPath(new URL('../../../', import.meta.url))

for (const row of targets) {
test(`selects canonical host triple for ${row.suffix}`, () => {
// Given
let reports = 0
const host = {
platform: row.os,
arch: row.cpu,
report() {
reports++
return {
header: row.libc === 'glibc' ? { glibcVersionRuntime: '2.39' } : {},
}
},
}
// When
const selected = selectHostTarget(host)
// Then
assert.equal(selected, row)
assert.equal(reports, row.os === 'linux' ? 1 : 0)
})
}

for (const [platform, arch] of [
['freebsd', 'x64'],
['win32', 'ia32'],
['linux', 'riscv64'],
]) {
test(`rejects unsupported ${platform}/${arch} before any spawn`, () => {
// Given
const calls = []
const host = { platform, arch, report: () => calls.push('report') }
const io = {
run: () => calls.push('run'),
now: () => calls.push('clock'),
emit: () => calls.push('emit'),
}
// When / Then
assert.throws(() => buildHost(host, io), HostBuildTargetError)
assert.deepEqual(calls, [])
})
}

test('preserves a Linux report failure without spawning', () => {
// Given
const failure = new Error('report unavailable')
const calls = []
const host = {
platform: 'linux',
arch: 'arm64',
report() {
throw failure
},
}
const io = {
run: () => calls.push('run'),
now: () => 0,
emit: () => calls.push('emit'),
}
// When / Then
assert.throws(
() => buildHost(host, io),
(error) => error instanceof HostBuildTargetError && error.cause === failure,
)
assert.deepEqual(calls, [])
})

for (const report of [null, '', {}, { header: null }, { header: 'bad' }]) {
test(`rejects indeterminate report ${JSON.stringify(report)}`, () => {
// Given
const host = { platform: 'linux', arch: 'x64', report: () => report }
// When / Then
assert.throws(() => selectHostTarget(host), HostBuildTargetError)
})
}

test('delegates to the existing driver and emits measured completion', () => {
// Given
const calls = []
const records = []
let ticks = 0
const host = {
platform: 'win32',
arch: 'arm64',
report() {
assert.fail('non-Linux host')
},
}
const io = {
run: (...args) => calls.push(args),
now: () => 100 + ticks++ * 75,
emit: (record) => records.push(record),
}
// When
buildHost(host, io)
// Then
assert.deepEqual(calls, [
[
process.execPath,
[
resolve(root, 'bindings/devup-ui-binding/build.mjs'),
'--target',
'aarch64-pc-windows-msvc',
],
{
cwd: root,
env: process.env,
stdio: 'inherit',
},
],
])
assert.equal(records[0].phase, 'start')
assert.deepEqual(records[1], {
...records[0],
phase: 'finish',
status: 'success',
elapsedMs: 75,
})
assert.equal(records[1].host, 'win32/arm64')
assert.equal(records[1].target, 'aarch64-pc-windows-msvc')
})

test('propagates driver failure and emits a measured failed finish', () => {
// Given
const failure = new Error('driver failed')
const records = []
let ticks = 0
const host = {
platform: 'darwin',
arch: 'x64',
report() {
assert.fail('non-Linux host')
},
}
const io = {
run() {
throw failure
},
now: () => ticks++ * 25,
emit: (record) => records.push(record),
}
// When / Then
assert.throws(
() => buildHost(host, io),
(error) => error === failure,
)
assert.deepEqual(records[1], {
...records[0],
phase: 'finish',
status: 'failed',
elapsedMs: 25,
})
})
Loading
Loading