@camstack/system 1.2.3 → 1.2.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/dist/builtins/device-manager/device-linked-devices.d.ts +6 -3
- package/dist/builtins/device-manager/device-manager.addon.js +13 -6
- package/dist/builtins/device-manager/device-manager.addon.mjs +13 -6
- package/dist/index.d.ts +2 -1
- package/dist/index.js +283 -148
- package/dist/index.mjs +274 -142
- package/dist/kernel/deps/ensure-addon-natives.d.ts +41 -0
- package/dist/kernel/deps/ensure-native-prebuilds.d.ts +4 -0
- package/package.json +1 -1
|
@@ -4,7 +4,9 @@ import { ProviderContext } from './device-provider-context.js';
|
|
|
4
4
|
export declare const LINKED_MODE_CONFIG_KEY = "_linkedDevicesMode";
|
|
5
5
|
export declare const LINKED_IDS_CONFIG_KEY = "_linkedDeviceIds";
|
|
6
6
|
/** Reserved key: subset of linked devices that materialize synthetic tracked
|
|
7
|
-
* events on the camera. Absent ⇒
|
|
7
|
+
* events on the camera. Absent ⇒ NONE — producing tracked events is an
|
|
8
|
+
* explicit operator OPT-IN (2026-07-22 flip: the old absent⇒ALL default
|
|
9
|
+
* auto-materialized sensor snapshots the operator never asked for). */
|
|
8
10
|
export declare const LINKED_TRACKED_IDS_CONFIG_KEY = "_linkedDeviceTrackedIds";
|
|
9
11
|
export interface LinkedDevicesConfig {
|
|
10
12
|
readonly mode: DeviceLinkMode;
|
|
@@ -32,8 +34,9 @@ export declare function resolveLinkedDeviceIds(params: {
|
|
|
32
34
|
}): readonly number[];
|
|
33
35
|
/**
|
|
34
36
|
* Which linked devices materialize synthetic tracked events on the camera.
|
|
35
|
-
* Absent / non-array config key ⇒
|
|
36
|
-
*
|
|
37
|
+
* Absent / non-array config key ⇒ NONE — the operator must explicitly pick
|
|
38
|
+
* producers (opt-in; flipped from the legacy absent⇒ALL default, which
|
|
39
|
+
* auto-materialized sensor snapshots for every linked device). Otherwise the
|
|
37
40
|
* configured subset intersected with the currently-resolved linked set (a
|
|
38
41
|
* picked id no longer linked is dropped). Ids may arrive as numbers or numeric
|
|
39
42
|
* strings (the multiselect stores string option values); non-numeric entries
|
|
@@ -1210,7 +1210,9 @@ async function testField(pctx, input) {
|
|
|
1210
1210
|
var LINKED_MODE_CONFIG_KEY = "_linkedDevicesMode";
|
|
1211
1211
|
var LINKED_IDS_CONFIG_KEY = "_linkedDeviceIds";
|
|
1212
1212
|
/** Reserved key: subset of linked devices that materialize synthetic tracked
|
|
1213
|
-
* events on the camera. Absent ⇒
|
|
1213
|
+
* events on the camera. Absent ⇒ NONE — producing tracked events is an
|
|
1214
|
+
* explicit operator OPT-IN (2026-07-22 flip: the old absent⇒ALL default
|
|
1215
|
+
* auto-materialized sensor snapshots the operator never asked for). */
|
|
1214
1216
|
var LINKED_TRACKED_IDS_CONFIG_KEY = "_linkedDeviceTrackedIds";
|
|
1215
1217
|
/** Parse the two reserved keys out of a device-config blob. Tolerant:
|
|
1216
1218
|
* ids may arrive as numbers or numeric strings (the multiselect field
|
|
@@ -1249,8 +1251,9 @@ function resolveLinkedDeviceIds(params) {
|
|
|
1249
1251
|
}
|
|
1250
1252
|
/**
|
|
1251
1253
|
* Which linked devices materialize synthetic tracked events on the camera.
|
|
1252
|
-
* Absent / non-array config key ⇒
|
|
1253
|
-
*
|
|
1254
|
+
* Absent / non-array config key ⇒ NONE — the operator must explicitly pick
|
|
1255
|
+
* producers (opt-in; flipped from the legacy absent⇒ALL default, which
|
|
1256
|
+
* auto-materialized sensor snapshots for every linked device). Otherwise the
|
|
1254
1257
|
* configured subset intersected with the currently-resolved linked set (a
|
|
1255
1258
|
* picked id no longer linked is dropped). Ids may arrive as numbers or numeric
|
|
1256
1259
|
* strings (the multiselect stores string option values); non-numeric entries
|
|
@@ -1258,7 +1261,7 @@ function resolveLinkedDeviceIds(params) {
|
|
|
1258
1261
|
*/
|
|
1259
1262
|
function resolveTrackedEventDeviceIds(config, linkedIds) {
|
|
1260
1263
|
const raw = config[LINKED_TRACKED_IDS_CONFIG_KEY];
|
|
1261
|
-
if (!Array.isArray(raw)) return
|
|
1264
|
+
if (!Array.isArray(raw)) return [];
|
|
1262
1265
|
const picked = /* @__PURE__ */ new Set();
|
|
1263
1266
|
for (const v of raw) {
|
|
1264
1267
|
const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN;
|
|
@@ -1363,7 +1366,7 @@ async function buildLinkedDevicesContribution(pctx, deviceId) {
|
|
|
1363
1366
|
type: "multiselect",
|
|
1364
1367
|
key: LINKED_TRACKED_IDS_CONFIG_KEY,
|
|
1365
1368
|
label: "Devices that produce tracked events",
|
|
1366
|
-
description: "Linked devices whose state changes materialize a synthetic tracked event on this camera. Default:
|
|
1369
|
+
description: "Linked devices whose state changes materialize a synthetic tracked event on this camera. Default: none (opt-in).",
|
|
1367
1370
|
options: trackedOptions,
|
|
1368
1371
|
value: trackedValue
|
|
1369
1372
|
}
|
|
@@ -1396,7 +1399,11 @@ async function applyLinkedDevicesPatch(pctx, deviceId, patch) {
|
|
|
1396
1399
|
//#region src/builtins/device-manager/device-store-sections.ts
|
|
1397
1400
|
/** The device-manager's device-store-backed sections. Add an entry to extend. */
|
|
1398
1401
|
var DEVICE_STORE_SECTIONS = [{
|
|
1399
|
-
keys: [
|
|
1402
|
+
keys: [
|
|
1403
|
+
LINKED_MODE_CONFIG_KEY,
|
|
1404
|
+
LINKED_IDS_CONFIG_KEY,
|
|
1405
|
+
LINKED_TRACKED_IDS_CONFIG_KEY
|
|
1406
|
+
],
|
|
1400
1407
|
apply: (pctx, deviceId, patch) => applyLinkedDevicesPatch(pctx, deviceId, patch)
|
|
1401
1408
|
}];
|
|
1402
1409
|
/**
|
|
@@ -1205,7 +1205,9 @@ async function testField(pctx, input) {
|
|
|
1205
1205
|
var LINKED_MODE_CONFIG_KEY = "_linkedDevicesMode";
|
|
1206
1206
|
var LINKED_IDS_CONFIG_KEY = "_linkedDeviceIds";
|
|
1207
1207
|
/** Reserved key: subset of linked devices that materialize synthetic tracked
|
|
1208
|
-
* events on the camera. Absent ⇒
|
|
1208
|
+
* events on the camera. Absent ⇒ NONE — producing tracked events is an
|
|
1209
|
+
* explicit operator OPT-IN (2026-07-22 flip: the old absent⇒ALL default
|
|
1210
|
+
* auto-materialized sensor snapshots the operator never asked for). */
|
|
1209
1211
|
var LINKED_TRACKED_IDS_CONFIG_KEY = "_linkedDeviceTrackedIds";
|
|
1210
1212
|
/** Parse the two reserved keys out of a device-config blob. Tolerant:
|
|
1211
1213
|
* ids may arrive as numbers or numeric strings (the multiselect field
|
|
@@ -1244,8 +1246,9 @@ function resolveLinkedDeviceIds(params) {
|
|
|
1244
1246
|
}
|
|
1245
1247
|
/**
|
|
1246
1248
|
* Which linked devices materialize synthetic tracked events on the camera.
|
|
1247
|
-
* Absent / non-array config key ⇒
|
|
1248
|
-
*
|
|
1249
|
+
* Absent / non-array config key ⇒ NONE — the operator must explicitly pick
|
|
1250
|
+
* producers (opt-in; flipped from the legacy absent⇒ALL default, which
|
|
1251
|
+
* auto-materialized sensor snapshots for every linked device). Otherwise the
|
|
1249
1252
|
* configured subset intersected with the currently-resolved linked set (a
|
|
1250
1253
|
* picked id no longer linked is dropped). Ids may arrive as numbers or numeric
|
|
1251
1254
|
* strings (the multiselect stores string option values); non-numeric entries
|
|
@@ -1253,7 +1256,7 @@ function resolveLinkedDeviceIds(params) {
|
|
|
1253
1256
|
*/
|
|
1254
1257
|
function resolveTrackedEventDeviceIds(config, linkedIds) {
|
|
1255
1258
|
const raw = config[LINKED_TRACKED_IDS_CONFIG_KEY];
|
|
1256
|
-
if (!Array.isArray(raw)) return
|
|
1259
|
+
if (!Array.isArray(raw)) return [];
|
|
1257
1260
|
const picked = /* @__PURE__ */ new Set();
|
|
1258
1261
|
for (const v of raw) {
|
|
1259
1262
|
const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN;
|
|
@@ -1358,7 +1361,7 @@ async function buildLinkedDevicesContribution(pctx, deviceId) {
|
|
|
1358
1361
|
type: "multiselect",
|
|
1359
1362
|
key: LINKED_TRACKED_IDS_CONFIG_KEY,
|
|
1360
1363
|
label: "Devices that produce tracked events",
|
|
1361
|
-
description: "Linked devices whose state changes materialize a synthetic tracked event on this camera. Default:
|
|
1364
|
+
description: "Linked devices whose state changes materialize a synthetic tracked event on this camera. Default: none (opt-in).",
|
|
1362
1365
|
options: trackedOptions,
|
|
1363
1366
|
value: trackedValue
|
|
1364
1367
|
}
|
|
@@ -1391,7 +1394,11 @@ async function applyLinkedDevicesPatch(pctx, deviceId, patch) {
|
|
|
1391
1394
|
//#region src/builtins/device-manager/device-store-sections.ts
|
|
1392
1395
|
/** The device-manager's device-store-backed sections. Add an entry to extend. */
|
|
1393
1396
|
var DEVICE_STORE_SECTIONS = [{
|
|
1394
|
-
keys: [
|
|
1397
|
+
keys: [
|
|
1398
|
+
LINKED_MODE_CONFIG_KEY,
|
|
1399
|
+
LINKED_IDS_CONFIG_KEY,
|
|
1400
|
+
LINKED_TRACKED_IDS_CONFIG_KEY
|
|
1401
|
+
],
|
|
1395
1402
|
apply: (pctx, deviceId, patch) => applyLinkedDevicesPatch(pctx, deviceId, patch)
|
|
1396
1403
|
}];
|
|
1397
1404
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -85,4 +85,5 @@ export type { DirectCallerOptions } from './addon/addon-api-factory.js';
|
|
|
85
85
|
export { IntegrationRegistry } from './builtins/sqlite-storage/integration-registry.js';
|
|
86
86
|
export * from './kernel/index.js';
|
|
87
87
|
export { runNpm, resolveNpmInvocation, type RunNpmOptions, type NpmInvocation, } from './kernel/deps/npm-command.js';
|
|
88
|
-
export { ensureNativePrebuilds, TRADITIONAL_NATIVE_PACKAGES, type EnsureNativePrebuildsOptions, type PrebuildTarget, type PrebuildFetchFn, } from './kernel/deps/ensure-native-prebuilds.js';
|
|
88
|
+
export { ensureNativePrebuilds, hasDotNode, NATIVE_SCAN_DEPTH, TRADITIONAL_NATIVE_PACKAGES, type EnsureNativePrebuildsOptions, type PrebuildTarget, type PrebuildFetchFn, } from './kernel/deps/ensure-native-prebuilds.js';
|
|
89
|
+
export { ensureAddonNativePrebuilds, type EnsureAddonNativePrebuildsOptions, type NativeEnsureOutcome, type NativeEnsureResult, } from './kernel/deps/ensure-addon-natives.js';
|
package/dist/index.js
CHANGED
|
@@ -3030,8 +3030,270 @@ var AddonManifest = class {
|
|
|
3030
3030
|
}
|
|
3031
3031
|
};
|
|
3032
3032
|
//#endregion
|
|
3033
|
-
//#region src/kernel/
|
|
3033
|
+
//#region src/kernel/deps/ensure-native-prebuilds.ts
|
|
3034
|
+
/**
|
|
3035
|
+
* `ensureNativePrebuilds` — the post-install native-prebuild ENSURE step the
|
|
3036
|
+
* shared `RootUpdateService` runs while staging a `@camstack/server` closure,
|
|
3037
|
+
* BEFORE its `findMissingNativePrebuilds` validator (in `@camstack/node-root`)
|
|
3038
|
+
* decides whether to arm the swap.
|
|
3039
|
+
*
|
|
3040
|
+
* Why this exists (Mac desktop incident, 2026-07-21): a runtime
|
|
3041
|
+
* `applyServerUpdate` on a packaged desktop stages the closure with
|
|
3042
|
+
* `npm install @camstack/server@X` under the app's BUNDLED bare Node. The one
|
|
3043
|
+
* TRADITIONAL (non-N-API) native, `better-sqlite3`, resolves its `.node` via
|
|
3044
|
+
* its `install` script `prebuild-install || node-gyp rebuild`. On a user's Mac
|
|
3045
|
+
* with no build toolchain (and where the auto `prebuild-install` does not
|
|
3046
|
+
* target the bundled Node's ABI/arch the way the DMG build's EXPLICIT
|
|
3047
|
+
* `prebuild-install --runtime node --target <bundled> --arch <arch>` call does)
|
|
3048
|
+
* that leaves the module SOURCE-ONLY → the closure has no `better-sqlite3`
|
|
3049
|
+
* `.node` → the validator refuses the swap ("staged closure missing native
|
|
3050
|
+
* prebuilds: better-sqlite3"). The docker image never hits this: its Node ABI
|
|
3051
|
+
* has a published prebuild the auto path fetches cleanly, and it carries a
|
|
3052
|
+
* build toolchain fallback.
|
|
3053
|
+
*
|
|
3054
|
+
* This mirrors the DMG build's explicit re-fetch (`scripts/stage-deps.mjs`)
|
|
3055
|
+
* into the runtime staging path: for each traditional native that is present
|
|
3056
|
+
* in the closure but missing its compiled `.node`, fetch the prebuild for the
|
|
3057
|
+
* RUNNING Node ABI/arch (the same Node that will load the swapped closure).
|
|
3058
|
+
*
|
|
3059
|
+
* N-API natives (`sharp`, `node-av`) are ABI-stable and ship their binaries as
|
|
3060
|
+
* platform SIBLING packages that npm fetches reliably with no compile step, so
|
|
3061
|
+
* they are not in scope here — a genuine miss there is a broken install the
|
|
3062
|
+
* validator should still reject, not something a prebuild re-fetch can heal.
|
|
3063
|
+
*
|
|
3064
|
+
* Injected into the update service from the construction site so the zero-dep
|
|
3065
|
+
* `@camstack/node-root` layer never depends on npm/@camstack/system, and the
|
|
3066
|
+
* fetch is unit-testable with a fake fetcher.
|
|
3067
|
+
*/
|
|
3034
3068
|
var execFileAsync$1 = (0, node_util.promisify)(node_child_process.execFile);
|
|
3069
|
+
/** Bounded recursive `.node` scan depth inside a module dir. */
|
|
3070
|
+
var NATIVE_SCAN_DEPTH = 4;
|
|
3071
|
+
/** Timeout for a single `prebuild-install` fetch. */
|
|
3072
|
+
var PREBUILD_FETCH_TIMEOUT_MS = 12e4;
|
|
3073
|
+
/**
|
|
3074
|
+
* Traditional (compile-or-prebuild) natives an installed closure must carry a
|
|
3075
|
+
* `.node` for. Only these are ENSURE-able by a prebuild re-fetch; N-API natives
|
|
3076
|
+
* are excluded (see file header).
|
|
3077
|
+
*/
|
|
3078
|
+
var TRADITIONAL_NATIVE_PACKAGES = ["better-sqlite3"];
|
|
3079
|
+
/** Bounded recursive scan for ANY `*.node` file under `dir`. */
|
|
3080
|
+
function hasDotNode(dir, maxDepth) {
|
|
3081
|
+
let entries;
|
|
3082
|
+
try {
|
|
3083
|
+
entries = node_fs.readdirSync(dir, { withFileTypes: true });
|
|
3084
|
+
} catch {
|
|
3085
|
+
return false;
|
|
3086
|
+
}
|
|
3087
|
+
for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".node")) return true;
|
|
3088
|
+
if (maxDepth <= 0) return false;
|
|
3089
|
+
for (const entry of entries) if (entry.isDirectory() && hasDotNode(node_path.join(dir, entry.name), maxDepth - 1)) return true;
|
|
3090
|
+
return false;
|
|
3091
|
+
}
|
|
3092
|
+
/** Locate the bundled `prebuild-install` bin.js inside the staged closure. */
|
|
3093
|
+
function resolvePrebuildInstallBin(closureDir, moduleDir) {
|
|
3094
|
+
return [node_path.join(moduleDir, "node_modules", "prebuild-install", "bin.js"), node_path.join(closureDir, "node_modules", "prebuild-install", "bin.js")].find((c) => node_fs.existsSync(c)) ?? null;
|
|
3095
|
+
}
|
|
3096
|
+
/**
|
|
3097
|
+
* Default fetcher: run the closure's own `prebuild-install` under the CURRENT
|
|
3098
|
+
* Node (`process.execPath`) — no reliance on `npx`/`npm` on PATH (a packaged
|
|
3099
|
+
* app ships a bare Node). Explicitly targets the running ABI/arch, mirroring
|
|
3100
|
+
* `scripts/stage-deps.mjs`.
|
|
3101
|
+
*/
|
|
3102
|
+
function makeDefaultFetch(closureDir, logger) {
|
|
3103
|
+
return async (moduleDir, target) => {
|
|
3104
|
+
const bin = resolvePrebuildInstallBin(closureDir, moduleDir);
|
|
3105
|
+
if (bin === null) throw new Error(`prebuild-install not found in the staged closure for ${moduleDir}`);
|
|
3106
|
+
logger.info("fetching native prebuild via prebuild-install", { meta: {
|
|
3107
|
+
moduleDir,
|
|
3108
|
+
bin,
|
|
3109
|
+
nodeVersion: target.nodeVersion,
|
|
3110
|
+
arch: target.arch
|
|
3111
|
+
} });
|
|
3112
|
+
await execFileAsync$1(process.execPath, [
|
|
3113
|
+
bin,
|
|
3114
|
+
"--runtime",
|
|
3115
|
+
"node",
|
|
3116
|
+
"--target",
|
|
3117
|
+
target.nodeVersion,
|
|
3118
|
+
"--arch",
|
|
3119
|
+
target.arch
|
|
3120
|
+
], {
|
|
3121
|
+
cwd: moduleDir,
|
|
3122
|
+
timeout: PREBUILD_FETCH_TIMEOUT_MS
|
|
3123
|
+
});
|
|
3124
|
+
};
|
|
3125
|
+
}
|
|
3126
|
+
/**
|
|
3127
|
+
* Ensure every traditional native in `<closureDir>/node_modules` carries its
|
|
3128
|
+
* compiled `.node`, fetching the prebuild for the running ABI when missing.
|
|
3129
|
+
*
|
|
3130
|
+
* A NO-OP when nothing is missing (docker / tests) and when a native package is
|
|
3131
|
+
* absent from the closure. Never throws for a single package's fetch failure —
|
|
3132
|
+
* it logs and continues so the downstream `findMissingNativePrebuilds`
|
|
3133
|
+
* validator makes the final swap/abort decision. Returns the packages whose
|
|
3134
|
+
* prebuild was successfully fetched (for logging/tests).
|
|
3135
|
+
*/
|
|
3136
|
+
async function ensureNativePrebuilds(closureDir, options) {
|
|
3137
|
+
const nm = node_path.join(closureDir, "node_modules");
|
|
3138
|
+
const target = options.target ?? {
|
|
3139
|
+
nodeVersion: process.versions.node,
|
|
3140
|
+
arch: process.arch
|
|
3141
|
+
};
|
|
3142
|
+
const packages = options.packages ?? TRADITIONAL_NATIVE_PACKAGES;
|
|
3143
|
+
const fetchPrebuild = options.fetchPrebuild ?? makeDefaultFetch(closureDir, options.logger);
|
|
3144
|
+
const fetched = [];
|
|
3145
|
+
for (const pkg of packages) {
|
|
3146
|
+
const moduleDir = node_path.join(nm, pkg);
|
|
3147
|
+
if (!node_fs.existsSync(moduleDir)) continue;
|
|
3148
|
+
if (hasDotNode(moduleDir, 4)) continue;
|
|
3149
|
+
options.logger.warn("staged closure native prebuild missing — fetching for running ABI", { meta: {
|
|
3150
|
+
pkg,
|
|
3151
|
+
moduleDir,
|
|
3152
|
+
nodeVersion: target.nodeVersion,
|
|
3153
|
+
arch: target.arch
|
|
3154
|
+
} });
|
|
3155
|
+
try {
|
|
3156
|
+
await fetchPrebuild(moduleDir, target);
|
|
3157
|
+
} catch (err) {
|
|
3158
|
+
options.logger.error("native prebuild fetch failed (validator will decide)", { meta: {
|
|
3159
|
+
pkg,
|
|
3160
|
+
error: (0, _camstack_types_addon.errMsg)(err)
|
|
3161
|
+
} });
|
|
3162
|
+
continue;
|
|
3163
|
+
}
|
|
3164
|
+
if (hasDotNode(moduleDir, 4)) {
|
|
3165
|
+
fetched.push(pkg);
|
|
3166
|
+
options.logger.info("native prebuild fetched", { meta: { pkg } });
|
|
3167
|
+
} else options.logger.error("native prebuild still missing after fetch", { meta: { pkg } });
|
|
3168
|
+
}
|
|
3169
|
+
return fetched;
|
|
3170
|
+
}
|
|
3171
|
+
//#endregion
|
|
3172
|
+
//#region src/kernel/deps/ensure-addon-natives.ts
|
|
3173
|
+
/**
|
|
3174
|
+
* `ensureAddonNativePrebuilds` — the POST-install native-prebuild ENSURE pass an
|
|
3175
|
+
* addon install runs on the INSTALLED addon dir AFTER `npm install`.
|
|
3176
|
+
*
|
|
3177
|
+
* Why this exists (Mac desktop first-boot FATAL, 2026-07-21): `installCopy`
|
|
3178
|
+
* copies `dist/`, plants prebuilt natives from the bundled addon copy
|
|
3179
|
+
* (`copyBundledNativeModules`), then `npm install`s the addon's runtime deps.
|
|
3180
|
+
* On a TOOLCHAIN-LESS host that `npm install` RE-CREATES the traditional native
|
|
3181
|
+
* `better-sqlite3` SOURCE-ONLY — clobbering the good `.node` the pre-install
|
|
3182
|
+
* copy just planted — and the runner later dies at addon init with a cryptic
|
|
3183
|
+
* "Could not locate the bindings file". The copy-BEFORE-install fix was
|
|
3184
|
+
* empirically insufficient: npm's own install in the copied dir replaces the
|
|
3185
|
+
* satisfied dep without its binary.
|
|
3186
|
+
*
|
|
3187
|
+
* This pass runs LAST, so it always sees the final on-disk state npm left, and
|
|
3188
|
+
* per referenced traditional native still lacking its compiled `.node`:
|
|
3189
|
+
* 1. bundled-copy first — if the bundled addon copy carries the matching
|
|
3190
|
+
* `.node` (same machine → same platform/arch/ABI), re-plant it DIRECTLY.
|
|
3191
|
+
* Cheapest + offline-safe; runs BEFORE any network fetch.
|
|
3192
|
+
* 2. fetch fallback — otherwise fetch the prebuild for the running ABI via the
|
|
3193
|
+
* shared `ensureNativePrebuilds` pointed at the addon dir.
|
|
3194
|
+
* 3. FAIL LOUD — if the native is STILL source-only after both, log a clear
|
|
3195
|
+
* error naming module + addon and THROW so the install fails at install
|
|
3196
|
+
* time rather than at addon init.
|
|
3197
|
+
*
|
|
3198
|
+
* N-API natives (`sharp`, `node-av`) ship their binaries as ABI-stable platform
|
|
3199
|
+
* SIBLING packages npm fetches without a compile step, so they are excluded from
|
|
3200
|
+
* the default set — a miss there is a broken install, not something a prebuild
|
|
3201
|
+
* re-fetch heals (same rationale as `ensureNativePrebuilds`).
|
|
3202
|
+
*/
|
|
3203
|
+
/**
|
|
3204
|
+
* Re-plant `<srcModuleDir>` → `<dstModuleDir>` when the source carries a `.node`.
|
|
3205
|
+
* Returns whether the destination ends up with a compiled binary.
|
|
3206
|
+
*/
|
|
3207
|
+
function replantFromBundled(srcModuleDir, dstModuleDir, hasNative) {
|
|
3208
|
+
if (!hasNative(srcModuleDir)) return false;
|
|
3209
|
+
try {
|
|
3210
|
+
node_fs.rmSync(dstModuleDir, {
|
|
3211
|
+
recursive: true,
|
|
3212
|
+
force: true
|
|
3213
|
+
});
|
|
3214
|
+
node_fs.mkdirSync(node_path.dirname(dstModuleDir), { recursive: true });
|
|
3215
|
+
node_fs.cpSync(srcModuleDir, dstModuleDir, {
|
|
3216
|
+
recursive: true,
|
|
3217
|
+
dereference: true
|
|
3218
|
+
});
|
|
3219
|
+
} catch {
|
|
3220
|
+
return false;
|
|
3221
|
+
}
|
|
3222
|
+
return hasNative(dstModuleDir);
|
|
3223
|
+
}
|
|
3224
|
+
/**
|
|
3225
|
+
* Ensure every referenced traditional native in `<addonDir>/node_modules`
|
|
3226
|
+
* carries its compiled `.node` after the addon's `npm install`. Bundled-copy
|
|
3227
|
+
* first (offline), prebuild-fetch fallback, LOUD throw on a residual miss.
|
|
3228
|
+
*
|
|
3229
|
+
* SKIPS a native absent from the addon's own `node_modules` (it resolves via a
|
|
3230
|
+
* hoisted host copy — not this addon's concern). A NO-OP in docker / dev where
|
|
3231
|
+
* npm produced a working `.node` (already-present). Returns the per-native
|
|
3232
|
+
* outcomes for logging/tests.
|
|
3233
|
+
*
|
|
3234
|
+
* @throws if any required native is still source-only after copy + fetch.
|
|
3235
|
+
*/
|
|
3236
|
+
async function ensureAddonNativePrebuilds(addonDir, options) {
|
|
3237
|
+
const packages = options.packages ?? TRADITIONAL_NATIVE_PACKAGES;
|
|
3238
|
+
const hasNative = options.hasNative ?? ((dir) => hasDotNode(dir, 4));
|
|
3239
|
+
const nm = node_path.join(addonDir, "node_modules");
|
|
3240
|
+
const results = [];
|
|
3241
|
+
const missing = [];
|
|
3242
|
+
for (const pkg of packages) {
|
|
3243
|
+
const moduleDir = node_path.join(nm, pkg);
|
|
3244
|
+
if (!node_fs.existsSync(moduleDir)) continue;
|
|
3245
|
+
if (hasNative(moduleDir)) {
|
|
3246
|
+
results.push({
|
|
3247
|
+
pkg,
|
|
3248
|
+
outcome: "already-present"
|
|
3249
|
+
});
|
|
3250
|
+
continue;
|
|
3251
|
+
}
|
|
3252
|
+
if (options.bundledSourceDir) {
|
|
3253
|
+
const src = node_path.join(options.bundledSourceDir, "node_modules", pkg);
|
|
3254
|
+
if (replantFromBundled(src, moduleDir, hasNative)) {
|
|
3255
|
+
options.logger.info("addon native prebuild re-planted from bundled copy (offline)", { meta: {
|
|
3256
|
+
addon: options.addonName,
|
|
3257
|
+
pkg,
|
|
3258
|
+
src,
|
|
3259
|
+
moduleDir
|
|
3260
|
+
} });
|
|
3261
|
+
results.push({
|
|
3262
|
+
pkg,
|
|
3263
|
+
outcome: "copied"
|
|
3264
|
+
});
|
|
3265
|
+
continue;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
if ((await ensureNativePrebuilds(addonDir, {
|
|
3269
|
+
logger: options.logger,
|
|
3270
|
+
packages: [pkg],
|
|
3271
|
+
...options.target ? { target: options.target } : {},
|
|
3272
|
+
...options.fetchPrebuild ? { fetchPrebuild: options.fetchPrebuild } : {}
|
|
3273
|
+
})).includes(pkg)) {
|
|
3274
|
+
results.push({
|
|
3275
|
+
pkg,
|
|
3276
|
+
outcome: "fetched"
|
|
3277
|
+
});
|
|
3278
|
+
continue;
|
|
3279
|
+
}
|
|
3280
|
+
options.logger.error("addon native prebuild MISSING after ensure — module has no compiled .node", { meta: {
|
|
3281
|
+
addon: options.addonName,
|
|
3282
|
+
pkg,
|
|
3283
|
+
moduleDir
|
|
3284
|
+
} });
|
|
3285
|
+
results.push({
|
|
3286
|
+
pkg,
|
|
3287
|
+
outcome: "missing"
|
|
3288
|
+
});
|
|
3289
|
+
missing.push(pkg);
|
|
3290
|
+
}
|
|
3291
|
+
if (missing.length > 0) throw new Error(`${options.addonName} — required native module(s) missing a compiled .node after install (bundled-copy + prebuild-fetch both failed): ${missing.join(", ")}`);
|
|
3292
|
+
return results;
|
|
3293
|
+
}
|
|
3294
|
+
//#endregion
|
|
3295
|
+
//#region src/kernel/addon-installer.ts
|
|
3296
|
+
var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
|
|
3035
3297
|
function parseInstallSource(value) {
|
|
3036
3298
|
switch (value) {
|
|
3037
3299
|
case "npm":
|
|
@@ -3340,6 +3602,11 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3340
3602
|
} catch (err) {
|
|
3341
3603
|
this.logger.warn(`${packageName} — native deps install failed (continuing)`, { meta: { error: require_dist.errMsg(err) } });
|
|
3342
3604
|
}
|
|
3605
|
+
await ensureAddonNativePrebuilds(targetDir, {
|
|
3606
|
+
addonName: packageName,
|
|
3607
|
+
logger: this.logger,
|
|
3608
|
+
bundledSourceDir: sourceDir
|
|
3609
|
+
});
|
|
3343
3610
|
this.logger.info(`${packageName} — copied from ${sourceDir}`);
|
|
3344
3611
|
return {
|
|
3345
3612
|
name: pkgData.name,
|
|
@@ -3489,7 +3756,7 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3489
3756
|
const stagingDir = node_path.join(stagingRoot, `install-${process.pid}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
|
|
3490
3757
|
ensureDir(stagingDir);
|
|
3491
3758
|
try {
|
|
3492
|
-
await execFileAsync
|
|
3759
|
+
await execFileAsync("tar", [
|
|
3493
3760
|
"-xzf",
|
|
3494
3761
|
tgzPath,
|
|
3495
3762
|
"-C",
|
|
@@ -3526,6 +3793,10 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3526
3793
|
} catch (nativeErr) {
|
|
3527
3794
|
throw new Error(`${pkgView.name} — native deps install failed: ${require_dist.errMsg(nativeErr)}`, { cause: nativeErr });
|
|
3528
3795
|
}
|
|
3796
|
+
await ensureAddonNativePrebuilds(pkgDir, {
|
|
3797
|
+
addonName: pkgView.name,
|
|
3798
|
+
logger: this.logger
|
|
3799
|
+
});
|
|
3529
3800
|
await this.evictInstallDir(targetDir);
|
|
3530
3801
|
ensureDir(node_path.dirname(targetDir));
|
|
3531
3802
|
await node_fs.promises.rename(pkgDir, targetDir);
|
|
@@ -80144,7 +80415,7 @@ var require_compression = /* @__PURE__ */ require_chunk.__commonJSMin(((exports,
|
|
|
80144
80415
|
var { defaultsDeep } = require_lodash();
|
|
80145
80416
|
var { parseByteString } = require_utils$3();
|
|
80146
80417
|
var zlib = require("zlib");
|
|
80147
|
-
var { promisify
|
|
80418
|
+
var { promisify } = require("util");
|
|
80148
80419
|
/**
|
|
80149
80420
|
* This is a transmission compression middleware. It supports
|
|
80150
80421
|
* the `deflate`, `deflateRaw` & `gzip` compression methods.
|
|
@@ -80162,16 +80433,16 @@ var require_compression = /* @__PURE__ */ require_chunk.__commonJSMin(((exports,
|
|
|
80162
80433
|
const threshold = parseByteString(opts.threshold);
|
|
80163
80434
|
switch (opts.method) {
|
|
80164
80435
|
case "deflate":
|
|
80165
|
-
compress = promisify
|
|
80166
|
-
decompress = promisify
|
|
80436
|
+
compress = promisify(zlib.deflate);
|
|
80437
|
+
decompress = promisify(zlib.inflate);
|
|
80167
80438
|
break;
|
|
80168
80439
|
case "deflateRaw":
|
|
80169
|
-
compress = promisify
|
|
80170
|
-
decompress = promisify
|
|
80440
|
+
compress = promisify(zlib.deflateRaw);
|
|
80441
|
+
decompress = promisify(zlib.inflateRaw);
|
|
80171
80442
|
break;
|
|
80172
80443
|
case "gzip":
|
|
80173
|
-
compress = promisify
|
|
80174
|
-
decompress = promisify
|
|
80444
|
+
compress = promisify(zlib.gzip);
|
|
80445
|
+
decompress = promisify(zlib.gunzip);
|
|
80175
80446
|
break;
|
|
80176
80447
|
default:
|
|
80177
80448
|
/* istanbul ignore next */
|
|
@@ -93398,145 +93669,6 @@ var LifecycleJobEngine = class {
|
|
|
93398
93669
|
}
|
|
93399
93670
|
};
|
|
93400
93671
|
//#endregion
|
|
93401
|
-
//#region src/kernel/deps/ensure-native-prebuilds.ts
|
|
93402
|
-
/**
|
|
93403
|
-
* `ensureNativePrebuilds` — the post-install native-prebuild ENSURE step the
|
|
93404
|
-
* shared `RootUpdateService` runs while staging a `@camstack/server` closure,
|
|
93405
|
-
* BEFORE its `findMissingNativePrebuilds` validator (in `@camstack/node-root`)
|
|
93406
|
-
* decides whether to arm the swap.
|
|
93407
|
-
*
|
|
93408
|
-
* Why this exists (Mac desktop incident, 2026-07-21): a runtime
|
|
93409
|
-
* `applyServerUpdate` on a packaged desktop stages the closure with
|
|
93410
|
-
* `npm install @camstack/server@X` under the app's BUNDLED bare Node. The one
|
|
93411
|
-
* TRADITIONAL (non-N-API) native, `better-sqlite3`, resolves its `.node` via
|
|
93412
|
-
* its `install` script `prebuild-install || node-gyp rebuild`. On a user's Mac
|
|
93413
|
-
* with no build toolchain (and where the auto `prebuild-install` does not
|
|
93414
|
-
* target the bundled Node's ABI/arch the way the DMG build's EXPLICIT
|
|
93415
|
-
* `prebuild-install --runtime node --target <bundled> --arch <arch>` call does)
|
|
93416
|
-
* that leaves the module SOURCE-ONLY → the closure has no `better-sqlite3`
|
|
93417
|
-
* `.node` → the validator refuses the swap ("staged closure missing native
|
|
93418
|
-
* prebuilds: better-sqlite3"). The docker image never hits this: its Node ABI
|
|
93419
|
-
* has a published prebuild the auto path fetches cleanly, and it carries a
|
|
93420
|
-
* build toolchain fallback.
|
|
93421
|
-
*
|
|
93422
|
-
* This mirrors the DMG build's explicit re-fetch (`scripts/stage-deps.mjs`)
|
|
93423
|
-
* into the runtime staging path: for each traditional native that is present
|
|
93424
|
-
* in the closure but missing its compiled `.node`, fetch the prebuild for the
|
|
93425
|
-
* RUNNING Node ABI/arch (the same Node that will load the swapped closure).
|
|
93426
|
-
*
|
|
93427
|
-
* N-API natives (`sharp`, `node-av`) are ABI-stable and ship their binaries as
|
|
93428
|
-
* platform SIBLING packages that npm fetches reliably with no compile step, so
|
|
93429
|
-
* they are not in scope here — a genuine miss there is a broken install the
|
|
93430
|
-
* validator should still reject, not something a prebuild re-fetch can heal.
|
|
93431
|
-
*
|
|
93432
|
-
* Injected into the update service from the construction site so the zero-dep
|
|
93433
|
-
* `@camstack/node-root` layer never depends on npm/@camstack/system, and the
|
|
93434
|
-
* fetch is unit-testable with a fake fetcher.
|
|
93435
|
-
*/
|
|
93436
|
-
var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
|
|
93437
|
-
/** Bounded recursive `.node` scan depth inside a module dir. */
|
|
93438
|
-
var NATIVE_SCAN_DEPTH = 4;
|
|
93439
|
-
/** Timeout for a single `prebuild-install` fetch. */
|
|
93440
|
-
var PREBUILD_FETCH_TIMEOUT_MS = 12e4;
|
|
93441
|
-
/**
|
|
93442
|
-
* Traditional (compile-or-prebuild) natives an installed closure must carry a
|
|
93443
|
-
* `.node` for. Only these are ENSURE-able by a prebuild re-fetch; N-API natives
|
|
93444
|
-
* are excluded (see file header).
|
|
93445
|
-
*/
|
|
93446
|
-
var TRADITIONAL_NATIVE_PACKAGES = ["better-sqlite3"];
|
|
93447
|
-
/** Bounded recursive scan for ANY `*.node` file under `dir`. */
|
|
93448
|
-
function hasDotNode(dir, maxDepth) {
|
|
93449
|
-
let entries;
|
|
93450
|
-
try {
|
|
93451
|
-
entries = node_fs.readdirSync(dir, { withFileTypes: true });
|
|
93452
|
-
} catch {
|
|
93453
|
-
return false;
|
|
93454
|
-
}
|
|
93455
|
-
for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".node")) return true;
|
|
93456
|
-
if (maxDepth <= 0) return false;
|
|
93457
|
-
for (const entry of entries) if (entry.isDirectory() && hasDotNode(node_path.join(dir, entry.name), maxDepth - 1)) return true;
|
|
93458
|
-
return false;
|
|
93459
|
-
}
|
|
93460
|
-
/** Locate the bundled `prebuild-install` bin.js inside the staged closure. */
|
|
93461
|
-
function resolvePrebuildInstallBin(closureDir, moduleDir) {
|
|
93462
|
-
return [node_path.join(moduleDir, "node_modules", "prebuild-install", "bin.js"), node_path.join(closureDir, "node_modules", "prebuild-install", "bin.js")].find((c) => node_fs.existsSync(c)) ?? null;
|
|
93463
|
-
}
|
|
93464
|
-
/**
|
|
93465
|
-
* Default fetcher: run the closure's own `prebuild-install` under the CURRENT
|
|
93466
|
-
* Node (`process.execPath`) — no reliance on `npx`/`npm` on PATH (a packaged
|
|
93467
|
-
* app ships a bare Node). Explicitly targets the running ABI/arch, mirroring
|
|
93468
|
-
* `scripts/stage-deps.mjs`.
|
|
93469
|
-
*/
|
|
93470
|
-
function makeDefaultFetch(closureDir, logger) {
|
|
93471
|
-
return async (moduleDir, target) => {
|
|
93472
|
-
const bin = resolvePrebuildInstallBin(closureDir, moduleDir);
|
|
93473
|
-
if (bin === null) throw new Error(`prebuild-install not found in the staged closure for ${moduleDir}`);
|
|
93474
|
-
logger.info("fetching native prebuild via prebuild-install", { meta: {
|
|
93475
|
-
moduleDir,
|
|
93476
|
-
bin,
|
|
93477
|
-
nodeVersion: target.nodeVersion,
|
|
93478
|
-
arch: target.arch
|
|
93479
|
-
} });
|
|
93480
|
-
await execFileAsync(process.execPath, [
|
|
93481
|
-
bin,
|
|
93482
|
-
"--runtime",
|
|
93483
|
-
"node",
|
|
93484
|
-
"--target",
|
|
93485
|
-
target.nodeVersion,
|
|
93486
|
-
"--arch",
|
|
93487
|
-
target.arch
|
|
93488
|
-
], {
|
|
93489
|
-
cwd: moduleDir,
|
|
93490
|
-
timeout: PREBUILD_FETCH_TIMEOUT_MS
|
|
93491
|
-
});
|
|
93492
|
-
};
|
|
93493
|
-
}
|
|
93494
|
-
/**
|
|
93495
|
-
* Ensure every traditional native in `<closureDir>/node_modules` carries its
|
|
93496
|
-
* compiled `.node`, fetching the prebuild for the running ABI when missing.
|
|
93497
|
-
*
|
|
93498
|
-
* A NO-OP when nothing is missing (docker / tests) and when a native package is
|
|
93499
|
-
* absent from the closure. Never throws for a single package's fetch failure —
|
|
93500
|
-
* it logs and continues so the downstream `findMissingNativePrebuilds`
|
|
93501
|
-
* validator makes the final swap/abort decision. Returns the packages whose
|
|
93502
|
-
* prebuild was successfully fetched (for logging/tests).
|
|
93503
|
-
*/
|
|
93504
|
-
async function ensureNativePrebuilds(closureDir, options) {
|
|
93505
|
-
const nm = node_path.join(closureDir, "node_modules");
|
|
93506
|
-
const target = options.target ?? {
|
|
93507
|
-
nodeVersion: process.versions.node,
|
|
93508
|
-
arch: process.arch
|
|
93509
|
-
};
|
|
93510
|
-
const packages = options.packages ?? TRADITIONAL_NATIVE_PACKAGES;
|
|
93511
|
-
const fetchPrebuild = options.fetchPrebuild ?? makeDefaultFetch(closureDir, options.logger);
|
|
93512
|
-
const fetched = [];
|
|
93513
|
-
for (const pkg of packages) {
|
|
93514
|
-
const moduleDir = node_path.join(nm, pkg);
|
|
93515
|
-
if (!node_fs.existsSync(moduleDir)) continue;
|
|
93516
|
-
if (hasDotNode(moduleDir, NATIVE_SCAN_DEPTH)) continue;
|
|
93517
|
-
options.logger.warn("staged closure native prebuild missing — fetching for running ABI", { meta: {
|
|
93518
|
-
pkg,
|
|
93519
|
-
moduleDir,
|
|
93520
|
-
nodeVersion: target.nodeVersion,
|
|
93521
|
-
arch: target.arch
|
|
93522
|
-
} });
|
|
93523
|
-
try {
|
|
93524
|
-
await fetchPrebuild(moduleDir, target);
|
|
93525
|
-
} catch (err) {
|
|
93526
|
-
options.logger.error("native prebuild fetch failed (validator will decide)", { meta: {
|
|
93527
|
-
pkg,
|
|
93528
|
-
error: (0, _camstack_types_addon.errMsg)(err)
|
|
93529
|
-
} });
|
|
93530
|
-
continue;
|
|
93531
|
-
}
|
|
93532
|
-
if (hasDotNode(moduleDir, NATIVE_SCAN_DEPTH)) {
|
|
93533
|
-
fetched.push(pkg);
|
|
93534
|
-
options.logger.info("native prebuild fetched", { meta: { pkg } });
|
|
93535
|
-
} else options.logger.error("native prebuild still missing after fetch", { meta: { pkg } });
|
|
93536
|
-
}
|
|
93537
|
-
return fetched;
|
|
93538
|
-
}
|
|
93539
|
-
//#endregion
|
|
93540
93672
|
exports.AGENT_CAP_FWD_ACTION = require_manifest_python_deps.AGENT_CAP_FWD_ACTION;
|
|
93541
93673
|
exports.AGENT_CAP_FWD_SERVICE = require_manifest_python_deps.AGENT_CAP_FWD_SERVICE;
|
|
93542
93674
|
exports.AGENT_READINESS_SERVICE_NAME = AGENT_READINESS_SERVICE_NAME;
|
|
@@ -93603,6 +93735,7 @@ exports.LogRingBuffer = LogRingBuffer;
|
|
|
93603
93735
|
exports.METHOD_ACCESS_MAP = require_dist.METHOD_ACCESS_MAP;
|
|
93604
93736
|
exports.ModelDownloadService = require_model_download_service.ModelDownloadService;
|
|
93605
93737
|
exports.NATIVE_PROVIDER_SERVICE_INFIX = require_manifest_python_deps.NATIVE_PROVIDER_SERVICE_INFIX;
|
|
93738
|
+
exports.NATIVE_SCAN_DEPTH = NATIVE_SCAN_DEPTH;
|
|
93606
93739
|
exports.NativeMetricsAddon = require_builtins_native_metrics_native_metrics_addon.default;
|
|
93607
93740
|
exports.NativeMetricsProvider = require_builtins_native_metrics_native_metrics_addon.NativeMetricsProvider;
|
|
93608
93741
|
exports.NetworkQualityTracker = NetworkQualityTracker;
|
|
@@ -93712,6 +93845,7 @@ exports.downloadFile = require_model_download_service.downloadFile;
|
|
|
93712
93845
|
exports.downloadModel = require_model_download_service.downloadModel;
|
|
93713
93846
|
exports.emitDownForOwnedCaps = require_dist.emitDownForOwnedCaps;
|
|
93714
93847
|
exports.encodeFrame = require_manifest_python_deps.encodeFrame;
|
|
93848
|
+
exports.ensureAddonNativePrebuilds = ensureAddonNativePrebuilds;
|
|
93715
93849
|
Object.defineProperty(exports, "ensureBinary", {
|
|
93716
93850
|
enumerable: true,
|
|
93717
93851
|
get: function() {
|
|
@@ -93772,6 +93906,7 @@ Object.defineProperty(exports, "getPythonDownloadUrl", {
|
|
|
93772
93906
|
exports.getRestartMarkerPath = getRestartMarkerPath;
|
|
93773
93907
|
exports.getSinglePidStats = require_resource_monitor.getSinglePidStats;
|
|
93774
93908
|
exports.getWorkerDeviceRegistry = require_manifest_python_deps.getWorkerDeviceRegistry;
|
|
93909
|
+
exports.hasDotNode = hasDotNode;
|
|
93775
93910
|
exports.hashClusterSecret = hashClusterSecret;
|
|
93776
93911
|
exports.installManifestNativeDeps = require_manifest_python_deps.installManifestNativeDeps;
|
|
93777
93912
|
exports.installManifestPythonDeps = require_manifest_python_deps.installManifestPythonDeps;
|
package/dist/index.mjs
CHANGED
|
@@ -3022,8 +3022,270 @@ var AddonManifest = class {
|
|
|
3022
3022
|
}
|
|
3023
3023
|
};
|
|
3024
3024
|
//#endregion
|
|
3025
|
-
//#region src/kernel/
|
|
3025
|
+
//#region src/kernel/deps/ensure-native-prebuilds.ts
|
|
3026
|
+
/**
|
|
3027
|
+
* `ensureNativePrebuilds` — the post-install native-prebuild ENSURE step the
|
|
3028
|
+
* shared `RootUpdateService` runs while staging a `@camstack/server` closure,
|
|
3029
|
+
* BEFORE its `findMissingNativePrebuilds` validator (in `@camstack/node-root`)
|
|
3030
|
+
* decides whether to arm the swap.
|
|
3031
|
+
*
|
|
3032
|
+
* Why this exists (Mac desktop incident, 2026-07-21): a runtime
|
|
3033
|
+
* `applyServerUpdate` on a packaged desktop stages the closure with
|
|
3034
|
+
* `npm install @camstack/server@X` under the app's BUNDLED bare Node. The one
|
|
3035
|
+
* TRADITIONAL (non-N-API) native, `better-sqlite3`, resolves its `.node` via
|
|
3036
|
+
* its `install` script `prebuild-install || node-gyp rebuild`. On a user's Mac
|
|
3037
|
+
* with no build toolchain (and where the auto `prebuild-install` does not
|
|
3038
|
+
* target the bundled Node's ABI/arch the way the DMG build's EXPLICIT
|
|
3039
|
+
* `prebuild-install --runtime node --target <bundled> --arch <arch>` call does)
|
|
3040
|
+
* that leaves the module SOURCE-ONLY → the closure has no `better-sqlite3`
|
|
3041
|
+
* `.node` → the validator refuses the swap ("staged closure missing native
|
|
3042
|
+
* prebuilds: better-sqlite3"). The docker image never hits this: its Node ABI
|
|
3043
|
+
* has a published prebuild the auto path fetches cleanly, and it carries a
|
|
3044
|
+
* build toolchain fallback.
|
|
3045
|
+
*
|
|
3046
|
+
* This mirrors the DMG build's explicit re-fetch (`scripts/stage-deps.mjs`)
|
|
3047
|
+
* into the runtime staging path: for each traditional native that is present
|
|
3048
|
+
* in the closure but missing its compiled `.node`, fetch the prebuild for the
|
|
3049
|
+
* RUNNING Node ABI/arch (the same Node that will load the swapped closure).
|
|
3050
|
+
*
|
|
3051
|
+
* N-API natives (`sharp`, `node-av`) are ABI-stable and ship their binaries as
|
|
3052
|
+
* platform SIBLING packages that npm fetches reliably with no compile step, so
|
|
3053
|
+
* they are not in scope here — a genuine miss there is a broken install the
|
|
3054
|
+
* validator should still reject, not something a prebuild re-fetch can heal.
|
|
3055
|
+
*
|
|
3056
|
+
* Injected into the update service from the construction site so the zero-dep
|
|
3057
|
+
* `@camstack/node-root` layer never depends on npm/@camstack/system, and the
|
|
3058
|
+
* fetch is unit-testable with a fake fetcher.
|
|
3059
|
+
*/
|
|
3026
3060
|
var execFileAsync$1 = promisify(execFile);
|
|
3061
|
+
/** Bounded recursive `.node` scan depth inside a module dir. */
|
|
3062
|
+
var NATIVE_SCAN_DEPTH = 4;
|
|
3063
|
+
/** Timeout for a single `prebuild-install` fetch. */
|
|
3064
|
+
var PREBUILD_FETCH_TIMEOUT_MS = 12e4;
|
|
3065
|
+
/**
|
|
3066
|
+
* Traditional (compile-or-prebuild) natives an installed closure must carry a
|
|
3067
|
+
* `.node` for. Only these are ENSURE-able by a prebuild re-fetch; N-API natives
|
|
3068
|
+
* are excluded (see file header).
|
|
3069
|
+
*/
|
|
3070
|
+
var TRADITIONAL_NATIVE_PACKAGES = ["better-sqlite3"];
|
|
3071
|
+
/** Bounded recursive scan for ANY `*.node` file under `dir`. */
|
|
3072
|
+
function hasDotNode(dir, maxDepth) {
|
|
3073
|
+
let entries;
|
|
3074
|
+
try {
|
|
3075
|
+
entries = fs$17.readdirSync(dir, { withFileTypes: true });
|
|
3076
|
+
} catch {
|
|
3077
|
+
return false;
|
|
3078
|
+
}
|
|
3079
|
+
for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".node")) return true;
|
|
3080
|
+
if (maxDepth <= 0) return false;
|
|
3081
|
+
for (const entry of entries) if (entry.isDirectory() && hasDotNode(path$39.join(dir, entry.name), maxDepth - 1)) return true;
|
|
3082
|
+
return false;
|
|
3083
|
+
}
|
|
3084
|
+
/** Locate the bundled `prebuild-install` bin.js inside the staged closure. */
|
|
3085
|
+
function resolvePrebuildInstallBin(closureDir, moduleDir) {
|
|
3086
|
+
return [path$39.join(moduleDir, "node_modules", "prebuild-install", "bin.js"), path$39.join(closureDir, "node_modules", "prebuild-install", "bin.js")].find((c) => fs$17.existsSync(c)) ?? null;
|
|
3087
|
+
}
|
|
3088
|
+
/**
|
|
3089
|
+
* Default fetcher: run the closure's own `prebuild-install` under the CURRENT
|
|
3090
|
+
* Node (`process.execPath`) — no reliance on `npx`/`npm` on PATH (a packaged
|
|
3091
|
+
* app ships a bare Node). Explicitly targets the running ABI/arch, mirroring
|
|
3092
|
+
* `scripts/stage-deps.mjs`.
|
|
3093
|
+
*/
|
|
3094
|
+
function makeDefaultFetch(closureDir, logger) {
|
|
3095
|
+
return async (moduleDir, target) => {
|
|
3096
|
+
const bin = resolvePrebuildInstallBin(closureDir, moduleDir);
|
|
3097
|
+
if (bin === null) throw new Error(`prebuild-install not found in the staged closure for ${moduleDir}`);
|
|
3098
|
+
logger.info("fetching native prebuild via prebuild-install", { meta: {
|
|
3099
|
+
moduleDir,
|
|
3100
|
+
bin,
|
|
3101
|
+
nodeVersion: target.nodeVersion,
|
|
3102
|
+
arch: target.arch
|
|
3103
|
+
} });
|
|
3104
|
+
await execFileAsync$1(process.execPath, [
|
|
3105
|
+
bin,
|
|
3106
|
+
"--runtime",
|
|
3107
|
+
"node",
|
|
3108
|
+
"--target",
|
|
3109
|
+
target.nodeVersion,
|
|
3110
|
+
"--arch",
|
|
3111
|
+
target.arch
|
|
3112
|
+
], {
|
|
3113
|
+
cwd: moduleDir,
|
|
3114
|
+
timeout: PREBUILD_FETCH_TIMEOUT_MS
|
|
3115
|
+
});
|
|
3116
|
+
};
|
|
3117
|
+
}
|
|
3118
|
+
/**
|
|
3119
|
+
* Ensure every traditional native in `<closureDir>/node_modules` carries its
|
|
3120
|
+
* compiled `.node`, fetching the prebuild for the running ABI when missing.
|
|
3121
|
+
*
|
|
3122
|
+
* A NO-OP when nothing is missing (docker / tests) and when a native package is
|
|
3123
|
+
* absent from the closure. Never throws for a single package's fetch failure —
|
|
3124
|
+
* it logs and continues so the downstream `findMissingNativePrebuilds`
|
|
3125
|
+
* validator makes the final swap/abort decision. Returns the packages whose
|
|
3126
|
+
* prebuild was successfully fetched (for logging/tests).
|
|
3127
|
+
*/
|
|
3128
|
+
async function ensureNativePrebuilds(closureDir, options) {
|
|
3129
|
+
const nm = path$39.join(closureDir, "node_modules");
|
|
3130
|
+
const target = options.target ?? {
|
|
3131
|
+
nodeVersion: process.versions.node,
|
|
3132
|
+
arch: process.arch
|
|
3133
|
+
};
|
|
3134
|
+
const packages = options.packages ?? TRADITIONAL_NATIVE_PACKAGES;
|
|
3135
|
+
const fetchPrebuild = options.fetchPrebuild ?? makeDefaultFetch(closureDir, options.logger);
|
|
3136
|
+
const fetched = [];
|
|
3137
|
+
for (const pkg of packages) {
|
|
3138
|
+
const moduleDir = path$39.join(nm, pkg);
|
|
3139
|
+
if (!fs$17.existsSync(moduleDir)) continue;
|
|
3140
|
+
if (hasDotNode(moduleDir, 4)) continue;
|
|
3141
|
+
options.logger.warn("staged closure native prebuild missing — fetching for running ABI", { meta: {
|
|
3142
|
+
pkg,
|
|
3143
|
+
moduleDir,
|
|
3144
|
+
nodeVersion: target.nodeVersion,
|
|
3145
|
+
arch: target.arch
|
|
3146
|
+
} });
|
|
3147
|
+
try {
|
|
3148
|
+
await fetchPrebuild(moduleDir, target);
|
|
3149
|
+
} catch (err) {
|
|
3150
|
+
options.logger.error("native prebuild fetch failed (validator will decide)", { meta: {
|
|
3151
|
+
pkg,
|
|
3152
|
+
error: errMsg(err)
|
|
3153
|
+
} });
|
|
3154
|
+
continue;
|
|
3155
|
+
}
|
|
3156
|
+
if (hasDotNode(moduleDir, 4)) {
|
|
3157
|
+
fetched.push(pkg);
|
|
3158
|
+
options.logger.info("native prebuild fetched", { meta: { pkg } });
|
|
3159
|
+
} else options.logger.error("native prebuild still missing after fetch", { meta: { pkg } });
|
|
3160
|
+
}
|
|
3161
|
+
return fetched;
|
|
3162
|
+
}
|
|
3163
|
+
//#endregion
|
|
3164
|
+
//#region src/kernel/deps/ensure-addon-natives.ts
|
|
3165
|
+
/**
|
|
3166
|
+
* `ensureAddonNativePrebuilds` — the POST-install native-prebuild ENSURE pass an
|
|
3167
|
+
* addon install runs on the INSTALLED addon dir AFTER `npm install`.
|
|
3168
|
+
*
|
|
3169
|
+
* Why this exists (Mac desktop first-boot FATAL, 2026-07-21): `installCopy`
|
|
3170
|
+
* copies `dist/`, plants prebuilt natives from the bundled addon copy
|
|
3171
|
+
* (`copyBundledNativeModules`), then `npm install`s the addon's runtime deps.
|
|
3172
|
+
* On a TOOLCHAIN-LESS host that `npm install` RE-CREATES the traditional native
|
|
3173
|
+
* `better-sqlite3` SOURCE-ONLY — clobbering the good `.node` the pre-install
|
|
3174
|
+
* copy just planted — and the runner later dies at addon init with a cryptic
|
|
3175
|
+
* "Could not locate the bindings file". The copy-BEFORE-install fix was
|
|
3176
|
+
* empirically insufficient: npm's own install in the copied dir replaces the
|
|
3177
|
+
* satisfied dep without its binary.
|
|
3178
|
+
*
|
|
3179
|
+
* This pass runs LAST, so it always sees the final on-disk state npm left, and
|
|
3180
|
+
* per referenced traditional native still lacking its compiled `.node`:
|
|
3181
|
+
* 1. bundled-copy first — if the bundled addon copy carries the matching
|
|
3182
|
+
* `.node` (same machine → same platform/arch/ABI), re-plant it DIRECTLY.
|
|
3183
|
+
* Cheapest + offline-safe; runs BEFORE any network fetch.
|
|
3184
|
+
* 2. fetch fallback — otherwise fetch the prebuild for the running ABI via the
|
|
3185
|
+
* shared `ensureNativePrebuilds` pointed at the addon dir.
|
|
3186
|
+
* 3. FAIL LOUD — if the native is STILL source-only after both, log a clear
|
|
3187
|
+
* error naming module + addon and THROW so the install fails at install
|
|
3188
|
+
* time rather than at addon init.
|
|
3189
|
+
*
|
|
3190
|
+
* N-API natives (`sharp`, `node-av`) ship their binaries as ABI-stable platform
|
|
3191
|
+
* SIBLING packages npm fetches without a compile step, so they are excluded from
|
|
3192
|
+
* the default set — a miss there is a broken install, not something a prebuild
|
|
3193
|
+
* re-fetch heals (same rationale as `ensureNativePrebuilds`).
|
|
3194
|
+
*/
|
|
3195
|
+
/**
|
|
3196
|
+
* Re-plant `<srcModuleDir>` → `<dstModuleDir>` when the source carries a `.node`.
|
|
3197
|
+
* Returns whether the destination ends up with a compiled binary.
|
|
3198
|
+
*/
|
|
3199
|
+
function replantFromBundled(srcModuleDir, dstModuleDir, hasNative) {
|
|
3200
|
+
if (!hasNative(srcModuleDir)) return false;
|
|
3201
|
+
try {
|
|
3202
|
+
fs$17.rmSync(dstModuleDir, {
|
|
3203
|
+
recursive: true,
|
|
3204
|
+
force: true
|
|
3205
|
+
});
|
|
3206
|
+
fs$17.mkdirSync(path$39.dirname(dstModuleDir), { recursive: true });
|
|
3207
|
+
fs$17.cpSync(srcModuleDir, dstModuleDir, {
|
|
3208
|
+
recursive: true,
|
|
3209
|
+
dereference: true
|
|
3210
|
+
});
|
|
3211
|
+
} catch {
|
|
3212
|
+
return false;
|
|
3213
|
+
}
|
|
3214
|
+
return hasNative(dstModuleDir);
|
|
3215
|
+
}
|
|
3216
|
+
/**
|
|
3217
|
+
* Ensure every referenced traditional native in `<addonDir>/node_modules`
|
|
3218
|
+
* carries its compiled `.node` after the addon's `npm install`. Bundled-copy
|
|
3219
|
+
* first (offline), prebuild-fetch fallback, LOUD throw on a residual miss.
|
|
3220
|
+
*
|
|
3221
|
+
* SKIPS a native absent from the addon's own `node_modules` (it resolves via a
|
|
3222
|
+
* hoisted host copy — not this addon's concern). A NO-OP in docker / dev where
|
|
3223
|
+
* npm produced a working `.node` (already-present). Returns the per-native
|
|
3224
|
+
* outcomes for logging/tests.
|
|
3225
|
+
*
|
|
3226
|
+
* @throws if any required native is still source-only after copy + fetch.
|
|
3227
|
+
*/
|
|
3228
|
+
async function ensureAddonNativePrebuilds(addonDir, options) {
|
|
3229
|
+
const packages = options.packages ?? TRADITIONAL_NATIVE_PACKAGES;
|
|
3230
|
+
const hasNative = options.hasNative ?? ((dir) => hasDotNode(dir, 4));
|
|
3231
|
+
const nm = path$39.join(addonDir, "node_modules");
|
|
3232
|
+
const results = [];
|
|
3233
|
+
const missing = [];
|
|
3234
|
+
for (const pkg of packages) {
|
|
3235
|
+
const moduleDir = path$39.join(nm, pkg);
|
|
3236
|
+
if (!fs$17.existsSync(moduleDir)) continue;
|
|
3237
|
+
if (hasNative(moduleDir)) {
|
|
3238
|
+
results.push({
|
|
3239
|
+
pkg,
|
|
3240
|
+
outcome: "already-present"
|
|
3241
|
+
});
|
|
3242
|
+
continue;
|
|
3243
|
+
}
|
|
3244
|
+
if (options.bundledSourceDir) {
|
|
3245
|
+
const src = path$39.join(options.bundledSourceDir, "node_modules", pkg);
|
|
3246
|
+
if (replantFromBundled(src, moduleDir, hasNative)) {
|
|
3247
|
+
options.logger.info("addon native prebuild re-planted from bundled copy (offline)", { meta: {
|
|
3248
|
+
addon: options.addonName,
|
|
3249
|
+
pkg,
|
|
3250
|
+
src,
|
|
3251
|
+
moduleDir
|
|
3252
|
+
} });
|
|
3253
|
+
results.push({
|
|
3254
|
+
pkg,
|
|
3255
|
+
outcome: "copied"
|
|
3256
|
+
});
|
|
3257
|
+
continue;
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
if ((await ensureNativePrebuilds(addonDir, {
|
|
3261
|
+
logger: options.logger,
|
|
3262
|
+
packages: [pkg],
|
|
3263
|
+
...options.target ? { target: options.target } : {},
|
|
3264
|
+
...options.fetchPrebuild ? { fetchPrebuild: options.fetchPrebuild } : {}
|
|
3265
|
+
})).includes(pkg)) {
|
|
3266
|
+
results.push({
|
|
3267
|
+
pkg,
|
|
3268
|
+
outcome: "fetched"
|
|
3269
|
+
});
|
|
3270
|
+
continue;
|
|
3271
|
+
}
|
|
3272
|
+
options.logger.error("addon native prebuild MISSING after ensure — module has no compiled .node", { meta: {
|
|
3273
|
+
addon: options.addonName,
|
|
3274
|
+
pkg,
|
|
3275
|
+
moduleDir
|
|
3276
|
+
} });
|
|
3277
|
+
results.push({
|
|
3278
|
+
pkg,
|
|
3279
|
+
outcome: "missing"
|
|
3280
|
+
});
|
|
3281
|
+
missing.push(pkg);
|
|
3282
|
+
}
|
|
3283
|
+
if (missing.length > 0) throw new Error(`${options.addonName} — required native module(s) missing a compiled .node after install (bundled-copy + prebuild-fetch both failed): ${missing.join(", ")}`);
|
|
3284
|
+
return results;
|
|
3285
|
+
}
|
|
3286
|
+
//#endregion
|
|
3287
|
+
//#region src/kernel/addon-installer.ts
|
|
3288
|
+
var execFileAsync = promisify(execFile);
|
|
3027
3289
|
function parseInstallSource(value) {
|
|
3028
3290
|
switch (value) {
|
|
3029
3291
|
case "npm":
|
|
@@ -3332,6 +3594,11 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3332
3594
|
} catch (err) {
|
|
3333
3595
|
this.logger.warn(`${packageName} — native deps install failed (continuing)`, { meta: { error: errMsg$1(err) } });
|
|
3334
3596
|
}
|
|
3597
|
+
await ensureAddonNativePrebuilds(targetDir, {
|
|
3598
|
+
addonName: packageName,
|
|
3599
|
+
logger: this.logger,
|
|
3600
|
+
bundledSourceDir: sourceDir
|
|
3601
|
+
});
|
|
3335
3602
|
this.logger.info(`${packageName} — copied from ${sourceDir}`);
|
|
3336
3603
|
return {
|
|
3337
3604
|
name: pkgData.name,
|
|
@@ -3481,7 +3748,7 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3481
3748
|
const stagingDir = path$39.join(stagingRoot, `install-${process.pid}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
|
|
3482
3749
|
ensureDir(stagingDir);
|
|
3483
3750
|
try {
|
|
3484
|
-
await execFileAsync
|
|
3751
|
+
await execFileAsync("tar", [
|
|
3485
3752
|
"-xzf",
|
|
3486
3753
|
tgzPath,
|
|
3487
3754
|
"-C",
|
|
@@ -3518,6 +3785,10 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3518
3785
|
} catch (nativeErr) {
|
|
3519
3786
|
throw new Error(`${pkgView.name} — native deps install failed: ${errMsg$1(nativeErr)}`, { cause: nativeErr });
|
|
3520
3787
|
}
|
|
3788
|
+
await ensureAddonNativePrebuilds(pkgDir, {
|
|
3789
|
+
addonName: pkgView.name,
|
|
3790
|
+
logger: this.logger
|
|
3791
|
+
});
|
|
3521
3792
|
await this.evictInstallDir(targetDir);
|
|
3522
3793
|
ensureDir(path$39.dirname(targetDir));
|
|
3523
3794
|
await fs$17.promises.rename(pkgDir, targetDir);
|
|
@@ -93390,143 +93661,4 @@ var LifecycleJobEngine = class {
|
|
|
93390
93661
|
}
|
|
93391
93662
|
};
|
|
93392
93663
|
//#endregion
|
|
93393
|
-
|
|
93394
|
-
/**
|
|
93395
|
-
* `ensureNativePrebuilds` — the post-install native-prebuild ENSURE step the
|
|
93396
|
-
* shared `RootUpdateService` runs while staging a `@camstack/server` closure,
|
|
93397
|
-
* BEFORE its `findMissingNativePrebuilds` validator (in `@camstack/node-root`)
|
|
93398
|
-
* decides whether to arm the swap.
|
|
93399
|
-
*
|
|
93400
|
-
* Why this exists (Mac desktop incident, 2026-07-21): a runtime
|
|
93401
|
-
* `applyServerUpdate` on a packaged desktop stages the closure with
|
|
93402
|
-
* `npm install @camstack/server@X` under the app's BUNDLED bare Node. The one
|
|
93403
|
-
* TRADITIONAL (non-N-API) native, `better-sqlite3`, resolves its `.node` via
|
|
93404
|
-
* its `install` script `prebuild-install || node-gyp rebuild`. On a user's Mac
|
|
93405
|
-
* with no build toolchain (and where the auto `prebuild-install` does not
|
|
93406
|
-
* target the bundled Node's ABI/arch the way the DMG build's EXPLICIT
|
|
93407
|
-
* `prebuild-install --runtime node --target <bundled> --arch <arch>` call does)
|
|
93408
|
-
* that leaves the module SOURCE-ONLY → the closure has no `better-sqlite3`
|
|
93409
|
-
* `.node` → the validator refuses the swap ("staged closure missing native
|
|
93410
|
-
* prebuilds: better-sqlite3"). The docker image never hits this: its Node ABI
|
|
93411
|
-
* has a published prebuild the auto path fetches cleanly, and it carries a
|
|
93412
|
-
* build toolchain fallback.
|
|
93413
|
-
*
|
|
93414
|
-
* This mirrors the DMG build's explicit re-fetch (`scripts/stage-deps.mjs`)
|
|
93415
|
-
* into the runtime staging path: for each traditional native that is present
|
|
93416
|
-
* in the closure but missing its compiled `.node`, fetch the prebuild for the
|
|
93417
|
-
* RUNNING Node ABI/arch (the same Node that will load the swapped closure).
|
|
93418
|
-
*
|
|
93419
|
-
* N-API natives (`sharp`, `node-av`) are ABI-stable and ship their binaries as
|
|
93420
|
-
* platform SIBLING packages that npm fetches reliably with no compile step, so
|
|
93421
|
-
* they are not in scope here — a genuine miss there is a broken install the
|
|
93422
|
-
* validator should still reject, not something a prebuild re-fetch can heal.
|
|
93423
|
-
*
|
|
93424
|
-
* Injected into the update service from the construction site so the zero-dep
|
|
93425
|
-
* `@camstack/node-root` layer never depends on npm/@camstack/system, and the
|
|
93426
|
-
* fetch is unit-testable with a fake fetcher.
|
|
93427
|
-
*/
|
|
93428
|
-
var execFileAsync = promisify(execFile);
|
|
93429
|
-
/** Bounded recursive `.node` scan depth inside a module dir. */
|
|
93430
|
-
var NATIVE_SCAN_DEPTH = 4;
|
|
93431
|
-
/** Timeout for a single `prebuild-install` fetch. */
|
|
93432
|
-
var PREBUILD_FETCH_TIMEOUT_MS = 12e4;
|
|
93433
|
-
/**
|
|
93434
|
-
* Traditional (compile-or-prebuild) natives an installed closure must carry a
|
|
93435
|
-
* `.node` for. Only these are ENSURE-able by a prebuild re-fetch; N-API natives
|
|
93436
|
-
* are excluded (see file header).
|
|
93437
|
-
*/
|
|
93438
|
-
var TRADITIONAL_NATIVE_PACKAGES = ["better-sqlite3"];
|
|
93439
|
-
/** Bounded recursive scan for ANY `*.node` file under `dir`. */
|
|
93440
|
-
function hasDotNode(dir, maxDepth) {
|
|
93441
|
-
let entries;
|
|
93442
|
-
try {
|
|
93443
|
-
entries = fs$17.readdirSync(dir, { withFileTypes: true });
|
|
93444
|
-
} catch {
|
|
93445
|
-
return false;
|
|
93446
|
-
}
|
|
93447
|
-
for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".node")) return true;
|
|
93448
|
-
if (maxDepth <= 0) return false;
|
|
93449
|
-
for (const entry of entries) if (entry.isDirectory() && hasDotNode(path$39.join(dir, entry.name), maxDepth - 1)) return true;
|
|
93450
|
-
return false;
|
|
93451
|
-
}
|
|
93452
|
-
/** Locate the bundled `prebuild-install` bin.js inside the staged closure. */
|
|
93453
|
-
function resolvePrebuildInstallBin(closureDir, moduleDir) {
|
|
93454
|
-
return [path$39.join(moduleDir, "node_modules", "prebuild-install", "bin.js"), path$39.join(closureDir, "node_modules", "prebuild-install", "bin.js")].find((c) => fs$17.existsSync(c)) ?? null;
|
|
93455
|
-
}
|
|
93456
|
-
/**
|
|
93457
|
-
* Default fetcher: run the closure's own `prebuild-install` under the CURRENT
|
|
93458
|
-
* Node (`process.execPath`) — no reliance on `npx`/`npm` on PATH (a packaged
|
|
93459
|
-
* app ships a bare Node). Explicitly targets the running ABI/arch, mirroring
|
|
93460
|
-
* `scripts/stage-deps.mjs`.
|
|
93461
|
-
*/
|
|
93462
|
-
function makeDefaultFetch(closureDir, logger) {
|
|
93463
|
-
return async (moduleDir, target) => {
|
|
93464
|
-
const bin = resolvePrebuildInstallBin(closureDir, moduleDir);
|
|
93465
|
-
if (bin === null) throw new Error(`prebuild-install not found in the staged closure for ${moduleDir}`);
|
|
93466
|
-
logger.info("fetching native prebuild via prebuild-install", { meta: {
|
|
93467
|
-
moduleDir,
|
|
93468
|
-
bin,
|
|
93469
|
-
nodeVersion: target.nodeVersion,
|
|
93470
|
-
arch: target.arch
|
|
93471
|
-
} });
|
|
93472
|
-
await execFileAsync(process.execPath, [
|
|
93473
|
-
bin,
|
|
93474
|
-
"--runtime",
|
|
93475
|
-
"node",
|
|
93476
|
-
"--target",
|
|
93477
|
-
target.nodeVersion,
|
|
93478
|
-
"--arch",
|
|
93479
|
-
target.arch
|
|
93480
|
-
], {
|
|
93481
|
-
cwd: moduleDir,
|
|
93482
|
-
timeout: PREBUILD_FETCH_TIMEOUT_MS
|
|
93483
|
-
});
|
|
93484
|
-
};
|
|
93485
|
-
}
|
|
93486
|
-
/**
|
|
93487
|
-
* Ensure every traditional native in `<closureDir>/node_modules` carries its
|
|
93488
|
-
* compiled `.node`, fetching the prebuild for the running ABI when missing.
|
|
93489
|
-
*
|
|
93490
|
-
* A NO-OP when nothing is missing (docker / tests) and when a native package is
|
|
93491
|
-
* absent from the closure. Never throws for a single package's fetch failure —
|
|
93492
|
-
* it logs and continues so the downstream `findMissingNativePrebuilds`
|
|
93493
|
-
* validator makes the final swap/abort decision. Returns the packages whose
|
|
93494
|
-
* prebuild was successfully fetched (for logging/tests).
|
|
93495
|
-
*/
|
|
93496
|
-
async function ensureNativePrebuilds(closureDir, options) {
|
|
93497
|
-
const nm = path$39.join(closureDir, "node_modules");
|
|
93498
|
-
const target = options.target ?? {
|
|
93499
|
-
nodeVersion: process.versions.node,
|
|
93500
|
-
arch: process.arch
|
|
93501
|
-
};
|
|
93502
|
-
const packages = options.packages ?? TRADITIONAL_NATIVE_PACKAGES;
|
|
93503
|
-
const fetchPrebuild = options.fetchPrebuild ?? makeDefaultFetch(closureDir, options.logger);
|
|
93504
|
-
const fetched = [];
|
|
93505
|
-
for (const pkg of packages) {
|
|
93506
|
-
const moduleDir = path$39.join(nm, pkg);
|
|
93507
|
-
if (!fs$17.existsSync(moduleDir)) continue;
|
|
93508
|
-
if (hasDotNode(moduleDir, NATIVE_SCAN_DEPTH)) continue;
|
|
93509
|
-
options.logger.warn("staged closure native prebuild missing — fetching for running ABI", { meta: {
|
|
93510
|
-
pkg,
|
|
93511
|
-
moduleDir,
|
|
93512
|
-
nodeVersion: target.nodeVersion,
|
|
93513
|
-
arch: target.arch
|
|
93514
|
-
} });
|
|
93515
|
-
try {
|
|
93516
|
-
await fetchPrebuild(moduleDir, target);
|
|
93517
|
-
} catch (err) {
|
|
93518
|
-
options.logger.error("native prebuild fetch failed (validator will decide)", { meta: {
|
|
93519
|
-
pkg,
|
|
93520
|
-
error: errMsg(err)
|
|
93521
|
-
} });
|
|
93522
|
-
continue;
|
|
93523
|
-
}
|
|
93524
|
-
if (hasDotNode(moduleDir, NATIVE_SCAN_DEPTH)) {
|
|
93525
|
-
fetched.push(pkg);
|
|
93526
|
-
options.logger.info("native prebuild fetched", { meta: { pkg } });
|
|
93527
|
-
} else options.logger.error("native prebuild still missing after fetch", { meta: { pkg } });
|
|
93528
|
-
}
|
|
93529
|
-
return fetched;
|
|
93530
|
-
}
|
|
93531
|
-
//#endregion
|
|
93532
|
-
export { AGENT_CAP_FWD_ACTION, AGENT_CAP_FWD_SERVICE, AGENT_READINESS_SERVICE_NAME, ALL_CAPABILITY_DEFINITIONS, AddonApiFactory, AddonDepsManager, AddonEngineManager, AddonHealthMonitor, AddonInstaller, AddonLoader, AddonManifest, AddonRouteRegistry, AlertCenterAddon, ApiKeyManager, AuthManager, CLUSTER_SECRET_MISMATCH_TYPE, CLUSTER_SECRET_REJECTED_EXIT_CODE, CORE_CAP_SERVICE_NAME, CapRouteError, CapRouteResolver, CapUsageRegistry, CapabilityHandle, CapabilityRegistry, CapabilityUnavailableError, ConfigManager, ConfigStore, ConsoleDestination, ConsoleLoggingAddon, CustomActionRegistry, DEFAULT_DATA_PATH, DEVICE_SETTINGS_CONTRIBUTION_METHODS, DEVICE_STATUS_METHOD, DataPlaneRegistry, DeviceManagerAddon, DeviceRegistry, DeviceStore, EVENT_TOPIC_PREFIX, EngineManagerResolver, EventBus, FeatureManager, FilesystemStorageAddon, FilesystemStorageProvider, FrameDecoder, FsStorageBackend, HEALTH_MONITOR_GRACE_PERIOD_MS, HEALTH_MONITOR_RETRY_INTERVALS_MS, HEALTH_MONITOR_TICK_MS, HUB_CAP_FWD_ACTION, HUB_CAP_FWD_SERVICE, HubForwarderAddon, HubForwarderDestination, HubLogForwarder, HubNodeRegistry, INFRA_CAPABILITIES, IntegrationRegistry, JobJournal, LifecycleJobEngine, LifecycleStateMachine, LocalAuthAddon, LocalChildClient, LocalChildRegistry, LogManager, LogRingBuffer, METHOD_ACCESS_MAP, ModelDownloadService, NATIVE_PROVIDER_SERVICE_INFIX, NativeMetricsAddon, NativeMetricsProvider, NetworkQualityTracker, NotificationService, PYTHON_VERSION, PipelineRunner, PipelineValidator, PythonEnvManager, RESTART_MARKER_FILE, RUNTIME_DEFAULTS, ReadinessRegistry, ReadinessTimeoutError, ReplEngine, RingBuffer, ScopedLogger, ScopedTokenManager, SocketChannel, SqliteSettingsAddon, SqliteSettingsBackend, StagingArea, StorageLocationManager, StorageManager, StorageOrchestratorAddon, StorageOrchestratorService, SystemConfigAddon, SystemEventBus, TRADITIONAL_NATIVE_PACKAGES, ToastService, UDS_NO_ROUTE_PREFIX, UdsLocalTransportClient, UdsLocalTransportServer, UserManager, WinstonDestination, WinstonLoggingAddon, __resetCapUsageRegistryForTests, adaptBrokerToCluster, addonSettingsCapability, bootstrapSchema, brokerCallForCap, brokerTransportLink, buildBinaryPath, buildCapRouters, buildLinkChain, buildNativeCapProxy, buildNodeManifest, buildStorageLocationRegistry, buildUdsNativeCapProxy, builderMountedCapNames, callRegisterNodeWithRetry, callWithServiceDiscovery, capActionName, capActionSuffix, capBareAction, capServiceName, classifyCapRoute, clearPendingRestart, clusterEventTopic, clusterSecretMatches, collectModelFiles, contentTypeFor, copyDirRecursive, copyExtraFileDirs, createAddonContext, createAddonDataPlaneFacility, createAddonService, createAuthenticatedFileServer, createBroker, createBrokerDeviceManagerApi, createCoreCapService, createFileDataPlaneHandler, createHubCapForwardService, createHubService, createKernelHwAccel, createLocalTransport, createParentUnownedCallHandler, createProcessService, createReadinessService, createReadinessServiceForRegistry, createScopedProcessManager, createStreamProbeBrokerService, createUdsAddonContext, createUdsEventBridge, createUdsEventBus, createUdsLogger, createUdsLoggerWithControl, deleteModelFromDisk, deriveAgentListenPort, describeProviderKindDrift, detectWorkspacePackagesDir, downloadBinary, downloadFile, downloadModel, emitDownForOwnedCaps, encodeFrame, ensureBinary, ensureDir, ensureFfmpeg, ensureLibraryBuilt, ensureModel, ensureNativePrebuilds, ensurePython, ensureTlsCert, expandCapMethods, fetchJson, findInPath, formatLogLine, getBrokerEventBus, getCapUsageRegistry, getFfmpegDownloadUrl, getModelFilePath, getMoleculerEventStats, getOrInitReadinessRegistry, getOrInitReadinessRegistryForClient, getPidStats, getPlatformInfo, getPythonDownloadUrl, getRestartMarkerPath, getSinglePidStats, getWorkerDeviceRegistry, hashClusterSecret, installManifestNativeDeps, installManifestPythonDeps, installPackageFromNpm, installPythonPackages, installPythonRequirements, ipcChildLink, ipcParentLink, isAddonDeploySource, isArrayOutputSchema, isClusterSecretMismatchError, isCollectionArrayMethod, isInfraCapability, isModelDownloaded, isSourceNewer, loadTlsCert, localEndpointPath, localProviderLink, mountNativeCapService, parseCapAction, parseRangeHeader, parseTokenizedUrl, proxyToUpstream, readPendingRestart, readinessKey, registerEventBusService, resolveFilePath, resolveHwAccel, resolveNpmInvocation, runNpm, scheduleSelfRestart, scopeKey, scopesAllowDeviceCap, serializeTypedArrays, setHubConnected, setNodeEventInterest, stripCamstackDeps, subscribePassthrough, udsChildLogToWorkerEntry, validateProviderRegistrations, writePendingRestart };
|
|
93664
|
+
export { AGENT_CAP_FWD_ACTION, AGENT_CAP_FWD_SERVICE, AGENT_READINESS_SERVICE_NAME, ALL_CAPABILITY_DEFINITIONS, AddonApiFactory, AddonDepsManager, AddonEngineManager, AddonHealthMonitor, AddonInstaller, AddonLoader, AddonManifest, AddonRouteRegistry, AlertCenterAddon, ApiKeyManager, AuthManager, CLUSTER_SECRET_MISMATCH_TYPE, CLUSTER_SECRET_REJECTED_EXIT_CODE, CORE_CAP_SERVICE_NAME, CapRouteError, CapRouteResolver, CapUsageRegistry, CapabilityHandle, CapabilityRegistry, CapabilityUnavailableError, ConfigManager, ConfigStore, ConsoleDestination, ConsoleLoggingAddon, CustomActionRegistry, DEFAULT_DATA_PATH, DEVICE_SETTINGS_CONTRIBUTION_METHODS, DEVICE_STATUS_METHOD, DataPlaneRegistry, DeviceManagerAddon, DeviceRegistry, DeviceStore, EVENT_TOPIC_PREFIX, EngineManagerResolver, EventBus, FeatureManager, FilesystemStorageAddon, FilesystemStorageProvider, FrameDecoder, FsStorageBackend, HEALTH_MONITOR_GRACE_PERIOD_MS, HEALTH_MONITOR_RETRY_INTERVALS_MS, HEALTH_MONITOR_TICK_MS, HUB_CAP_FWD_ACTION, HUB_CAP_FWD_SERVICE, HubForwarderAddon, HubForwarderDestination, HubLogForwarder, HubNodeRegistry, INFRA_CAPABILITIES, IntegrationRegistry, JobJournal, LifecycleJobEngine, LifecycleStateMachine, LocalAuthAddon, LocalChildClient, LocalChildRegistry, LogManager, LogRingBuffer, METHOD_ACCESS_MAP, ModelDownloadService, NATIVE_PROVIDER_SERVICE_INFIX, NATIVE_SCAN_DEPTH, NativeMetricsAddon, NativeMetricsProvider, NetworkQualityTracker, NotificationService, PYTHON_VERSION, PipelineRunner, PipelineValidator, PythonEnvManager, RESTART_MARKER_FILE, RUNTIME_DEFAULTS, ReadinessRegistry, ReadinessTimeoutError, ReplEngine, RingBuffer, ScopedLogger, ScopedTokenManager, SocketChannel, SqliteSettingsAddon, SqliteSettingsBackend, StagingArea, StorageLocationManager, StorageManager, StorageOrchestratorAddon, StorageOrchestratorService, SystemConfigAddon, SystemEventBus, TRADITIONAL_NATIVE_PACKAGES, ToastService, UDS_NO_ROUTE_PREFIX, UdsLocalTransportClient, UdsLocalTransportServer, UserManager, WinstonDestination, WinstonLoggingAddon, __resetCapUsageRegistryForTests, adaptBrokerToCluster, addonSettingsCapability, bootstrapSchema, brokerCallForCap, brokerTransportLink, buildBinaryPath, buildCapRouters, buildLinkChain, buildNativeCapProxy, buildNodeManifest, buildStorageLocationRegistry, buildUdsNativeCapProxy, builderMountedCapNames, callRegisterNodeWithRetry, callWithServiceDiscovery, capActionName, capActionSuffix, capBareAction, capServiceName, classifyCapRoute, clearPendingRestart, clusterEventTopic, clusterSecretMatches, collectModelFiles, contentTypeFor, copyDirRecursive, copyExtraFileDirs, createAddonContext, createAddonDataPlaneFacility, createAddonService, createAuthenticatedFileServer, createBroker, createBrokerDeviceManagerApi, createCoreCapService, createFileDataPlaneHandler, createHubCapForwardService, createHubService, createKernelHwAccel, createLocalTransport, createParentUnownedCallHandler, createProcessService, createReadinessService, createReadinessServiceForRegistry, createScopedProcessManager, createStreamProbeBrokerService, createUdsAddonContext, createUdsEventBridge, createUdsEventBus, createUdsLogger, createUdsLoggerWithControl, deleteModelFromDisk, deriveAgentListenPort, describeProviderKindDrift, detectWorkspacePackagesDir, downloadBinary, downloadFile, downloadModel, emitDownForOwnedCaps, encodeFrame, ensureAddonNativePrebuilds, ensureBinary, ensureDir, ensureFfmpeg, ensureLibraryBuilt, ensureModel, ensureNativePrebuilds, ensurePython, ensureTlsCert, expandCapMethods, fetchJson, findInPath, formatLogLine, getBrokerEventBus, getCapUsageRegistry, getFfmpegDownloadUrl, getModelFilePath, getMoleculerEventStats, getOrInitReadinessRegistry, getOrInitReadinessRegistryForClient, getPidStats, getPlatformInfo, getPythonDownloadUrl, getRestartMarkerPath, getSinglePidStats, getWorkerDeviceRegistry, hasDotNode, hashClusterSecret, installManifestNativeDeps, installManifestPythonDeps, installPackageFromNpm, installPythonPackages, installPythonRequirements, ipcChildLink, ipcParentLink, isAddonDeploySource, isArrayOutputSchema, isClusterSecretMismatchError, isCollectionArrayMethod, isInfraCapability, isModelDownloaded, isSourceNewer, loadTlsCert, localEndpointPath, localProviderLink, mountNativeCapService, parseCapAction, parseRangeHeader, parseTokenizedUrl, proxyToUpstream, readPendingRestart, readinessKey, registerEventBusService, resolveFilePath, resolveHwAccel, resolveNpmInvocation, runNpm, scheduleSelfRestart, scopeKey, scopesAllowDeviceCap, serializeTypedArrays, setHubConnected, setNodeEventInterest, stripCamstackDeps, subscribePassthrough, udsChildLogToWorkerEntry, validateProviderRegistrations, writePendingRestart };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { IScopedLogger } from '@camstack/types';
|
|
2
|
+
import { PrebuildFetchFn, PrebuildTarget } from './ensure-native-prebuilds.js';
|
|
3
|
+
/** Per-native outcome of the ensure pass. */
|
|
4
|
+
export type NativeEnsureOutcome = 'already-present' | 'copied' | 'fetched' | 'missing';
|
|
5
|
+
export interface NativeEnsureResult {
|
|
6
|
+
readonly pkg: string;
|
|
7
|
+
readonly outcome: NativeEnsureOutcome;
|
|
8
|
+
}
|
|
9
|
+
export interface EnsureAddonNativePrebuildsOptions {
|
|
10
|
+
/** Addon package name — for loud error messages naming module + addon. */
|
|
11
|
+
readonly addonName: string;
|
|
12
|
+
readonly logger: IScopedLogger;
|
|
13
|
+
/**
|
|
14
|
+
* Bundled addon copy dir (the `installCopy` `sourceDir` / a
|
|
15
|
+
* `CAMSTACK_BUNDLED_ADDONS_DIR/<addon>` copy) whose own
|
|
16
|
+
* `node_modules/<pkg>` may carry a prebuilt `.node` for THIS machine.
|
|
17
|
+
* Copied in directly before any network fetch. Absent on the npm/tgz path.
|
|
18
|
+
*/
|
|
19
|
+
readonly bundledSourceDir?: string;
|
|
20
|
+
/** Running ABI/arch the fetched prebuild must match. Defaults to the process. */
|
|
21
|
+
readonly target?: PrebuildTarget;
|
|
22
|
+
/** Native package set. Defaults to `TRADITIONAL_NATIVE_PACKAGES`. */
|
|
23
|
+
readonly packages?: readonly string[];
|
|
24
|
+
/** Override the fetcher (tests). Production uses the bundled `prebuild-install`. */
|
|
25
|
+
readonly fetchPrebuild?: PrebuildFetchFn;
|
|
26
|
+
/** Override the `.node` presence scan (tests). */
|
|
27
|
+
readonly hasNative?: (moduleDir: string) => boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Ensure every referenced traditional native in `<addonDir>/node_modules`
|
|
31
|
+
* carries its compiled `.node` after the addon's `npm install`. Bundled-copy
|
|
32
|
+
* first (offline), prebuild-fetch fallback, LOUD throw on a residual miss.
|
|
33
|
+
*
|
|
34
|
+
* SKIPS a native absent from the addon's own `node_modules` (it resolves via a
|
|
35
|
+
* hoisted host copy — not this addon's concern). A NO-OP in docker / dev where
|
|
36
|
+
* npm produced a working `.node` (already-present). Returns the per-native
|
|
37
|
+
* outcomes for logging/tests.
|
|
38
|
+
*
|
|
39
|
+
* @throws if any required native is still source-only after copy + fetch.
|
|
40
|
+
*/
|
|
41
|
+
export declare function ensureAddonNativePrebuilds(addonDir: string, options: EnsureAddonNativePrebuildsOptions): Promise<readonly NativeEnsureResult[]>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { IScopedLogger } from '@camstack/types';
|
|
2
|
+
/** Bounded recursive `.node` scan depth inside a module dir. */
|
|
3
|
+
export declare const NATIVE_SCAN_DEPTH = 4;
|
|
2
4
|
/**
|
|
3
5
|
* Traditional (compile-or-prebuild) natives an installed closure must carry a
|
|
4
6
|
* `.node` for. Only these are ENSURE-able by a prebuild re-fetch; N-API natives
|
|
@@ -23,6 +25,8 @@ export interface EnsureNativePrebuildsOptions {
|
|
|
23
25
|
/** Override the fetcher (tests). Production uses the bundled `prebuild-install`. */
|
|
24
26
|
readonly fetchPrebuild?: PrebuildFetchFn;
|
|
25
27
|
}
|
|
28
|
+
/** Bounded recursive scan for ANY `*.node` file under `dir`. */
|
|
29
|
+
export declare function hasDotNode(dir: string, maxDepth: number): boolean;
|
|
26
30
|
/**
|
|
27
31
|
* Ensure every traditional native in `<closureDir>/node_modules` carries its
|
|
28
32
|
* compiled `.node`, fetching the prebuild for the running ABI when missing.
|