@xemahq/repo-build-tooling 0.26.2 → 0.26.4
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.
|
|
3
|
+
"version": "0.26.4",
|
|
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)",
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"@xemahq/release-policy": "^16.0.0"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
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"
|
|
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:
|
|
57
|
+
"sourceDigest": "sha256:c6b72c65876d81f6a2548bb6da641b908eccb57c9a6ecaa435e31da9b237d75e"
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -10,6 +10,7 @@ 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
14
|
|
|
14
15
|
/** The install-flag words that make pnpm copy workspace packages into the store layout. */
|
|
15
16
|
export const injectFlags = () => ['--config.inject-workspace-packages=true', '--config.dedupe-injected-deps=false'];
|
|
@@ -26,7 +27,7 @@ export const lockfileOnlyCommand = (filters) => ['pnpm', 'install', '--lockfile-
|
|
|
26
27
|
/** The production install for `filters` on `linux/<arch>` as `[file, ...args]`. */
|
|
27
28
|
export const productionInstallCommand = (filters, arch) => [
|
|
28
29
|
'pnpm', 'install', '--prod', '--offline', '--ignore-scripts', ...injectFlags(), ...filters, '--frozen-lockfile',
|
|
29
|
-
'--os=linux', `--cpu=${arch}`, '--libc=glibc',
|
|
30
|
+
'--os=linux', `--cpu=${nodeArch(arch)}`, '--libc=glibc',
|
|
30
31
|
];
|
|
31
32
|
|
|
32
33
|
const defaultExec = (file, args, { cwd }) => execFileSync(file, args, { cwd, stdio: 'inherit' });
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// pnpm's `--cpu` takes Node `process.arch` names. The OCI name `amd64` is unknown to it and silently skips every
|
|
2
|
+
// optional x64 binary, so an amd64 image lost its @swc/core binding.
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import test from 'node:test';
|
|
9
|
+
import { injectFlags, productionInstallCommand } from './composed-image-install.mjs';
|
|
10
|
+
import { deployArgs } from './service-image-stage.mjs';
|
|
11
|
+
import { nodeArch } from './service-image-platform.mjs';
|
|
12
|
+
|
|
13
|
+
test('nodeArch maps amd64 to x64 and arm64 to arm64, and refuses an arch with no row', () => {
|
|
14
|
+
assert.equal(nodeArch('amd64'), 'x64');
|
|
15
|
+
assert.equal(nodeArch('arm64'), 'arm64');
|
|
16
|
+
assert.throws(() => nodeArch('riscv64'), /no Node arch for image arch 'riscv64'/);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('both install commands pass the Node arch to --cpu and throw for an unknown arch', () => {
|
|
20
|
+
assert.ok(productionInstallCommand([], 'amd64').includes('--cpu=x64'));
|
|
21
|
+
assert.ok(productionInstallCommand([], 'arm64').includes('--cpu=arm64'));
|
|
22
|
+
assert.ok(deployArgs({ service: 's', arch: 'amd64', destination: '/d' }).includes('--cpu=x64'));
|
|
23
|
+
assert.ok(deployArgs({ service: 's', arch: 'arm64', destination: '/d' }).includes('--cpu=arm64'));
|
|
24
|
+
assert.throws(() => productionInstallCommand([], 'riscv64'), /no Node arch/);
|
|
25
|
+
assert.throws(() => deployArgs({ service: 's', arch: 'riscv64', destination: '/d' }), /no Node arch/);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// A fixture shaped like @swc/core: a package whose optional dependencies are one binding per platform.
|
|
29
|
+
function fixturePackage(dir, name, manifest) {
|
|
30
|
+
const pkg = join(dir, name.replace(/\W/g, '_'));
|
|
31
|
+
mkdirSync(pkg, { recursive: true });
|
|
32
|
+
writeFileSync(join(pkg, 'package.json'), JSON.stringify({ name, version: '1.0.0', ...manifest }));
|
|
33
|
+
return pkg;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test('an amd64 stage installs @swc/core-linux-x64-gnu and no arm64 binding; arm64 the reverse', (t) => {
|
|
37
|
+
const dir = mkdtempSync(join(tmpdir(), 'xema-cpu-arch-'));
|
|
38
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
39
|
+
const x64 = fixturePackage(dir, '@swc/core-linux-x64-gnu', { os: ['linux'], cpu: ['x64'], libc: ['glibc'] });
|
|
40
|
+
const arm64 = fixturePackage(dir, '@swc/core-linux-arm64-gnu', { os: ['linux'], cpu: ['arm64'], libc: ['glibc'] });
|
|
41
|
+
const core = fixturePackage(dir, '@swc/core', {
|
|
42
|
+
optionalDependencies: { '@swc/core-linux-x64-gnu': `file:${x64}`, '@swc/core-linux-arm64-gnu': `file:${arm64}` },
|
|
43
|
+
});
|
|
44
|
+
const store = join(dir, 'store');
|
|
45
|
+
const env = { ...process.env, npm_config_store_dir: store };
|
|
46
|
+
const run = (cwd, args) => {
|
|
47
|
+
try {
|
|
48
|
+
return execFileSync('pnpm', args, { cwd, env, stdio: 'pipe' });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw new Error(`pnpm ${args.join(' ')}: ${error.stdout}${error.stderr}`);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
for (const arch of ['amd64', 'arm64']) {
|
|
54
|
+
const app = join(dir, `app-${arch}`);
|
|
55
|
+
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 });
|
|
60
|
+
const [file, ...args] = productionInstallCommand(['--ignore-workspace'], arch);
|
|
61
|
+
run(app, args);
|
|
62
|
+
assert.equal(file, 'pnpm');
|
|
63
|
+
const present = (binding) => existsSync(join(app, 'node_modules', '.pnpm', 'node_modules', '@swc', binding));
|
|
64
|
+
assert.equal(present('core-linux-x64-gnu'), arch === 'amd64', `${arch}: x64 binding`);
|
|
65
|
+
assert.equal(present('core-linux-arm64-gnu'), arch === 'arm64', `${arch}: arm64 binding`);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
@@ -17,6 +17,14 @@ export const IMAGE_PLATFORMS = Object.freeze({
|
|
|
17
17
|
arm64: Object.freeze({ node: 'arm64', elfMachine: 183, prismaBinaryTarget: 'linux-arm64-openssl-3.0.x' }),
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
/** The Node `process.arch` name of an image arch (`amd64` → `x64`): what pnpm's `--cpu` takes. Refuses an arch with no row. */
|
|
21
|
+
export function nodeArch(arch) {
|
|
22
|
+
if (!Object.hasOwn(IMAGE_PLATFORMS, arch)) {
|
|
23
|
+
throw new Error(`no Node arch for image arch '${arch}'; known: ${Object.keys(IMAGE_PLATFORMS).join(', ')}`);
|
|
24
|
+
}
|
|
25
|
+
return IMAGE_PLATFORMS[arch].node;
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
/** `linux/<arch>` → `<arch>`; refuses any platform this recipe does not build. */
|
|
21
29
|
export function platformArch(platform) {
|
|
22
30
|
const match = /^linux\/([a-z0-9]+)$/.exec(String(platform ?? ''));
|
|
@@ -31,7 +31,7 @@ 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, platformArch, scanNatives } from './service-image-platform.mjs';
|
|
34
|
+
import { IMAGE_PLATFORMS, nodeArch, platformArch, scanNatives } from './service-image-platform.mjs';
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
export const DEFAULT_STAGE_DIR = '.xema-stage';
|
|
@@ -48,7 +48,7 @@ export const deployArgs = ({ service, arch, destination }) => [
|
|
|
48
48
|
'--offline',
|
|
49
49
|
'--ignore-scripts',
|
|
50
50
|
'--os=linux',
|
|
51
|
-
`--cpu=${arch}`,
|
|
51
|
+
`--cpu=${nodeArch(arch)}`,
|
|
52
52
|
'--libc=glibc',
|
|
53
53
|
'--config.force-legacy-deploy=false',
|
|
54
54
|
'--config.inject-workspace-packages=true',
|
|
@@ -85,10 +85,31 @@ export function deployedPackages(appDir) {
|
|
|
85
85
|
|
|
86
86
|
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
/** What names a failed engine download: the engine, the URL, the engines commit and the target. */
|
|
89
|
+
const engineFailure = ({ url, enginesVersion, binaryTarget }, detail) =>
|
|
90
|
+
`Prisma schema-engine download failed: ${detail} (url ${url}, engines commit ${enginesVersion}, target ${binaryTarget})`;
|
|
91
|
+
|
|
92
|
+
/** GET `url` as bytes. A transport failure or a non-2xx status names the engine, URL, commit, target and bytes received. */
|
|
93
|
+
async function getEngine(url, identity) {
|
|
94
|
+
const named = { url, ...identity };
|
|
95
|
+
let response;
|
|
96
|
+
try {
|
|
97
|
+
response = await fetch(url);
|
|
98
|
+
} catch (cause) {
|
|
99
|
+
throw new Error(engineFailure(named, `request failed after 0 bytes: ${cause?.message ?? cause}`), { cause });
|
|
100
|
+
}
|
|
101
|
+
if (!response.ok) throw new Error(engineFailure(named, `HTTP ${response.status} ${response.statusText}`));
|
|
102
|
+
const chunks = [];
|
|
103
|
+
let received = 0;
|
|
104
|
+
try {
|
|
105
|
+
for await (const chunk of response.body) {
|
|
106
|
+
chunks.push(chunk);
|
|
107
|
+
received += chunk.length;
|
|
108
|
+
}
|
|
109
|
+
} catch (cause) {
|
|
110
|
+
throw new Error(engineFailure(named, `body read failed after ${received} bytes: ${cause?.message ?? cause}`), { cause });
|
|
111
|
+
}
|
|
112
|
+
return Buffer.concat(chunks);
|
|
92
113
|
}
|
|
93
114
|
|
|
94
115
|
/**
|
|
@@ -105,12 +126,32 @@ export const SCHEMA_ENGINE_SHA256 = {
|
|
|
105
126
|
},
|
|
106
127
|
};
|
|
107
128
|
|
|
129
|
+
/** In-process memo of the verified engine bytes, by `${enginesVersion}:${binaryTarget}`; holds the promise so concurrent stagers share one download. */
|
|
130
|
+
const verifiedEngines = new Map();
|
|
131
|
+
|
|
132
|
+
async function fetchVerifiedEngine({ binaryTarget, enginesVersion, expected }) {
|
|
133
|
+
const url = `${PRISMA_ENGINES_URL}/${enginesVersion}/${binaryTarget}/schema-engine`;
|
|
134
|
+
const identity = { enginesVersion, binaryTarget };
|
|
135
|
+
const compressed = await getEngine(`${url}.gz`, identity);
|
|
136
|
+
let engine;
|
|
137
|
+
try {
|
|
138
|
+
engine = gunzipSync(compressed);
|
|
139
|
+
} catch (cause) {
|
|
140
|
+
throw new Error(engineFailure({ url: `${url}.gz`, ...identity }, `gunzip failed on ${compressed.length} bytes: ${cause?.message ?? cause}`), { cause });
|
|
141
|
+
}
|
|
142
|
+
const actual = sha256(engine);
|
|
143
|
+
if (actual !== expected) {
|
|
144
|
+
throw new Error(`Prisma schema-engine ${url}.gz (engines commit ${enginesVersion}, target ${binaryTarget}) unzips to sha256 ${actual}, the committed sha256 is ${expected}`);
|
|
145
|
+
}
|
|
146
|
+
return engine;
|
|
147
|
+
}
|
|
148
|
+
|
|
108
149
|
/**
|
|
109
150
|
* The schema engine at `enginesVersion` (the engines commit the lockfile pins, through the
|
|
110
|
-
* deploy's @prisma/engines-version) for `binaryTarget`, fetched from Prisma
|
|
151
|
+
* deploy's @prisma/engines-version) for `binaryTarget`, fetched from Prisma once per process (the verified bytes are memoised by commit and target) and
|
|
111
152
|
* written to `enginesDir`. The unzipped engine must have the sha256 `checksums` commits for
|
|
112
153
|
* (enginesVersion, binaryTarget); a pair it does not list is refused before any download. There is
|
|
113
|
-
* no cache: Prisma's own downloader keeps one across runs in the home directory, honours mirror
|
|
154
|
+
* no cross-run cache: Prisma's own downloader keeps one across runs in the home directory, honours mirror
|
|
114
155
|
* variables and retries, so it is not used.
|
|
115
156
|
*/
|
|
116
157
|
export async function downloadSchemaEngine({ enginesDir, binaryTarget, enginesVersion, checksums = SCHEMA_ENGINE_SHA256 }) {
|
|
@@ -119,9 +160,17 @@ export async function downloadSchemaEngine({ enginesDir, binaryTarget, enginesVe
|
|
|
119
160
|
if (expected === undefined) {
|
|
120
161
|
throw new Error(`no committed sha256 for the Prisma schema engine at engines commit ${enginesVersion} for ${binaryTarget}: add it to SCHEMA_ENGINE_SHA256 in service-image-stage.mjs`);
|
|
121
162
|
}
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
if (
|
|
163
|
+
const key = `${enginesVersion}:${binaryTarget}`;
|
|
164
|
+
let verified = verifiedEngines.get(key);
|
|
165
|
+
if (verified === undefined) {
|
|
166
|
+
verified = fetchVerifiedEngine({ binaryTarget, enginesVersion, expected });
|
|
167
|
+
verifiedEngines.set(key, verified);
|
|
168
|
+
// A failed download is not remembered: the next call fetches again.
|
|
169
|
+
verified.catch(() => {
|
|
170
|
+
if (verifiedEngines.get(key) === verified) verifiedEngines.delete(key);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const engine = await verified;
|
|
125
174
|
const file = join(enginesDir, `schema-engine-${binaryTarget}`);
|
|
126
175
|
writeFileSync(file, engine);
|
|
127
176
|
chmodSync(file, 0o755);
|
|
@@ -282,8 +282,10 @@ test('downloadSchemaEngine fetches the pinned engine, checks it against the COMM
|
|
|
282
282
|
|
|
283
283
|
// A binary whose published .sha256 agrees with it is still refused when it is not the committed one.
|
|
284
284
|
const other = elf(184);
|
|
285
|
-
|
|
286
|
-
|
|
285
|
+
const mismatchChecksums = { ['d'.repeat(40)]: { [target]: sha256(engine) } };
|
|
286
|
+
const mismatchUrl = `${PRISMA_ENGINES_URL}/${'d'.repeat(40)}/${target}/schema-engine`;
|
|
287
|
+
serve({ [`${mismatchUrl}.gz`]: gzipSync(other), [`${mismatchUrl}.sha256`]: `${sha256(other)} x\n` });
|
|
288
|
+
await assert.rejects(downloadSchemaEngine({ enginesDir: temp(), binaryTarget: target, enginesVersion: 'd'.repeat(40), checksums: mismatchChecksums }), new RegExp(`unzips to sha256 ${sha256(other)}, the committed sha256 is ${sha256(engine)}`));
|
|
287
289
|
t.mock.restoreAll();
|
|
288
290
|
const unfetched = serve({});
|
|
289
291
|
await assert.rejects(downloadSchemaEngine({ enginesDir: temp(), binaryTarget: target, enginesVersion: 'c'.repeat(40), checksums }), /no committed sha256 .* engines commit c{40} for linux-arm64/);
|
|
@@ -292,6 +294,94 @@ test('downloadSchemaEngine fetches the pinned engine, checks it against the COMM
|
|
|
292
294
|
await assert.rejects(downloadSchemaEngine({ enginesDir: temp(), binaryTarget: 'x', enginesVersion: '1.2.3' }), /is not an engines commit/);
|
|
293
295
|
});
|
|
294
296
|
|
|
297
|
+
test('downloadSchemaEngine downloads once per engines commit and target, however many services stage', async (t) => {
|
|
298
|
+
const engine = elf(185);
|
|
299
|
+
const version = 'e'.repeat(40);
|
|
300
|
+
const target = 'linux-arm64-openssl-3.0.x';
|
|
301
|
+
const checksums = { [version]: { [target]: sha256(engine) } };
|
|
302
|
+
const fetches = [];
|
|
303
|
+
t.mock.method(globalThis, 'fetch', async (asked) => {
|
|
304
|
+
fetches.push(asked);
|
|
305
|
+
return new Response(gzipSync(engine));
|
|
306
|
+
});
|
|
307
|
+
const dirs = [temp(), temp(), temp()];
|
|
308
|
+
await Promise.all(dirs.slice(0, 2).map((enginesDir) => downloadSchemaEngine({ enginesDir, binaryTarget: target, enginesVersion: version, checksums })));
|
|
309
|
+
await downloadSchemaEngine({ enginesDir: dirs[2], binaryTarget: target, enginesVersion: version, checksums });
|
|
310
|
+
assert.equal(fetches.length, 1);
|
|
311
|
+
for (const dir of dirs) assert.deepEqual(fs.readFileSync(path.join(dir, `schema-engine-${target}`)), engine);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('downloadSchemaEngine names the engine, URL, commit, target, bytes and cause when the body read fails', async (t) => {
|
|
315
|
+
const engine = elf(186);
|
|
316
|
+
const version = 'f'.repeat(40);
|
|
317
|
+
const target = 'linux-arm64-openssl-3.0.x';
|
|
318
|
+
const url = `${PRISMA_ENGINES_URL}/${version}/${target}/schema-engine.gz`;
|
|
319
|
+
const checksums = { [version]: { [target]: sha256(engine) } };
|
|
320
|
+
t.mock.method(globalThis, 'fetch', async () => {
|
|
321
|
+
let sent = false;
|
|
322
|
+
const body = new ReadableStream({
|
|
323
|
+
pull(controller) {
|
|
324
|
+
if (sent) return controller.error(new TypeError('terminated'));
|
|
325
|
+
sent = true;
|
|
326
|
+
controller.enqueue(new Uint8Array(7));
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
return new Response(body);
|
|
330
|
+
});
|
|
331
|
+
const error = await downloadSchemaEngine({ enginesDir: temp(), binaryTarget: target, enginesVersion: version, checksums }).then(
|
|
332
|
+
() => assert.fail('expected a rejection'),
|
|
333
|
+
(e) => e,
|
|
334
|
+
);
|
|
335
|
+
for (const part of ['schema-engine', url, version, target, '7 bytes', 'terminated']) assert.ok(error.message.includes(part), `${part} in: ${error.message}`);
|
|
336
|
+
assert.equal(error.cause.message, 'terminated');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('downloadSchemaEngine names the engine, URL, commit and target on a non-2xx status', async (t) => {
|
|
340
|
+
const engine = elf(187);
|
|
341
|
+
const version = '1'.repeat(40);
|
|
342
|
+
const target = 'linux-arm64-openssl-3.0.x';
|
|
343
|
+
const url = `${PRISMA_ENGINES_URL}/${version}/${target}/schema-engine.gz`;
|
|
344
|
+
const checksums = { [version]: { [target]: sha256(engine) } };
|
|
345
|
+
t.mock.method(globalThis, 'fetch', async () => new Response('', { status: 503, statusText: 'Service Unavailable' }));
|
|
346
|
+
await assert.rejects(
|
|
347
|
+
downloadSchemaEngine({ enginesDir: temp(), binaryTarget: target, enginesVersion: version, checksums }),
|
|
348
|
+
(error) => [`schema-engine`, url, version, target, 'HTTP 503 Service Unavailable'].every((part) => error.message.includes(part)),
|
|
349
|
+
);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('downloadSchemaEngine does not remember a failed download', async (t) => {
|
|
353
|
+
const engine = elf(188);
|
|
354
|
+
const version = '2'.repeat(40);
|
|
355
|
+
const target = 'linux-arm64-openssl-3.0.x';
|
|
356
|
+
const checksums = { [version]: { [target]: sha256(engine) } };
|
|
357
|
+
let calls = 0;
|
|
358
|
+
t.mock.method(globalThis, 'fetch', async () => {
|
|
359
|
+
calls += 1;
|
|
360
|
+
return calls === 1 ? new Response('', { status: 502, statusText: 'Bad Gateway' }) : new Response(gzipSync(engine));
|
|
361
|
+
});
|
|
362
|
+
await assert.rejects(downloadSchemaEngine({ enginesDir: temp(), binaryTarget: target, enginesVersion: version, checksums }), /HTTP 502/);
|
|
363
|
+
const enginesDir = temp();
|
|
364
|
+
await downloadSchemaEngine({ enginesDir, binaryTarget: target, enginesVersion: version, checksums });
|
|
365
|
+
assert.equal(calls, 2);
|
|
366
|
+
assert.deepEqual(fs.readFileSync(path.join(enginesDir, `schema-engine-${target}`)), engine);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test('downloadSchemaEngine names the engine, URL, commit, target and cause when the gzip is corrupt', async (t) => {
|
|
370
|
+
const engine = elf(189);
|
|
371
|
+
const version = '3'.repeat(40);
|
|
372
|
+
const target = 'linux-arm64-openssl-3.0.x';
|
|
373
|
+
const url = `${PRISMA_ENGINES_URL}/${version}/${target}/schema-engine.gz`;
|
|
374
|
+
const checksums = { [version]: { [target]: sha256(engine) } };
|
|
375
|
+
t.mock.method(globalThis, 'fetch', async () => new Response(Buffer.from('not gzip at all')));
|
|
376
|
+
const error = await downloadSchemaEngine({ enginesDir: temp(), binaryTarget: target, enginesVersion: version, checksums }).then(
|
|
377
|
+
() => assert.fail('expected a rejection'),
|
|
378
|
+
(e) => e,
|
|
379
|
+
);
|
|
380
|
+
for (const part of ['schema-engine', url, version, target, 'gunzip failed']) assert.ok(error.message.includes(part), `${part} in: ${error.message}`);
|
|
381
|
+
assert.ok(error.cause instanceof Error);
|
|
382
|
+
assert.ok(error.message.includes(error.cause.message));
|
|
383
|
+
});
|
|
384
|
+
|
|
295
385
|
test('the committed engine table has the amd64 and arm64 rows for the engines commit xema-base pins', () => {
|
|
296
386
|
const rows = SCHEMA_ENGINE_SHA256['3c6e192761c0362d496ed980de936e2f3cebcd3a'];
|
|
297
387
|
assert.deepEqual(Object.keys(rows).sort(), ['debian-openssl-3.0.x', 'linux-arm64-openssl-3.0.x']);
|