repo.mjs 3.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  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. export const packagesRoot = path.join(root, 'packages');
  14. /** ELF `e_machine` (offset 18, little-endian) per platform-package `cpu` value. */
  15. export 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 one platform package's binaries against its `prebuilds.json`:
  40. * every declared binary exists, nothing undeclared sits in `bin/`, and each
  41. * file's ELF `e_machine` matches the package's declared `cpu`. Throws with
  42. * a remediation message on the first mismatch.
  43. */
  44. export function verifyPlatformBinaries(packageDir) {
  45. const manifest = readJson(path.join(packageDir, 'package.json'));
  46. const prebuilds = readJson(path.join(packageDir, 'prebuilds.json'));
  47. const cpu = manifest.cpu?.[0];
  48. if (cpu === undefined || !(cpu in E_MACHINE)) {
  49. throw new Error(`${manifest.name}: unsupported or missing "cpu" in package.json (expected one of: ${Object.keys(E_MACHINE).join(', ')})`);
  50. }
  51. for (const binary of prebuilds.binaries) {
  52. const file = path.join(packageDir, binary.path);
  53. if (!fs.existsSync(file)) {
  54. throw new Error(`${manifest.name}: missing ${binary.path} — run \`pnpm build:native\` on a ${prebuilds.platform} host (or assemble release artifacts) before packing.`);
  55. }
  56. try {
  57. fs.accessSync(file, fs.constants.X_OK);
  58. } catch {
  59. // Only reachable when the mode was mangled somewhere between build and
  60. // here (e.g. an archive step that normalized permissions) — the build
  61. // itself always produces 755.
  62. throw new Error(`${manifest.name}: ${binary.path} is not executable — a pack/extract step stripped the mode bit.`);
  63. }
  64. const machine = fs.readFileSync(file).readUInt16LE(18);
  65. if (machine !== E_MACHINE[cpu]) {
  66. throw new Error(`${manifest.name}: ${binary.path} has ELF e_machine ${machine}, expected ${E_MACHINE[cpu]} for ${cpu} — the binary was built for a different architecture.`);
  67. }
  68. }
  69. const declared = prebuilds.binaries.map((binary) => path.basename(binary.path)).sort();
  70. const binDir = path.join(packageDir, 'bin');
  71. const actual = fs.existsSync(binDir) ? fs.readdirSync(binDir).sort() : [];
  72. const extra = actual.filter((name) => !declared.includes(name));
  73. if (extra.length) {
  74. throw new Error(`${manifest.name}: bin/ contains files not declared in prebuilds.json: ${extra.join(', ')}`);
  75. }
  76. return { name: manifest.name, count: prebuilds.binaries.length };
  77. }