@wavehouse/chtypes 0.3.2 → 0.5.0
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/CHANGELOG.md +38 -0
- package/dist/cli.js.map +1 -1
- package/dist/error-codes.d.ts +80 -0
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/error-codes.js +139 -0
- package/dist/error-codes.js.map +1 -0
- package/dist/errors.d.ts +5 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +5 -1
- package/dist/errors.js.map +1 -1
- package/dist/fetch.d.ts +63 -14
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +418 -94
- package/dist/fetch.js.map +1 -1
- package/dist/ffi.d.ts +18 -0
- package/dist/ffi.d.ts.map +1 -1
- package/dist/ffi.js +53 -0
- package/dist/ffi.js.map +1 -1
- package/dist/format.d.ts +9 -4
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +9 -4
- package/dist/format.js.map +1 -1
- package/dist/index.d.ts +12 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/library.d.ts +27 -0
- package/dist/library.d.ts.map +1 -1
- package/dist/library.js +29 -0
- package/dist/library.js.map +1 -1
- package/dist/registry.d.ts +176 -66
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +475 -161
- package/dist/registry.js.map +1 -1
- package/dist/results.d.ts +16 -0
- package/dist/results.d.ts.map +1 -1
- package/dist/results.js +10 -0
- package/dist/results.js.map +1 -1
- package/dist/schema.d.ts +41 -7
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +31 -1
- package/dist/schema.js.map +1 -1
- package/package.json +1 -1
package/dist/registry.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The artifact-directory loader: one subdirectory per ClickHouse minor line,
|
|
3
|
-
* each self-contained (docs/reference/artifact.md)
|
|
3
|
+
* each self-contained (docs/reference/artifact.md), plus — since #284 — every
|
|
4
|
+
* OTHER installed exact patch of a line, in a sibling `patches/` tree:
|
|
4
5
|
*
|
|
5
6
|
* <registry>/25.8/{manifest.json, libchtypes.dylib, CH_VERSION, unsafe_families.txt}
|
|
7
|
+
* <registry>/patches/25.8/25.8.28.1-lts/{manifest.json, libchtypes.dylib, ...}
|
|
8
|
+
*
|
|
9
|
+
* The flat `<registry>/<minor>/` slot is what a LINE request resolves to —
|
|
10
|
+
* exactly the pre-#284 layout, so every SDK through 0.4.x keeps reading it
|
|
11
|
+
* unchanged. Any OTHER exact patch of that line lives in
|
|
12
|
+
* `<registry>/patches/<minor>/<clickhouse_version>/`, which no released SDK
|
|
13
|
+
* (0.4.x and earlier) scans, fetches into or deletes (measured, issue #284
|
|
14
|
+
* comment 5919199794): it is a new, additive tree, not a migration of the old
|
|
15
|
+
* one. When a line fetch changes which patch occupies the flat slot, the
|
|
16
|
+
* outgoing install is DEMOTED — an atomic, same-filesystem rename into
|
|
17
|
+
* `patches/<minor>/<its version>/` — never deleted, so a server still on the
|
|
18
|
+
* older patch keeps its exact match with no re-fetch.
|
|
6
19
|
*
|
|
7
20
|
* Two rules here were paid for and are not negotiable:
|
|
8
21
|
*
|
|
@@ -14,35 +27,175 @@
|
|
|
14
27
|
* the platform that matters.
|
|
15
28
|
* - **Each artifact is loaded into its own symbol scope (`RTLD_LOCAL`).** That
|
|
16
29
|
* is the entire mechanism by which two builds that both define
|
|
17
|
-
* `DB::DataTypeFactory` live in one process
|
|
18
|
-
*
|
|
19
|
-
*
|
|
30
|
+
* `DB::DataTypeFactory` live in one process — now routinely two builds of
|
|
31
|
+
* the SAME minor line, one per patch. ffi-rs loads through libloading,
|
|
32
|
+
* which uses `RTLD_LAZY | RTLD_LOCAL`; `assertLocalSymbolScope()` in the
|
|
33
|
+
* test suite proves it from outside rather than trusting the claim.
|
|
20
34
|
*
|
|
21
35
|
* Where a registry IS follows the search path of docs/guides/fetch.md §1 (`paths.ts`):
|
|
22
36
|
* the explicit directory, `CHTYPES_REGISTRY`, the per-user cache, then the
|
|
23
37
|
* reserved system locations. Construction READS THE MANIFESTS on that path and
|
|
24
|
-
* `dlopen`s nothing; a
|
|
25
|
-
* has it.
|
|
26
|
-
*
|
|
38
|
+
* `dlopen`s nothing; a version is taken, on request, from the first directory
|
|
39
|
+
* that has it. Resolution (docs/reference/bindings.md §Version selection):
|
|
40
|
+
*
|
|
41
|
+
* - **A line request** ("25.8") never falls back and never crosses lines: the
|
|
42
|
+
* newest patch of the line, in the first search-path directory that holds
|
|
43
|
+
* any patch of it (a nested install beating a flat one on a version tie),
|
|
44
|
+
* pinned for this registry for as long as it runs.
|
|
45
|
+
* - **A patch request** ("25.8.28.1-lts") loads that exact patch when it is
|
|
46
|
+
* open or installed anywhere on the search path. Otherwise it falls back to
|
|
47
|
+
* the newest installed patch of the same line, flags the result
|
|
48
|
+
* `exact: false`, and warns once per (requested, actual) pair per process —
|
|
49
|
+
* never another line, which stays the one §7 error, `ArtifactMissingError`.
|
|
50
|
+
*
|
|
51
|
+
* A patch spelled with no channel suffix matches that patch on any channel
|
|
52
|
+
* (docs/guides/fetch.md Decision 7); ordering is numeric, channel ignored.
|
|
27
53
|
*/
|
|
28
54
|
import { createHash } from 'node:crypto';
|
|
29
55
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
30
56
|
import path from 'node:path';
|
|
31
|
-
import { ArtifactMissingError, FETCH_COMMAND, RegistryError } from './errors.js';
|
|
32
|
-
import { ensure } from './fetch.js';
|
|
57
|
+
import { ABI_REVISION, ArtifactMissingError, ArtifactUnpublishedError, FETCH_COMMAND, RegistryError } from './errors.js';
|
|
58
|
+
import { compareVersions, ensure, parseVersionSpelling, patchMatches } from './fetch.js';
|
|
33
59
|
import { NativeLibrary } from './ffi.js';
|
|
34
60
|
import { Library, minorOf } from './library.js';
|
|
35
61
|
import { cacheRegistryDir, ENV_AUTOFETCH, ENV_REGISTRY, fetchDestination, hostPlatform, registrySearchPath, systemRegistryDirs, } from './paths.js';
|
|
62
|
+
/** Every patch (flat + `patches/`) a search-path root holds, whatever line it claims. */
|
|
63
|
+
function flatLocationsInRoot(root, rootIndex) {
|
|
64
|
+
const out = [];
|
|
65
|
+
if (!isDirectory(root))
|
|
66
|
+
return out;
|
|
67
|
+
let entries;
|
|
68
|
+
try {
|
|
69
|
+
entries = readdirSync(root);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
for (const entry of entries.sort()) {
|
|
75
|
+
// A dot-directory is never a version (fetch stages downloads in hidden
|
|
76
|
+
// siblings), and `patches/` is the sibling tree, never a line itself.
|
|
77
|
+
if (entry.startsWith('.') || entry === 'patches')
|
|
78
|
+
continue;
|
|
79
|
+
const sub = path.join(root, entry);
|
|
80
|
+
if (!isDirectory(sub))
|
|
81
|
+
continue;
|
|
82
|
+
const manifest = readManifest(sub);
|
|
83
|
+
if (manifest === null)
|
|
84
|
+
continue;
|
|
85
|
+
const claimedMinor = manifest.clickhouse_minor ?? minorOf(manifest.clickhouse_version ?? entry);
|
|
86
|
+
const minor = claimedMinor !== '' ? claimedMinor : entry;
|
|
87
|
+
const version = manifest.clickhouse_version !== undefined && manifest.clickhouse_version !== '' ? manifest.clickhouse_version : entry;
|
|
88
|
+
out.push({ minor, version, dir: sub, flat: true, rootIndex });
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
/** Every patch under `<root>/patches/*\/*\/`. */
|
|
93
|
+
function nestedLocationsInRoot(root, rootIndex) {
|
|
94
|
+
const out = [];
|
|
95
|
+
const patchesRoot = path.join(root, 'patches');
|
|
96
|
+
if (!isDirectory(patchesRoot))
|
|
97
|
+
return out;
|
|
98
|
+
let minorEntries;
|
|
99
|
+
try {
|
|
100
|
+
minorEntries = readdirSync(patchesRoot);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
for (const minorEntry of minorEntries.sort()) {
|
|
106
|
+
if (minorEntry.startsWith('.'))
|
|
107
|
+
continue;
|
|
108
|
+
const minorDir = path.join(patchesRoot, minorEntry);
|
|
109
|
+
if (!isDirectory(minorDir))
|
|
110
|
+
continue;
|
|
111
|
+
let versionEntries;
|
|
112
|
+
try {
|
|
113
|
+
versionEntries = readdirSync(minorDir);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
for (const versionEntry of versionEntries.sort()) {
|
|
119
|
+
if (versionEntry.startsWith('.'))
|
|
120
|
+
continue;
|
|
121
|
+
const sub = path.join(minorDir, versionEntry);
|
|
122
|
+
if (!isDirectory(sub))
|
|
123
|
+
continue;
|
|
124
|
+
const manifest = readManifest(sub);
|
|
125
|
+
if (manifest === null)
|
|
126
|
+
continue;
|
|
127
|
+
const claimedMinor = manifest.clickhouse_minor ?? minorOf(manifest.clickhouse_version ?? minorEntry);
|
|
128
|
+
const minor = claimedMinor !== '' ? claimedMinor : minorEntry;
|
|
129
|
+
const version = manifest.clickhouse_version !== undefined && manifest.clickhouse_version !== '' ? manifest.clickhouse_version : versionEntry;
|
|
130
|
+
out.push({ minor, version, dir: sub, flat: false, rootIndex });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
/** Every patch (both slots) a root holds. */
|
|
136
|
+
function locationsInRoot(root, rootIndex) {
|
|
137
|
+
return [...flatLocationsInRoot(root, rootIndex), ...nestedLocationsInRoot(root, rootIndex)];
|
|
138
|
+
}
|
|
139
|
+
/** R3/R4: the newest patch, among locations from the FIRST root that has any — ties: nested beats flat. */
|
|
140
|
+
function pickLineWinner(locs) {
|
|
141
|
+
if (locs.length === 0)
|
|
142
|
+
return undefined;
|
|
143
|
+
const firstRoot = Math.min(...locs.map((l) => l.rootIndex));
|
|
144
|
+
const candidates = locs.filter((l) => l.rootIndex === firstRoot);
|
|
145
|
+
let best = candidates[0];
|
|
146
|
+
for (const cur of candidates.slice(1)) {
|
|
147
|
+
const cmp = compareVersions(cur.version, best.version);
|
|
148
|
+
if (cmp > 0 || (cmp === 0 && !cur.flat && best.flat))
|
|
149
|
+
best = cur;
|
|
150
|
+
}
|
|
151
|
+
return best;
|
|
152
|
+
}
|
|
153
|
+
/** R4 step 2: the first root (in search-path order) holding a location whose version matches `req` under R2. */
|
|
154
|
+
function pickPatchMatch(locs, req) {
|
|
155
|
+
return [...locs].sort((a, b) => a.rootIndex - b.rootIndex).find((l) => patchMatches(l.version, req));
|
|
156
|
+
}
|
|
157
|
+
/** §3: the fallback warning, once per (requested, actual) pair per process — shared by every Registry instance. */
|
|
158
|
+
const warnedPatchFallbacks = new Set();
|
|
159
|
+
function emitPatchFallbackWarning(requested, actual, minor, platform, kind) {
|
|
160
|
+
const key = `${requested}\u0000${actual}`;
|
|
161
|
+
// Recorded BEFORE emitting: a caller's warning filter that throws must not
|
|
162
|
+
// cause a retry to warn again for the same pair.
|
|
163
|
+
if (warnedPatchFallbacks.has(key))
|
|
164
|
+
return;
|
|
165
|
+
warnedPatchFallbacks.add(key);
|
|
166
|
+
const body = kind === 'installed'
|
|
167
|
+
? `ClickHouse ${requested} is not installed for ${platform}; using ${actual}, the newest installed patch of ${minor}. ` +
|
|
168
|
+
`Behavior can differ between patches. If ${requested} is published, install it with: ${FETCH_COMMAND} ${requested}`
|
|
169
|
+
: `ClickHouse ${requested} is not published for ${platform} at ABI revision ${ABI_REVISION}; using ${actual}, ` +
|
|
170
|
+
`the newest published patch of ${minor}. Behavior can differ between patches.`;
|
|
171
|
+
process.emitWarning(`chtypes: ${body}`, { type: 'PatchFallbackWarning', code: 'CHTYPES_PATCH_FALLBACK' });
|
|
172
|
+
}
|
|
173
|
+
/** TEST-ONLY: forget every warned pair, so a suite can assert a fresh warning fires. Not re-exported from index.ts. */
|
|
174
|
+
export function resetPatchFallbackWarnings() {
|
|
175
|
+
warnedPatchFallbacks.clear();
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* R-c: re-scan the search path for a patch fallback at most this often, per
|
|
179
|
+
* requested patch, per process. Caches the CHOSEN directory, not a loaded
|
|
180
|
+
* `Library` — the load itself (one known path, not a directory read per
|
|
181
|
+
* search-path entry) still runs on every call, so a fallback whose chosen
|
|
182
|
+
* artifact is broken keeps reporting the same failure rather than being
|
|
183
|
+
* silently remembered as unresolvable.
|
|
184
|
+
*/
|
|
185
|
+
const FALLBACK_RECHECK_MS = 60_000;
|
|
186
|
+
const fallbackCache = new Map();
|
|
36
187
|
/**
|
|
37
188
|
* The artifact-directory loader — the multi-version entry point of this
|
|
38
189
|
* package.
|
|
39
190
|
*
|
|
40
191
|
* **Construction reads `manifest.json` files and `dlopen`s nothing**, with or
|
|
41
192
|
* without a directory. Nothing in this package opens an artifact except a
|
|
42
|
-
* request for a specific version (`for()` / `
|
|
43
|
-
* `preload` — not `versions()`, not
|
|
44
|
-
* about 120 MB resident per
|
|
45
|
-
* caller's behalf
|
|
193
|
+
* request for a specific version (`for()` / `resolve()` / `open()` /
|
|
194
|
+
* `openResolution()`) or an explicit `preload` — not `versions()`, not
|
|
195
|
+
* `libraries()`, not `has()`. An open costs about 120 MB resident per patch,
|
|
196
|
+
* which a listing call must not spend on a caller's behalf, and which grows
|
|
197
|
+
* with every DISTINCT patch a process is asked for — libraries are never
|
|
198
|
+
* dlclosed (docs/guides/multi-version.md).
|
|
46
199
|
*
|
|
47
200
|
* An open dlopens the artifact into its own symbol scope (`RTLD_LOCAL`),
|
|
48
201
|
* verifies its ABI revision against this binding's `ABI_REVISION` (a different
|
|
@@ -65,14 +218,17 @@ export class Registry {
|
|
|
65
218
|
searchPath;
|
|
66
219
|
/** This host's platform key, e.g. `darwin-arm64` — the only artifacts a process can dlopen. */
|
|
67
220
|
platform;
|
|
68
|
-
byId = new Map();
|
|
69
221
|
byPath = new Map();
|
|
70
222
|
loaded = [];
|
|
223
|
+
/** Line -> the library currently answering FOR THE LINE. Set once per line; never re-pointed while this registry runs (R3/R8). */
|
|
224
|
+
linePins = new Map();
|
|
71
225
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
226
|
+
* Line -> every patch the construction-time manifest scan found for it,
|
|
227
|
+
* across the whole search path. What `versions()` answers from, and what
|
|
228
|
+
* makes "no artifact anywhere" and a bad `preload` entry decidable at
|
|
229
|
+
* construction without a single `dlopen`. Resolution itself re-scans the
|
|
230
|
+
* live filesystem on every call (a patch installed after construction, by
|
|
231
|
+
* this process or another, is used from the next call on).
|
|
76
232
|
*/
|
|
77
233
|
known = new Map();
|
|
78
234
|
timezone;
|
|
@@ -80,9 +236,13 @@ export class Registry {
|
|
|
80
236
|
autofetch;
|
|
81
237
|
fetchOptions;
|
|
82
238
|
explicit;
|
|
239
|
+
/** (dest, line) already `Ensure()`d successfully by THIS registry's autofetch — never re-read over the network again. */
|
|
240
|
+
ensuredLines = new Set();
|
|
241
|
+
/** (dest, exact patch) autofetch has already confirmed CHTYPES_ARTIFACT_UNPUBLISHED for — never retried within this registry's lifetime. */
|
|
242
|
+
unpublishedPatches = new Set();
|
|
83
243
|
/**
|
|
84
244
|
* Scan a registry. Reads manifests; opens nothing unless `preload` names a
|
|
85
|
-
*
|
|
245
|
+
* version.
|
|
86
246
|
*
|
|
87
247
|
* @param dir - the registry root; the head of the search path. Absent, the
|
|
88
248
|
* path is `CHTYPES_REGISTRY`, the per-user artifact cache, then the system
|
|
@@ -92,7 +252,7 @@ export class Registry {
|
|
|
92
252
|
* @throws {RegistryError} for what manifests can decide, and only that: a
|
|
93
253
|
* directory named explicitly (the argument or `CHTYPES_REGISTRY`) that does
|
|
94
254
|
* not exist, and no directory on the search path holding a readable
|
|
95
|
-
*
|
|
255
|
+
* manifest at either slot — both suppressed when autofetch is on. A
|
|
96
256
|
* `preload` entry no directory holds is `ArtifactMissingError`. Everything
|
|
97
257
|
* a bad artifact can be wrong about — a failed checksum, a load failure, a
|
|
98
258
|
* library whose ClickHouse version disagrees with its manifest — is
|
|
@@ -120,41 +280,16 @@ export class Registry {
|
|
|
120
280
|
throw new RegistryError(`chtypes: cannot read registry ${d}: not a directory`);
|
|
121
281
|
}
|
|
122
282
|
}
|
|
123
|
-
// The scan: every directory on the path, in order,
|
|
124
|
-
//
|
|
283
|
+
// The scan: every directory on the path, in order, every patch at either
|
|
284
|
+
// slot. Manifests only — this is the cheap half of what construction used
|
|
125
285
|
// to do, and it is all that is left of it.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
entries = readdirSync(root);
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
continue; // unreadable: not a registry, and not this call's business
|
|
135
|
-
}
|
|
136
|
-
for (const entry of entries.sort()) {
|
|
137
|
-
// A dot-directory is never a version: fetch stages its downloads and
|
|
138
|
-
// unpacks in hidden siblings, and a .DS_Store is not a version either.
|
|
139
|
-
if (entry.startsWith('.'))
|
|
140
|
-
continue;
|
|
141
|
-
const sub = path.join(root, entry);
|
|
142
|
-
if (!isDirectory(sub))
|
|
143
|
-
continue;
|
|
144
|
-
// A registry may legitimately hold scratch directories: a missing or
|
|
145
|
-
// unparseable manifest is skipped in silence.
|
|
146
|
-
const manifest = readManifest(sub);
|
|
147
|
-
if (manifest === null)
|
|
148
|
-
continue;
|
|
149
|
-
// The manifest's own claim; the directory name is only the last
|
|
150
|
-
// resort, exactly as it is when the library is finally loaded and
|
|
151
|
-
// names itself.
|
|
152
|
-
const claimed = manifest.clickhouse_minor ?? minorOf(manifest.clickhouse_version ?? '');
|
|
153
|
-
const line = claimed !== '' ? claimed : entry;
|
|
154
|
-
if (!this.known.has(line))
|
|
155
|
-
this.known.set(line, sub);
|
|
286
|
+
this.searchPath.forEach((root, rootIndex) => {
|
|
287
|
+
for (const loc of locationsInRoot(root, rootIndex)) {
|
|
288
|
+
const arr = this.known.get(loc.minor) ?? [];
|
|
289
|
+
arr.push(loc);
|
|
290
|
+
this.known.set(loc.minor, arr);
|
|
156
291
|
}
|
|
157
|
-
}
|
|
292
|
+
});
|
|
158
293
|
const primary = this.searchPath.find((d) => looksLikeRegistry(d));
|
|
159
294
|
if (primary === undefined) {
|
|
160
295
|
if (!this.autofetch) {
|
|
@@ -174,16 +309,22 @@ export class Registry {
|
|
|
174
309
|
}
|
|
175
310
|
/**
|
|
176
311
|
* Open one `preload` entry, before the constructor returns, without
|
|
177
|
-
* fetching. Resolution is `for()`'s, and so is the
|
|
178
|
-
* directory holds is the same
|
|
312
|
+
* fetching. Resolution is `for()`'s (minus any fetch), and so is the
|
|
313
|
+
* failure: a version no directory holds anywhere is the same
|
|
314
|
+
* `ArtifactMissingError`, raised earlier. A patch entry that falls back
|
|
315
|
+
* warns here, at construction, exactly as `for()` would.
|
|
179
316
|
*/
|
|
180
317
|
preloadLine(version) {
|
|
181
318
|
if (version === '') {
|
|
182
319
|
throw new RegistryError("chtypes: preload: an empty version does not mean 'pick one'");
|
|
183
320
|
}
|
|
184
|
-
|
|
185
|
-
|
|
321
|
+
const req = parseVersionSpelling(version);
|
|
322
|
+
const resolved = this.resolveSync(req);
|
|
323
|
+
if (resolved === undefined) {
|
|
324
|
+
throw new ArtifactMissingError(req.line, this.platform, this.searchPath);
|
|
186
325
|
}
|
|
326
|
+
// resolveSync (resolvePatchSync, for a patch entry) already warns
|
|
327
|
+
// internally when it falls back — nothing further to do here.
|
|
187
328
|
}
|
|
188
329
|
/** dlopen one artifact directory, cross-check it, `chs_init` it, index it. */
|
|
189
330
|
load(sub, manifest) {
|
|
@@ -217,73 +358,130 @@ export class Registry {
|
|
|
217
358
|
native.init(this.timezone, readUnsafeFamilies(sub, manifest));
|
|
218
359
|
const library = new Library(native);
|
|
219
360
|
this.loaded.push(library);
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
this.loaded.sort((a, b) =>
|
|
361
|
+
// Full numeric version order (docs/reference/bindings.md §Version
|
|
362
|
+
// selection, rule 2): two patches of one line both sort by their own
|
|
363
|
+
// exact version, never merely grouped by minor.
|
|
364
|
+
this.loaded.sort((a, b) => compareVersions(a.version, b.version));
|
|
224
365
|
this.byPath.set(sub, library);
|
|
225
|
-
// Indexed under both spellings: docker tags drift, and an exact-match-only
|
|
226
|
-
// lookup silently loses a whole version column. First directory wins: a
|
|
227
|
-
// line already loaded from earlier on the search path is not displaced.
|
|
228
|
-
if (!this.byId.has(library.version))
|
|
229
|
-
this.byId.set(library.version, library);
|
|
230
|
-
if (!this.byId.has(library.minor))
|
|
231
|
-
this.byId.set(library.minor, library);
|
|
232
366
|
return library;
|
|
233
367
|
}
|
|
234
|
-
/** Load
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
if (!isDirectory(sub))
|
|
238
|
-
return null;
|
|
239
|
-
const manifest = readManifest(sub);
|
|
368
|
+
/** Load an artifact directory whose manifest is already known-readable, or return null when it is not one. */
|
|
369
|
+
loadDir(dir) {
|
|
370
|
+
const manifest = readManifest(dir);
|
|
240
371
|
if (manifest === null)
|
|
241
372
|
return null;
|
|
242
|
-
return this.load(
|
|
373
|
+
return this.load(dir, manifest);
|
|
243
374
|
}
|
|
244
|
-
|
|
245
|
-
|
|
375
|
+
/** Every patch (both slots, every search-path root) this registry can currently see for `minor` — a live scan. */
|
|
376
|
+
scanLine(minor) {
|
|
377
|
+
const out = [];
|
|
378
|
+
this.searchPath.forEach((root, rootIndex) => {
|
|
379
|
+
for (const loc of locationsInRoot(root, rootIndex)) {
|
|
380
|
+
if (loc.minor === minor)
|
|
381
|
+
out.push(loc);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
return out;
|
|
385
|
+
}
|
|
386
|
+
/** R3: line request — the pin if this registry already has one, else the newest patch in the first root that holds any. */
|
|
387
|
+
resolveLineSync(req) {
|
|
388
|
+
const pinned = this.linePins.get(req.line);
|
|
389
|
+
if (pinned !== undefined)
|
|
390
|
+
return { library: pinned, actual: pinned.version, exact: true };
|
|
391
|
+
const winner = pickLineWinner(this.scanLine(req.line));
|
|
392
|
+
if (winner === undefined)
|
|
393
|
+
return undefined;
|
|
394
|
+
const library = this.loadDir(winner.dir);
|
|
395
|
+
if (library === null)
|
|
396
|
+
return undefined;
|
|
397
|
+
this.linePins.set(req.line, library);
|
|
398
|
+
return { library, actual: library.version, exact: true };
|
|
399
|
+
}
|
|
400
|
+
/** R4 steps 1-2: an exact match, already open or anywhere on the search path — never a fallback. */
|
|
401
|
+
resolveExactPatchSync(req) {
|
|
402
|
+
const already = this.loaded.find((l) => patchMatches(l.version, req));
|
|
403
|
+
if (already !== undefined)
|
|
404
|
+
return already;
|
|
405
|
+
const matched = pickPatchMatch(this.scanLine(req.line), req);
|
|
406
|
+
if (matched === undefined)
|
|
407
|
+
return undefined;
|
|
408
|
+
return this.loadDir(matched.dir) ?? undefined;
|
|
246
409
|
}
|
|
247
410
|
/**
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
411
|
+
* R4 in full, synchronous shape: exact, else the newest-installed fallback
|
|
412
|
+
* within the line. R-c: the already-open check is free (no I/O) and always
|
|
413
|
+
* current; everything past it needs a directory read per search-path entry,
|
|
414
|
+
* so once a request has fallen back, the WHOLE re-check — retrying the exact
|
|
415
|
+
* match and picking the fallback alike — is throttled together, at most
|
|
416
|
+
* once per `FALLBACK_RECHECK_MS` per requested patch per process.
|
|
417
|
+
*
|
|
418
|
+
* §3: the warning fires as soon as the fallback patch is CHOSEN — its
|
|
419
|
+
* version is already known from the manifest scan, before `loadDir` ever
|
|
420
|
+
* runs — so a caller sees the warning even when the chosen directory then
|
|
421
|
+
* fails to load (a broken artifact is still a fallback that was taken).
|
|
254
422
|
*/
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
if (
|
|
258
|
-
return
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
423
|
+
resolvePatchSync(req) {
|
|
424
|
+
const already = this.loaded.find((l) => patchMatches(l.version, req));
|
|
425
|
+
if (already !== undefined)
|
|
426
|
+
return { library: already, actual: already.version, exact: true };
|
|
427
|
+
const cacheKey = req.exact;
|
|
428
|
+
const cached = fallbackCache.get(cacheKey);
|
|
429
|
+
const now = Date.now();
|
|
430
|
+
if (cached !== undefined && now - cached.checkedAt < FALLBACK_RECHECK_MS) {
|
|
431
|
+
// Within the window: skip the re-scan (the exact-match retry AND the
|
|
432
|
+
// fallback pick alike), but still attempt to load the chosen directory
|
|
433
|
+
// — one already-known path, not a directory read per search-path
|
|
434
|
+
// entry, so a broken artifact keeps failing rather than being silently
|
|
435
|
+
// remembered as fine.
|
|
436
|
+
const library = this.loadDir(cached.dir);
|
|
437
|
+
if (library === null)
|
|
438
|
+
return undefined;
|
|
439
|
+
return { library, actual: library.version, exact: false };
|
|
273
440
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
441
|
+
const locs = this.scanLine(req.line);
|
|
442
|
+
const matched = pickPatchMatch(locs, req);
|
|
443
|
+
if (matched !== undefined) {
|
|
444
|
+
const library = this.loadDir(matched.dir);
|
|
445
|
+
if (library !== null) {
|
|
446
|
+
fallbackCache.delete(cacheKey);
|
|
447
|
+
return { library, actual: library.version, exact: true };
|
|
279
448
|
}
|
|
280
449
|
}
|
|
281
|
-
|
|
450
|
+
const winner = pickLineWinner(locs);
|
|
451
|
+
if (winner === undefined) {
|
|
452
|
+
fallbackCache.delete(cacheKey);
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
this.warnFallback(req, winner.version, 'installed');
|
|
456
|
+
fallbackCache.set(cacheKey, { dir: winner.dir, checkedAt: now });
|
|
457
|
+
const library = this.loadDir(winner.dir);
|
|
458
|
+
if (library === null)
|
|
459
|
+
return undefined;
|
|
460
|
+
return { library, actual: library.version, exact: false };
|
|
461
|
+
}
|
|
462
|
+
resolveSync(req) {
|
|
463
|
+
return req.exact === null ? this.resolveLineSync(req) : this.resolvePatchSync(req);
|
|
464
|
+
}
|
|
465
|
+
/** Would `req` resolve without opening anything? Mirrors `resolveSync` with no `load()` call. */
|
|
466
|
+
wouldResolve(req) {
|
|
467
|
+
if (req.exact === null) {
|
|
468
|
+
if (this.linePins.has(req.line))
|
|
469
|
+
return true;
|
|
470
|
+
return pickLineWinner(this.scanLine(req.line)) !== undefined;
|
|
471
|
+
}
|
|
472
|
+
if (this.loaded.some((l) => patchMatches(l.version, req)))
|
|
473
|
+
return true;
|
|
474
|
+
// A patch resolves via its line's fallback too (R9: has() is true when the
|
|
475
|
+
// patch, or any patch of its line, is installed or open).
|
|
476
|
+
return this.scanLine(req.line).length > 0;
|
|
477
|
+
}
|
|
478
|
+
warnFallback(req, actual, kind) {
|
|
479
|
+
emitPatchFallbackWarning(req.exact, actual, req.line, this.platform, kind);
|
|
282
480
|
}
|
|
283
481
|
/**
|
|
284
482
|
* Every ClickHouse minor line this registry CAN ANSWER FOR, oldest first —
|
|
285
483
|
* the ones it has opened plus the ones its construction-time manifest scan
|
|
286
|
-
* discovered on the search path.
|
|
484
|
+
* discovered on the search path, at either slot.
|
|
287
485
|
*
|
|
288
486
|
* That is one meaning in all four bindings, and it is the meaning that
|
|
289
487
|
* survives lazy loading: "the lines that happen to be open" would read as an
|
|
@@ -296,84 +494,172 @@ export class Registry {
|
|
|
296
494
|
return [...lines].sort(compareMinor);
|
|
297
495
|
}
|
|
298
496
|
/**
|
|
299
|
-
* The libraries this registry has OPENED, in
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
* and
|
|
497
|
+
* The libraries this registry has OPENED, in full numeric version order —
|
|
498
|
+
* what is open right now, never what could be. Two patches of one line both
|
|
499
|
+
* appear, each in its own slot in this order. A discovered patch that no
|
|
500
|
+
* `for()` and no `preload` has opened appears in `versions()` and not here.
|
|
501
|
+
* It opens nothing.
|
|
303
502
|
*/
|
|
304
503
|
libraries() {
|
|
305
504
|
return this.loaded;
|
|
306
505
|
}
|
|
307
506
|
/**
|
|
308
|
-
* Resolve a version to its library. A minor line ("25.8")
|
|
309
|
-
*
|
|
310
|
-
*
|
|
507
|
+
* Resolve a version to its library. A minor line ("25.8") never falls back:
|
|
508
|
+
* the newest patch of the line, from the first search-path directory that
|
|
509
|
+
* holds any patch of it. An exact patch ("25.8.28.1-lts") loads that patch
|
|
510
|
+
* when it is open or installed anywhere on the search path; otherwise the
|
|
511
|
+
* newest installed patch of the same line is loaded instead, and one
|
|
512
|
+
* warning is written per (requested, actual) pair per process — `resolve()`
|
|
513
|
+
* reports the same fallback as `exact: false` instead of only warning.
|
|
311
514
|
*
|
|
312
|
-
* **This is what opens an artifact.** Construction does not:
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
* may fetch.
|
|
515
|
+
* **This is what opens an artifact.** Construction does not: a version is
|
|
516
|
+
* loaded on first request, `dlopen`ed once, and joins `libraries()` from
|
|
517
|
+
* then on. Never a fetch: this call is synchronous; `open()` /
|
|
518
|
+
* `openResolution()` are the ones that may fetch.
|
|
317
519
|
*
|
|
318
|
-
* Failure is the one §7 error, never a fallback to
|
|
319
|
-
*
|
|
320
|
-
*
|
|
520
|
+
* Failure is the one §7 error, never a fallback to another line: answering
|
|
521
|
+
* 26.7 semantics from a 25.8 artifact is a lie, and silent wrongness is what
|
|
522
|
+
* the rigs score hardest.
|
|
321
523
|
*
|
|
322
524
|
* @param version - a minor line (`"25.8"`) or an exact patch
|
|
323
525
|
* (`"25.8.28.1-lts"`), e.g. what `parseVersionResult` discovered.
|
|
324
526
|
* @returns the loaded `Library` for that version.
|
|
325
527
|
* @throws {ArtifactMissingError} (`code` `CHTYPES_ARTIFACT_MISSING`, a
|
|
326
|
-
* `RegistryError`) when no directory on the search path holds
|
|
327
|
-
* the message names every directory looked in and the
|
|
328
|
-
*
|
|
528
|
+
* `RegistryError`) when no directory on the search path holds a matching
|
|
529
|
+
* or fallback patch; the message names every directory looked in and the
|
|
530
|
+
* fetch command.
|
|
531
|
+
* @throws {ChtypesError} when `version` is not a ClickHouse version spelling at all.
|
|
532
|
+
* @throws {RegistryError} when a directory holds a version but it does not load.
|
|
329
533
|
*/
|
|
330
534
|
for(version) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
535
|
+
return this.resolve(version).library;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* `for()`, but returns the full `Resolution` — the requested spelling, what
|
|
539
|
+
* actually loaded, and whether the two are the same patch. Never fetches;
|
|
540
|
+
* `openResolution()` is the async twin that may.
|
|
541
|
+
*
|
|
542
|
+
* @throws {ArtifactMissingError} as `for()`.
|
|
543
|
+
* @throws {ChtypesError} when `version` is not a ClickHouse version spelling at all.
|
|
544
|
+
*/
|
|
545
|
+
resolve(version) {
|
|
546
|
+
const requested = version.trim();
|
|
547
|
+
const req = parseVersionSpelling(version);
|
|
548
|
+
// resolveSync (resolvePatchSync, for a patch request) warns internally
|
|
549
|
+
// when it falls back, at the moment the fallback patch is CHOSEN — before
|
|
550
|
+
// load, so the warning still fires even if that load then fails.
|
|
551
|
+
const resolved = this.resolveSync(req);
|
|
552
|
+
if (resolved === undefined)
|
|
553
|
+
throw new ArtifactMissingError(req.line, this.platform, this.searchPath);
|
|
554
|
+
return { library: resolved.library, requested, version: resolved.actual, exact: resolved.exact };
|
|
335
555
|
}
|
|
336
556
|
/**
|
|
337
|
-
* `for()`, with the lazy fetch of docs/guides/fetch.md §6 in front of it
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
* with
|
|
557
|
+
* `for()`, with the lazy fetch of docs/guides/fetch.md §6 in front of it. A
|
|
558
|
+
* LINE request no directory holds is fetched through `ensure()` and loaded.
|
|
559
|
+
* A PATCH request tries `ensure()` of that exact patch first; on
|
|
560
|
+
* `CHTYPES_ARTIFACT_UNPUBLISHED` (remembered for this registry, so it costs
|
|
561
|
+
* one network round trip, not one per call) it falls back to `ensure()` of
|
|
562
|
+
* the line and loads whatever that installs, with `exact: false` and one
|
|
563
|
+
* warning. Any other fetch failure (untrusted, corrupt, pinned, source
|
|
564
|
+
* unreachable) surfaces as itself and is never remembered, so a later call
|
|
565
|
+
* retries. With `autofetch` off, this is `for()` behind a promise.
|
|
343
566
|
*
|
|
344
|
-
* @throws {ArtifactMissingError} when
|
|
567
|
+
* @throws {ArtifactMissingError} when nothing is found and autofetch is off.
|
|
345
568
|
* @throws {FetchError} the §7 fetch verdicts (`CHTYPES_ARTIFACT_UNTRUSTED`,
|
|
346
569
|
* `…_CORRUPT`, `…_PINNED`, `…_UNPUBLISHED`, `CHTYPES_SOURCE_UNREACHABLE`).
|
|
347
570
|
* @throws {RegistryError} when the fetched artifact does not load.
|
|
348
571
|
*/
|
|
349
572
|
async open(version) {
|
|
350
|
-
|
|
351
|
-
|
|
573
|
+
return (await this.openResolution(version)).library;
|
|
574
|
+
}
|
|
575
|
+
/** `open()`, but returns the full `Resolution` — see `resolve()` and `open()`. */
|
|
576
|
+
async openResolution(version) {
|
|
577
|
+
const requested = version.trim();
|
|
578
|
+
const req = parseVersionSpelling(version);
|
|
579
|
+
const dest = this.fetchOptions.dest ?? fetchDestination(this.explicit, this.platform);
|
|
580
|
+
if (req.exact === null) {
|
|
581
|
+
const sync = this.resolveLineSync(req);
|
|
582
|
+
if (sync !== undefined)
|
|
583
|
+
return { library: sync.library, requested, version: sync.actual, exact: true };
|
|
584
|
+
if (!this.autofetch)
|
|
585
|
+
throw new ArtifactMissingError(req.line, this.platform, this.searchPath);
|
|
586
|
+
await this.ensureLineOnce(req.line, dest);
|
|
587
|
+
const after = this.resolveLineSync(req);
|
|
588
|
+
if (after === undefined)
|
|
589
|
+
throw new ArtifactMissingError(req.line, this.platform, this.searchPath);
|
|
590
|
+
return { library: after.library, requested, version: after.actual, exact: true };
|
|
352
591
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
592
|
+
const exact = this.resolveExactPatchSync(req);
|
|
593
|
+
if (exact !== undefined)
|
|
594
|
+
return { library: exact, requested, version: exact.version, exact: true };
|
|
595
|
+
if (this.autofetch) {
|
|
596
|
+
const unpublishedKey = `${dest}\u0000${req.exact}`;
|
|
597
|
+
if (!this.unpublishedPatches.has(unpublishedKey)) {
|
|
598
|
+
try {
|
|
599
|
+
await ensure(req.exact, { ...this.fetchOptions, dest, platform: this.platform });
|
|
600
|
+
}
|
|
601
|
+
catch (err) {
|
|
602
|
+
if (err instanceof ArtifactUnpublishedError) {
|
|
603
|
+
this.unpublishedPatches.add(unpublishedKey);
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
// Untrusted, corrupt, pinned or unreachable: surface it as-is and
|
|
607
|
+
// remember nothing, so a later call retries (R4 step 3).
|
|
608
|
+
throw err;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (!this.unpublishedPatches.has(unpublishedKey)) {
|
|
612
|
+
const found = this.resolveExactPatchSync(req);
|
|
613
|
+
if (found !== undefined)
|
|
614
|
+
return { library: found, requested, version: found.version, exact: true };
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
// Step 4 (autofetch on): Ensure(line) at most once per (dest, line) for this registry.
|
|
618
|
+
await this.ensureLineOnce(req.line, dest);
|
|
619
|
+
const winner = pickLineWinner(this.scanLine(req.line));
|
|
620
|
+
if (winner === undefined)
|
|
621
|
+
throw new ArtifactMissingError(req.line, this.platform, this.searchPath);
|
|
622
|
+
// Warn on the CHOSEN version, before the load — so a caller sees it even
|
|
623
|
+
// if the chosen directory then fails to load (§3).
|
|
624
|
+
this.warnFallback(req, winner.version, 'published');
|
|
625
|
+
const library = this.loadDir(winner.dir);
|
|
626
|
+
if (library === null)
|
|
627
|
+
throw new ArtifactMissingError(req.line, this.platform, this.searchPath);
|
|
628
|
+
return { library, requested, version: library.version, exact: false };
|
|
356
629
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
630
|
+
// Autofetch off: the same synchronous fallback `for()`/`resolve()` take
|
|
631
|
+
// (resolvePatchSync warns internally when it falls back).
|
|
632
|
+
const resolved = this.resolvePatchSync(req);
|
|
633
|
+
if (resolved === undefined)
|
|
634
|
+
throw new ArtifactMissingError(req.line, this.platform, this.searchPath);
|
|
635
|
+
return { library: resolved.library, requested, version: resolved.actual, exact: resolved.exact };
|
|
636
|
+
}
|
|
637
|
+
/** `ensure(line)`, at most once per (dest, line) over this registry's lifetime — never re-read over the network again. */
|
|
638
|
+
async ensureLineOnce(line, dest) {
|
|
639
|
+
const key = `${dest}\u0000${line}`;
|
|
640
|
+
if (this.ensuredLines.has(key))
|
|
641
|
+
return;
|
|
642
|
+
const result = await ensure(line, { ...this.fetchOptions, dest, platform: this.platform });
|
|
643
|
+
this.loadDir(result.dir);
|
|
644
|
+
this.ensuredLines.add(key);
|
|
362
645
|
}
|
|
363
646
|
/**
|
|
364
|
-
* True when this registry can answer for a version
|
|
365
|
-
* by a directory on the search path (
|
|
366
|
-
*
|
|
647
|
+
* True when this registry can answer for a version without fetching or
|
|
648
|
+
* loading: already open, or held by a directory on the search path (at
|
|
649
|
+
* either slot) which `for()` would load. A patch resolves true when the
|
|
650
|
+
* patch itself, or any patch of its line, is installed or open — the same
|
|
651
|
+
* condition `for()` would resolve, fallback included. Never a fetch, and
|
|
652
|
+
* never a load.
|
|
367
653
|
*/
|
|
368
654
|
has(version) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
655
|
+
let req;
|
|
656
|
+
try {
|
|
657
|
+
req = parseVersionSpelling(version);
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
return this.wouldResolve(req);
|
|
377
663
|
}
|
|
378
664
|
/**
|
|
379
665
|
* Join every loaded library's background threads. `chs_init` registers
|
|
@@ -436,13 +722,17 @@ export function resolveRegistryDir(explicit) {
|
|
|
436
722
|
export function defaultRegistryDir() {
|
|
437
723
|
return cacheRegistryDir(hostPlatform());
|
|
438
724
|
}
|
|
439
|
-
/**
|
|
725
|
+
/**
|
|
726
|
+
* Does this directory hold at least one artifact with a usable manifest, at
|
|
727
|
+
* either slot — the flat `<dir>/<minor>/` layout or the `<dir>/patches/<minor>/<version>/`
|
|
728
|
+
* sibling tree? A registry that holds only nested installs is not empty.
|
|
729
|
+
*/
|
|
440
730
|
export function looksLikeRegistry(dir) {
|
|
441
731
|
if (!isDirectory(dir))
|
|
442
732
|
return false;
|
|
443
733
|
try {
|
|
444
|
-
|
|
445
|
-
if (entry.startsWith('.'))
|
|
734
|
+
const hasFlat = readdirSync(dir).some((entry) => {
|
|
735
|
+
if (entry.startsWith('.') || entry === 'patches')
|
|
446
736
|
return false;
|
|
447
737
|
const sub = path.join(dir, entry);
|
|
448
738
|
if (!isDirectory(sub))
|
|
@@ -450,6 +740,30 @@ export function looksLikeRegistry(dir) {
|
|
|
450
740
|
const manifest = readManifest(sub);
|
|
451
741
|
return manifest !== null && existsSync(path.join(sub, manifest.library));
|
|
452
742
|
});
|
|
743
|
+
if (hasFlat)
|
|
744
|
+
return true;
|
|
745
|
+
const patchesDir = path.join(dir, 'patches');
|
|
746
|
+
if (!isDirectory(patchesDir))
|
|
747
|
+
return false;
|
|
748
|
+
return readdirSync(patchesDir).some((minorEntry) => {
|
|
749
|
+
const minorDir = path.join(patchesDir, minorEntry);
|
|
750
|
+
if (!isDirectory(minorDir))
|
|
751
|
+
return false;
|
|
752
|
+
let versionEntries;
|
|
753
|
+
try {
|
|
754
|
+
versionEntries = readdirSync(minorDir);
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
return versionEntries.some((versionEntry) => {
|
|
760
|
+
const sub = path.join(minorDir, versionEntry);
|
|
761
|
+
if (!isDirectory(sub))
|
|
762
|
+
return false;
|
|
763
|
+
const manifest = readManifest(sub);
|
|
764
|
+
return manifest !== null && existsSync(path.join(sub, manifest.library));
|
|
765
|
+
});
|
|
766
|
+
});
|
|
453
767
|
}
|
|
454
768
|
catch {
|
|
455
769
|
return false;
|