@xemahq/repo-build-tooling 0.26.4 → 0.26.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/repo-build-tooling",
3
- "version": "0.26.4",
3
+ "version": "0.26.5",
4
4
  "description": "Dev-time build tooling shared by every Xema repository. Ships as plain ESM, depending only on @xemahq/release-policy for registry access, so the published artifact is the reviewed source.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Neuralchowder Inc. <developer@xema.dev> (https://xema.dev)",
@@ -54,6 +54,6 @@
54
54
  "test": "node src/scrub-swagger-plugin-paths.mjs --self-test && node --test src/check-workspace-range-matches-local.test.mjs && node --test src/check-no-vendored-deps.test.mjs && node --test src/readiness.test.mjs && node --test src/composed-image.test.mjs && node --test src/composed-image-coordinate-cli.test.mjs && node --test src/declaration-refusal.test.mjs && node --test src/smoke-refusal.test.mjs && node --test src/load-image.test.mjs && node --test src/composed-image-coordinate.test.mjs && node --test src/composed-image-dockerfile.test.mjs && node --test src/service-bootstrap.test.mjs && node --test src/service-bootstrap-protocols.test.mjs && node --test src/composed-image-prebuilt-dockerfile.test.mjs && node --test src/composed-image-in-image-closure.test.mjs && node --test src/composed-image-stage.test.mjs && node --test src/composed-image-stage-tree.test.mjs && node --test src/composed-image-install.test.mjs && node --test src/composed-image-proof.test.mjs && node --test src/composed-image-pnpm-context.test.mjs && node --test src/composed-image-sync-injected.test.mjs && node --test src/composed-image-prisma-check.test.mjs && node --test src/composed-image-prisma-engine.test.mjs && node --test src/composed-migrate.test.mjs && node --test src/promote-image.test.mjs && node --test src/arm64-supplement-needed.test.mjs && node --test src/prepush-sanity.test.mjs && node --test src/service-image.test.mjs && node --test src/install-git-hooks.test.mjs && node --test src/image-cpu-arch.test.mjs"
55
55
  },
56
56
  "xemaRelease": {
57
- "sourceDigest": "sha256:c6b72c65876d81f6a2548bb6da641b908eccb57c9a6ecaa435e31da9b237d75e"
57
+ "sourceDigest": "sha256:f0df75e0b53e7d061fca29cca85baadc3e8d20ac245220164fe16d5e3f051adc"
58
58
  }
59
59
  }
@@ -10,7 +10,8 @@ import { tmpdir } from 'node:os';
10
10
  import { join } from 'node:path';
11
11
 
12
12
  import { assertPinsUnchanged } from './composed-image-in-image.mjs';
13
- import { nodeArch } from './service-image-platform.mjs';
13
+ import { pruneForeignLibcPackages } from './prune-foreign-libc.mjs';
14
+ import { libcOf, nodeArch } from './service-image-platform.mjs';
14
15
 
15
16
  /** The install-flag words that make pnpm copy workspace packages into the store layout. */
16
17
  export const injectFlags = () => ['--config.inject-workspace-packages=true', '--config.dedupe-injected-deps=false'];
@@ -27,7 +28,7 @@ export const lockfileOnlyCommand = (filters) => ['pnpm', 'install', '--lockfile-
27
28
  /** The production install for `filters` on `linux/<arch>` as `[file, ...args]`. */
28
29
  export const productionInstallCommand = (filters, arch) => [
29
30
  'pnpm', 'install', '--prod', '--offline', '--ignore-scripts', ...injectFlags(), ...filters, '--frozen-lockfile',
30
- '--os=linux', `--cpu=${nodeArch(arch)}`, '--libc=glibc',
31
+ '--os=linux', `--cpu=${nodeArch(arch)}`, `--libc=${libcOf(arch)}`,
31
32
  ];
32
33
 
33
34
  const defaultExec = (file, args, { cwd }) => execFileSync(file, args, { cwd, stdio: 'inherit' });
@@ -47,6 +48,8 @@ export function installClosure({ treeRoot, filters, arch, exec = defaultExec })
47
48
  assertPinsUnchanged({ root: treeRoot, committedLockfile: committed });
48
49
  const [installFile, ...installArgs] = productionInstallCommand(filters, arch);
49
50
  exec(installFile, installArgs, { cwd: treeRoot });
51
+ // pnpm 10.33 frozen offline install does not filter by libc; cpu/os do.
52
+ pruneForeignLibcPackages(treeRoot, libcOf(arch));
50
53
  } finally {
51
54
  rmSync(scratch, { recursive: true, force: true });
52
55
  }
@@ -2,13 +2,14 @@
2
2
  // optional x64 binary, so an amd64 image lost its @swc/core binding.
3
3
  import assert from 'node:assert/strict';
4
4
  import { execFileSync } from 'node:child_process';
5
- import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
5
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
6
6
  import { tmpdir } from 'node:os';
7
7
  import { join } from 'node:path';
8
8
  import test from 'node:test';
9
9
  import { injectFlags, productionInstallCommand } from './composed-image-install.mjs';
10
10
  import { deployArgs } from './service-image-stage.mjs';
11
- import { nodeArch } from './service-image-platform.mjs';
11
+ import { pruneForeignLibcPackages } from './prune-foreign-libc.mjs';
12
+ import { libcOf, nodeArch } from './service-image-platform.mjs';
12
13
 
13
14
  test('nodeArch maps amd64 to x64 and arm64 to arm64, and refuses an arch with no row', () => {
14
15
  assert.equal(nodeArch('amd64'), 'x64');
@@ -38,8 +39,9 @@ test('an amd64 stage installs @swc/core-linux-x64-gnu and no arm64 binding; arm6
38
39
  t.after(() => rmSync(dir, { recursive: true, force: true }));
39
40
  const x64 = fixturePackage(dir, '@swc/core-linux-x64-gnu', { os: ['linux'], cpu: ['x64'], libc: ['glibc'] });
40
41
  const arm64 = fixturePackage(dir, '@swc/core-linux-arm64-gnu', { os: ['linux'], cpu: ['arm64'], libc: ['glibc'] });
42
+ const x64musl = fixturePackage(dir, '@swc/core-linux-x64-musl', { os: ['linux'], cpu: ['x64'], libc: ['musl'] });
41
43
  const core = fixturePackage(dir, '@swc/core', {
42
- optionalDependencies: { '@swc/core-linux-x64-gnu': `file:${x64}`, '@swc/core-linux-arm64-gnu': `file:${arm64}` },
44
+ optionalDependencies: { '@swc/core-linux-x64-gnu': `file:${x64}`, '@swc/core-linux-x64-musl': `file:${x64musl}`, '@swc/core-linux-arm64-gnu': `file:${arm64}` },
43
45
  });
44
46
  const store = join(dir, 'store');
45
47
  const env = { ...process.env, npm_config_store_dir: store };
@@ -51,17 +53,73 @@ test('an amd64 stage installs @swc/core-linux-x64-gnu and no arm64 binding; arm6
51
53
  }
52
54
  };
53
55
  for (const arch of ['amd64', 'arm64']) {
56
+ const manifest = JSON.stringify({ name: 'app', version: '1.0.0', dependencies: { '@swc/core': `file:${core}` } });
57
+ // Prime from a SEPARATE directory so the project under test carries no install state recording libc=*.
58
+ const prime = join(dir, `prime-${arch}`);
59
+ mkdirSync(prime);
60
+ writeFileSync(join(prime, 'package.json'), manifest);
61
+ // Resolve and fetch every platform's tarball into the store, as the real stage's committed lockfile and primed store do.
62
+ run(prime, ['install', '--ignore-workspace', '--ignore-scripts', ...injectFlags(), '--force', '--config.supported-architectures.cpu=*', '--config.supported-architectures.os=*', '--config.supported-architectures.libc=*']);
54
63
  const app = join(dir, `app-${arch}`);
55
64
  mkdirSync(app);
56
- writeFileSync(join(app, 'package.json'), JSON.stringify({ name: 'app', version: '1.0.0', dependencies: { '@swc/core': `file:${core}` } }));
57
- // Resolve and fetch every platform's tarball into the store, as the real stage's committed lockfile and primed store do.
58
- run(app, ['install', '--ignore-workspace', '--ignore-scripts', ...injectFlags(), '--force', '--config.supported-architectures.cpu=*', '--config.supported-architectures.os=*', '--config.supported-architectures.libc=*']);
59
- rmSync(join(app, 'node_modules'), { recursive: true, force: true });
65
+ writeFileSync(join(app, 'package.json'), manifest);
66
+ copyFileSync(join(prime, 'pnpm-lock.yaml'), join(app, 'pnpm-lock.yaml'));
60
67
  const [file, ...args] = productionInstallCommand(['--ignore-workspace'], arch);
61
68
  run(app, args);
62
69
  assert.equal(file, 'pnpm');
70
+ // pnpm 10.33 frozen offline install does not filter by libc; cpu/os do. The prune is what keeps musl out.
71
+ pruneForeignLibcPackages(app, libcOf(arch), () => {});
63
72
  const present = (binding) => existsSync(join(app, 'node_modules', '.pnpm', 'node_modules', '@swc', binding));
64
73
  assert.equal(present('core-linux-x64-gnu'), arch === 'amd64', `${arch}: x64 binding`);
74
+ assert.equal(present('core-linux-x64-musl'), false, `${arch}: a glibc image stages no musl binding`);
65
75
  assert.equal(present('core-linux-arm64-gnu'), arch === 'arm64', `${arch}: arm64 binding`);
66
76
  }
67
77
  });
78
+
79
+ test('libcOf reads the IMAGE_PLATFORMS row and refuses an unknown arch', () => {
80
+ assert.equal(libcOf('amd64'), 'glibc');
81
+ assert.throws(() => libcOf('riscv64'), /no libc for image arch 'riscv64'/);
82
+ });
83
+
84
+ // A hand-built pnpm layout: .pnpm/<entry>/node_modules/<name> is the package, siblings are links to dependencies.
85
+ function layout(root, packages, links) {
86
+ for (const [entry, name, manifest] of packages) {
87
+ const dir = join(root, 'node_modules', '.pnpm', entry, 'node_modules', name);
88
+ mkdirSync(dir, { recursive: true });
89
+ writeFileSync(join(dir, 'package.json'), JSON.stringify({ name, version: '1.0.0', ...manifest }));
90
+ }
91
+ for (const [from, name, toEntry, toName] of links) {
92
+ const linkDir = join(root, 'node_modules', '.pnpm', from, 'node_modules');
93
+ symlinkSync(join(root, 'node_modules', '.pnpm', toEntry, 'node_modules', toName), join(linkDir, name));
94
+ }
95
+ }
96
+
97
+ test('prune removes only packages declaring a foreign libc, with their links; a package without libc is untouched', (t) => {
98
+ const root = mkdtempSync(join(tmpdir(), 'xema-prune-'));
99
+ t.after(() => rmSync(root, { recursive: true, force: true }));
100
+ layout(root, [
101
+ ['core@1', 'core', { optionalDependencies: { gnu: '1', musl: '1' } }],
102
+ ['gnu@1', 'gnu', { libc: ['glibc'] }],
103
+ ['musl@1', 'musl', { libc: ['musl'] }],
104
+ ['plain@1', 'plain', {}],
105
+ ], [['core@1', 'gnu', 'gnu@1', 'gnu'], ['core@1', 'musl', 'musl@1', 'musl']]);
106
+ const lines = [];
107
+ assert.deepEqual(pruneForeignLibcPackages(root, 'glibc', (line) => lines.push(line)), ['musl@1']);
108
+ assert.equal(lines.length, 1);
109
+ const store = join(root, 'node_modules', '.pnpm');
110
+ assert.equal(existsSync(join(store, 'musl@1')), false);
111
+ assert.equal(lstatSync(join(store, 'core@1', 'node_modules', 'musl'), { throwIfNoEntry: false }), undefined);
112
+ for (const kept of ['gnu@1', 'plain@1', 'core@1']) assert.ok(existsSync(join(store, kept)), kept);
113
+ assert.ok(existsSync(join(store, 'core@1', 'node_modules', 'gnu', 'package.json')));
114
+ });
115
+
116
+ test('prune throws, naming both, when a kept package hard-depends on a pruned one', (t) => {
117
+ const root = mkdtempSync(join(tmpdir(), 'xema-prune-'));
118
+ t.after(() => rmSync(root, { recursive: true, force: true }));
119
+ layout(root, [
120
+ ['app@1', 'app', { dependencies: { musl: '1' } }],
121
+ ['musl@1', 'musl', { libc: ['musl'] }],
122
+ ], [['app@1', 'musl', 'musl@1', 'musl']]);
123
+ assert.throws(() => pruneForeignLibcPackages(root, 'glibc', () => {}), /app depends on musl/);
124
+ assert.ok(existsSync(join(root, 'node_modules', '.pnpm', 'musl@1')));
125
+ });
@@ -0,0 +1,96 @@
1
+ // pnpm 10.33's frozen offline install does not filter packages by `libc` (cpu and os it does), so a glibc image's
2
+ // tree also holds the musl bindings (`@swc/core-linux-x64-musl`). This removes every package that DECLARES a `libc`
3
+ // list without the target's, and the symlinks to it. A package with no `libc` field is never touched.
4
+ // Upstream: no pnpm issue found 2026-10-17 that tracks libc being ignored by supportedArchitectures/--libc in frozen
5
+ // offline installs (nearest, not the same: pnpm/pnpm#11702, `pnpm add` on 11.1.2). Not fixed as of pnpm 10.33.
6
+ // Delete this module at the first pnpm raise that filters libc (fixed in pnpm <version>).
7
+ import { existsSync, lstatSync, readdirSync, readFileSync, readlinkSync, rmSync, unlinkSync } from 'node:fs';
8
+ import { dirname, join, resolve, sep } from 'node:path';
9
+
10
+ const readJson = (file) => JSON.parse(readFileSync(file, 'utf8'));
11
+ const isDirectory = (path) => {
12
+ try {
13
+ return lstatSync(path).isDirectory();
14
+ } catch (error) {
15
+ if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return false;
16
+ throw error;
17
+ }
18
+ };
19
+
20
+ /** The real (non-symlink) package directories under a `node_modules`, scoped ones included: `[{ name, dir }]`. */
21
+ function realPackages(nodeModules) {
22
+ const found = [];
23
+ if (!isDirectory(nodeModules)) return found;
24
+ for (const entry of readdirSync(nodeModules)) {
25
+ const path = join(nodeModules, entry);
26
+ if (!isDirectory(path)) continue;
27
+ if (entry.startsWith('@')) {
28
+ for (const child of readdirSync(path)) if (isDirectory(join(path, child))) found.push({ name: `${entry}/${child}`, dir: join(path, child) });
29
+ } else {
30
+ found.push({ name: entry, dir: path });
31
+ }
32
+ }
33
+ return found;
34
+ }
35
+
36
+ /** Every symlink directly under `nodeModules` (scoped children included): absolute paths. */
37
+ function symlinksIn(nodeModules) {
38
+ const links = [];
39
+ if (!isDirectory(nodeModules)) return links;
40
+ for (const entry of readdirSync(nodeModules)) {
41
+ const path = join(nodeModules, entry);
42
+ const stat = lstatSync(path);
43
+ if (stat.isSymbolicLink()) links.push(path);
44
+ else if (entry.startsWith('@') && stat.isDirectory()) {
45
+ for (const child of readdirSync(path)) if (lstatSync(join(path, child)).isSymbolicLink()) links.push(join(path, child));
46
+ }
47
+ }
48
+ return links;
49
+ }
50
+
51
+ /**
52
+ * Remove, under `<stagedRoot>/node_modules/.pnpm`, every package whose `libc` array lacks `libc`, with the symlinks
53
+ * to it. Throws, naming both, when a package that stays depends on a removed one non-optionally. Returns the removed
54
+ * `.pnpm` entry names, one log line each.
55
+ */
56
+ export function pruneForeignLibcPackages(stagedRoot, libc, log = (line) => console.log(line)) {
57
+ const store = join(stagedRoot, 'node_modules', '.pnpm');
58
+ if (!isDirectory(store)) return [];
59
+ const removed = new Map(); // entry -> package name
60
+ const kept = [];
61
+ for (const entry of readdirSync(store)) {
62
+ const entryDir = join(store, entry);
63
+ if (entry === 'node_modules' || entry === 'lock.yaml' || !isDirectory(entryDir)) continue;
64
+ for (const { name, dir } of realPackages(join(entryDir, 'node_modules'))) {
65
+ const manifestFile = join(dir, 'package.json');
66
+ if (!existsSync(manifestFile)) continue;
67
+ const manifest = readJson(manifestFile);
68
+ // The entry's own package is the one whose directory is not a dependency link; a dependency's real dir lives in its own entry.
69
+ if (Array.isArray(manifest.libc) && !manifest.libc.includes(libc)) removed.set(entry, name);
70
+ else kept.push({ entry, name, manifest });
71
+ }
72
+ }
73
+ if (removed.size === 0) return [];
74
+ const removedNames = new Set(removed.values());
75
+ for (const { name, manifest } of kept) {
76
+ for (const dependency of Object.keys(manifest.dependencies ?? {})) {
77
+ if (removedNames.has(dependency)) {
78
+ throw new Error(`${name} depends on ${dependency} (not optionally), which declares a libc without '${libc}' and would be removed; refusing to break the image`);
79
+ }
80
+ }
81
+ }
82
+ const doomed = [...removed.keys()].map((entry) => join(store, entry) + sep);
83
+ const nodeModulesDirs = [join(stagedRoot, 'node_modules'), join(store, 'node_modules')];
84
+ for (const entry of readdirSync(store)) nodeModulesDirs.push(join(store, entry, 'node_modules'));
85
+ for (const directory of nodeModulesDirs) {
86
+ for (const link of symlinksIn(directory)) {
87
+ const target = resolve(dirname(link), readlinkSync(link)) + sep;
88
+ if (doomed.some((prefix) => target.startsWith(prefix))) unlinkSync(link);
89
+ }
90
+ }
91
+ for (const [entry, name] of removed) {
92
+ rmSync(join(store, entry), { recursive: true, force: true });
93
+ log(`pruned ${name} (.pnpm/${entry}): declares a libc without '${libc}'`);
94
+ }
95
+ return [...removed.keys()];
96
+ }
@@ -13,8 +13,8 @@ import { join } from 'node:path';
13
13
  * schema engine Prisma would not use.
14
14
  */
15
15
  export const IMAGE_PLATFORMS = Object.freeze({
16
- amd64: Object.freeze({ node: 'x64', elfMachine: 62, prismaBinaryTarget: 'debian-openssl-3.0.x' }),
17
- arm64: Object.freeze({ node: 'arm64', elfMachine: 183, prismaBinaryTarget: 'linux-arm64-openssl-3.0.x' }),
16
+ amd64: Object.freeze({ node: 'x64', libc: 'glibc', elfMachine: 62, prismaBinaryTarget: 'debian-openssl-3.0.x' }),
17
+ arm64: Object.freeze({ node: 'arm64', libc: 'glibc', elfMachine: 183, prismaBinaryTarget: 'linux-arm64-openssl-3.0.x' }),
18
18
  });
19
19
 
20
20
  /** The Node `process.arch` name of an image arch (`amd64` → `x64`): what pnpm's `--cpu` takes. Refuses an arch with no row. */
@@ -25,6 +25,14 @@ export function nodeArch(arch) {
25
25
  return IMAGE_PLATFORMS[arch].node;
26
26
  }
27
27
 
28
+ /** The libc of an image arch's runtime base (glibc: node:22-bookworm-slim), from the same row as `nodeArch`. Refuses an arch with no row. */
29
+ export function libcOf(arch) {
30
+ if (!Object.hasOwn(IMAGE_PLATFORMS, arch)) {
31
+ throw new Error(`no libc for image arch '${arch}'; known: ${Object.keys(IMAGE_PLATFORMS).join(', ')}`);
32
+ }
33
+ return IMAGE_PLATFORMS[arch].libc;
34
+ }
35
+
28
36
  /** `linux/<arch>` → `<arch>`; refuses any platform this recipe does not build. */
29
37
  export function platformArch(platform) {
30
38
  const match = /^linux\/([a-z0-9]+)$/.exec(String(platform ?? ''));
@@ -31,7 +31,8 @@ import { prismaEnginesDirectories } from './composed-image-prisma-engine.mjs';
31
31
  import { copyTool, toolVersion } from './composed-image-stage.mjs';
32
32
  import { MANIFEST_FILE, MANIFEST_SCHEMA } from './service-image-in-image.mjs';
33
33
  import { LOCKFILE, TOOL_NAME, rootBuildInputNames, sha256File } from './staged-image-proof.mjs';
34
- import { IMAGE_PLATFORMS, nodeArch, platformArch, scanNatives } from './service-image-platform.mjs';
34
+ import { pruneForeignLibcPackages } from './prune-foreign-libc.mjs';
35
+ import { IMAGE_PLATFORMS, libcOf, nodeArch, platformArch, scanNatives } from './service-image-platform.mjs';
35
36
 
36
37
 
37
38
  export const DEFAULT_STAGE_DIR = '.xema-stage';
@@ -49,7 +50,7 @@ export const deployArgs = ({ service, arch, destination }) => [
49
50
  '--ignore-scripts',
50
51
  '--os=linux',
51
52
  `--cpu=${nodeArch(arch)}`,
52
- '--libc=glibc',
53
+ `--libc=${libcOf(arch)}`,
53
54
  '--config.force-legacy-deploy=false',
54
55
  '--config.inject-workspace-packages=true',
55
56
  '--config.dedupe-injected-deps=true',
@@ -249,6 +250,8 @@ export async function stageService({
249
250
  const appDir = join(serviceStage, APP_DIRECTORY);
250
251
  mkdirSync(serviceStage, { recursive: true });
251
252
  deploy(deployArgs({ service, arch, destination: resolve(appDir) }), root);
253
+ // pnpm 10.33 frozen offline install does not filter by libc; cpu/os do.
254
+ pruneForeignLibcPackages(appDir, libcOf(arch));
252
255
  if (!existsSync(join(appDir, 'dist', 'main.js'))) problems.push(`the deploy of ${service} has no dist/main.js: is dist excluded from the package's files?`);
253
256
 
254
257
  // A dependency whose build script matters would have to run on the target; this recipe runs none.