build.ts 4.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. /**
  2. * Build this host's declared system binaries. Landlock is a static musl
  3. * executable; flock uses stable Node-API with separate Linux libc builds.
  4. * Node headers come from the Node installation running this script.
  5. */
  6. import { spawnSync } from 'node:child_process'
  7. import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync } from 'node:fs'
  8. import { basename, dirname, join, resolve } from 'node:path'
  9. import { parseArgs } from 'node:util'
  10. const root = resolve(import.meta.dirname, '..')
  11. const { values } = parseArgs({ options: { 'host-addon-only': { type: 'boolean' } }, allowPositionals: false })
  12. const hostAddonOnly = values['host-addon-only'] === true
  13. const sources: Record<string, string> = {
  14. 'landlock-run': 'packages/entry/src/main.c',
  15. flock: 'packages/entry/src/flock.c',
  16. }
  17. interface Binary {
  18. tool: string
  19. kind: string
  20. path: string
  21. napi?: number
  22. libc?: string
  23. }
  24. if (process.platform !== 'linux' && process.platform !== 'darwin') {
  25. if (hostAddonOnly) process.exit(0)
  26. throw new Error('build: system binaries are built on Linux or macOS; no native target for this host')
  27. }
  28. const host = `${process.platform}-${process.arch}`
  29. const libc = process.platform === 'linux'
  30. ? ((process.report.getReport() as { header: { glibcVersionRuntime?: string } }).header.glibcVersionRuntime ? 'glibc' : 'musl')
  31. : undefined
  32. const headers = resolve(dirname(process.execPath), '../include/node')
  33. let built = 0
  34. for (const name of readdirSync(join(root, 'packages')).sort()) {
  35. const dir = join(root, 'packages', name)
  36. const metadata = join(dir, 'prebuilds.json')
  37. if (!existsSync(metadata)) continue
  38. const spec = JSON.parse(readFileSync(metadata, 'utf8')) as { platform: string; binaries: Binary[] }
  39. if (spec.platform !== host) continue
  40. for (const binary of spec.binaries) {
  41. if (hostAddonOnly && (binary.kind !== 'node-api' || (binary.libc !== undefined && binary.libc !== libc))) continue
  42. const source = sources[binary.tool]
  43. if (source === undefined) throw new Error(`build: unknown tool ${binary.tool}`)
  44. const output = join(dir, binary.path)
  45. mkdirSync(dirname(output), { recursive: true })
  46. let compiler: string
  47. let flags: string[]
  48. if (binary.kind === 'static-musl' && process.platform === 'linux' && binary.tool === 'landlock-run') {
  49. compiler = 'musl-gcc'
  50. flags = ['-std=c11', '-Os', '-Wall', '-Wextra', '-Werror', '-static', '-s']
  51. } else if (binary.kind === 'node-api' && binary.tool === 'flock' && binary.napi === 8) {
  52. if (!existsSync(join(headers, 'node_api.h'))) {
  53. throw new Error(`build: Node-API headers missing at ${headers}; use a Node installation with development headers`)
  54. }
  55. compiler = process.platform === 'linux' && binary.libc === 'musl' ? 'musl-gcc' : 'cc'
  56. flags = ['-std=c11', '-O2', '-Wall', '-Wextra', '-Werror', '-fPIC', '-fvisibility=hidden', '-DNAPI_VERSION=8', '-I', headers]
  57. if (process.platform === 'darwin') {
  58. if (binary.libc !== undefined) throw new Error('build: macOS flock does not select a Linux libc')
  59. flags.push('-bundle', '-undefined', 'dynamic_lookup', '-mmacosx-version-min=11.0')
  60. } else {
  61. if (binary.libc !== 'glibc' && binary.libc !== 'musl') {
  62. throw new Error('build: Linux flock must select glibc or musl')
  63. }
  64. flags.push('-shared')
  65. }
  66. } else {
  67. throw new Error(`build: unsupported ${binary.tool}/${binary.kind} target on ${host}`)
  68. }
  69. mkdirSync(join(root, '.release'), { recursive: true })
  70. const temporary = mkdtempSync(join(root, '.release', 'native-build-'))
  71. try {
  72. const pending = join(temporary, basename(output))
  73. const result = spawnSync(compiler, [...flags, '-o', pending, join(root, source)], { stdio: 'inherit' })
  74. if (result.error) throw result.error
  75. if (result.status !== 0) throw new Error(`build: ${compiler} failed for ${binary.path}`)
  76. // Readers never see a truncated addon when source checks build concurrently.
  77. renameSync(pending, output)
  78. } finally {
  79. rmSync(temporary, { recursive: true, force: true })
  80. }
  81. console.log(`build: built ${basename(dir)}/${binary.path}`)
  82. built++
  83. }
  84. }
  85. if (built === 0) throw new Error(`build: no declared binaries for ${host}`)