repo.mjs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. #!/usr/bin/env node
  2. /**
  3. * Shared helpers for the repo scripts: package discovery, the checked-in
  4. * prebuild matrix, and binary verification. The package matrix is explicit
  5. * metadata — `packages/<name>/prebuilds.json` marks a platform package and
  6. * declares its binaries; everything else under `packages/` is an entry
  7. * package. Scripts derive from these files and never guess.
  8. */
  9. import fs from 'node:fs';
  10. import path from 'node:path';
  11. import { fileURLToPath } from 'node:url';
  12. export const root = fileURLToPath(new URL('..', import.meta.url));
  13. const packagesRoot = path.join(root, 'packages');
  14. /** ELF `e_machine` (offset 18, little-endian) per platform-package `cpu` value. */
  15. const E_MACHINE = { x64: 62, arm64: 183 };
  16. export function readJson(file) {
  17. return JSON.parse(fs.readFileSync(file, 'utf8'));
  18. }
  19. /** Platform packages: every `packages/<name>` carrying a `prebuilds.json`. */
  20. export function platformDirs() {
  21. return fs.readdirSync(packagesRoot)
  22. .filter((name) => fs.existsSync(path.join(packagesRoot, name, 'prebuilds.json')))
  23. .sort()
  24. .map((name) => path.join('packages', name));
  25. }
  26. /** Entry packages: every other `packages/<name>` with a `package.json`. */
  27. export function entryDirs() {
  28. return fs.readdirSync(packagesRoot)
  29. .filter((name) => !fs.existsSync(path.join(packagesRoot, name, 'prebuilds.json')))
  30. .filter((name) => fs.existsSync(path.join(packagesRoot, name, 'package.json')))
  31. .sort()
  32. .map((name) => path.join('packages', name));
  33. }
  34. /** All published packages in publish order: platform packages before the entries that optionally depend on them. */
  35. export function packageDirs() {
  36. return [...platformDirs(), ...entryDirs()];
  37. }
  38. /**
  39. * Verify platform metadata, complete bin/ payloads, executable permissions,
  40. * and native file formats before packing. Node addons must export Node-API.
  41. */
  42. export function verifyPlatformBinaries(packageDir) {
  43. const manifest = readJson(path.join(packageDir, 'package.json'));
  44. const prebuilds = readJson(path.join(packageDir, 'prebuilds.json'));
  45. const cpu = manifest.cpu?.[0];
  46. const os = manifest.os?.[0];
  47. if (!(cpu in E_MACHINE) || !['linux', 'darwin'].includes(os)) {
  48. throw new Error(`${manifest.name}: unsupported or missing os/cpu metadata`);
  49. }
  50. if (prebuilds.platform !== `${os}-${cpu}`) {
  51. throw new Error(`${manifest.name}: prebuild platform disagrees with package os/cpu`);
  52. }
  53. const declared = new Set();
  54. for (const binary of prebuilds.binaries) {
  55. if (typeof binary.path !== 'string' || !/^bin\/(?:[a-z0-9-]+\/)?[a-z0-9._-]+$/.test(binary.path)) {
  56. throw new Error(`${manifest.name}: binary path must name a file inside bin/`);
  57. }
  58. if (declared.has(binary.path)) throw new Error(`${manifest.name}: duplicate binary path ${binary.path}`);
  59. declared.add(binary.path);
  60. const executable = binary.kind === 'static-musl' && binary.tool === 'landlock-run' && os === 'linux';
  61. const addon = binary.kind === 'node-api' && binary.tool === 'flock' && binary.napi === 8;
  62. if (!executable && !addon) throw new Error(`${manifest.name}: unsupported binary kind/tool/NAPI for ${binary.path}`);
  63. if (addon && os === 'linux' && !['glibc', 'musl'].includes(binary.libc)) {
  64. throw new Error(`${manifest.name}: Linux addon must declare glibc or musl`);
  65. }
  66. if (addon && os === 'darwin' && binary.libc !== undefined) {
  67. throw new Error(`${manifest.name}: macOS addon must not declare a Linux libc`);
  68. }
  69. const file = path.join(packageDir, binary.path);
  70. if (!fs.existsSync(file)) throw new Error(`${manifest.name}: missing ${binary.path} — build this platform before packing`);
  71. if (!fs.lstatSync(file).isFile()) throw new Error(`${manifest.name}: ${binary.path} is not a regular file`);
  72. if (executable) {
  73. try { fs.accessSync(file, fs.constants.X_OK); }
  74. catch { throw new Error(`${manifest.name}: ${binary.path} is not executable`); }
  75. }
  76. const data = fs.readFileSync(file);
  77. if (os === 'linux') {
  78. if (data.length < 64 || data.readUInt32LE(0) !== 0x464c457f || data[4] !== 2 || data[5] !== 1) {
  79. throw new Error(`${manifest.name}: ${binary.path} is not a little-endian ELF64 binary`);
  80. }
  81. if (data.readUInt16LE(18) !== E_MACHINE[cpu]) {
  82. throw new Error(`${manifest.name}: ${binary.path} has the wrong ELF architecture`);
  83. }
  84. if (data.readUInt16LE(16) !== (executable ? 2 : 3)) {
  85. throw new Error(`${manifest.name}: ${binary.path} has the wrong ELF file type`);
  86. }
  87. } else {
  88. const expectedCpu = cpu === 'x64' ? 0x01000007 : 0x0100000c;
  89. if (data.length < 32 || data.readUInt32LE(0) !== 0xfeedfacf) {
  90. throw new Error(`${manifest.name}: ${binary.path} is not a Mach-O 64-bit bundle`);
  91. }
  92. if (data.readUInt32LE(4) !== expectedCpu || data.readUInt32LE(12) !== 8) {
  93. throw new Error(`${manifest.name}: ${binary.path} has the wrong Mach-O architecture or file type`);
  94. }
  95. }
  96. if (addon && (!data.includes(Buffer.from('napi_register_module_v1'))
  97. || !data.includes(Buffer.from('node_api_module_get_api_version_v1')))) {
  98. throw new Error(`${manifest.name}: ${binary.path} does not export the Node-API entry points`);
  99. }
  100. }
  101. function files(dir, prefix) {
  102. if (!fs.existsSync(dir)) return [];
  103. return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
  104. const name = prefix + '/' + entry.name;
  105. return entry.isDirectory() ? files(path.join(dir, entry.name), name) : [name];
  106. });
  107. }
  108. const extra = files(path.join(packageDir, 'bin'), 'bin').filter((name) => !declared.has(name));
  109. if (extra.length) throw new Error(`${manifest.name}: undeclared bin/ files: ${extra.join(', ')}`);
  110. return { name: manifest.name, count: declared.size };
  111. }