@agent-vm/agent-vm 0.0.87 → 0.0.88
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/build/docker-image-builder.d.ts +12 -0
- package/dist/build/docker-image-builder.d.ts.map +1 -1
- package/dist/build/docker-image-builder.js +57 -2
- package/dist/build/docker-image-builder.js.map +1 -1
- package/dist/build/gondolin-image-builder.d.ts +8 -1
- package/dist/build/gondolin-image-builder.d.ts.map +1 -1
- package/dist/build/gondolin-image-builder.js +38 -4
- package/dist/build/gondolin-image-builder.js.map +1 -1
- package/dist/build/managed-image-dockerfile.d.ts +2 -0
- package/dist/build/managed-image-dockerfile.d.ts.map +1 -1
- package/dist/build/managed-image-dockerfile.js +84 -15
- package/dist/build/managed-image-dockerfile.js.map +1 -1
- package/dist/build/prepared-gondolin-image-cache.d.ts +17 -0
- package/dist/build/prepared-gondolin-image-cache.d.ts.map +1 -0
- package/dist/build/prepared-gondolin-image-cache.js +91 -0
- package/dist/build/prepared-gondolin-image-cache.js.map +1 -0
- package/dist/cli/build-command.d.ts +7 -1
- package/dist/cli/build-command.d.ts.map +1 -1
- package/dist/cli/build-command.js +329 -79
- package/dist/cli/build-command.js.map +1 -1
- package/dist/cli/codex-harness-auth-command.js +1 -1
- package/dist/cli/codex-harness-auth-command.js.map +1 -1
- package/dist/cli/commands/build-definition.d.ts.map +1 -1
- package/dist/cli/commands/build-definition.js +3 -2
- package/dist/cli/commands/build-definition.js.map +1 -1
- package/dist/cli/init-command.d.ts.map +1 -1
- package/dist/cli/init-command.js +33 -29
- package/dist/cli/init-command.js.map +1 -1
- package/dist/cli/manual-templates.d.ts.map +1 -1
- package/dist/cli/manual-templates.js +6 -5
- package/dist/cli/manual-templates.js.map +1 -1
- package/dist/cli/run-task.d.ts +3 -1
- package/dist/cli/run-task.d.ts.map +1 -1
- package/dist/cli/run-task.js +54 -11
- package/dist/cli/run-task.js.map +1 -1
- package/dist/gateway/gateway-image-builder.d.ts.map +1 -1
- package/dist/gateway/gateway-image-builder.js +9 -0
- package/dist/gateway/gateway-image-builder.js.map +1 -1
- package/dist/operations/openclaw-deployment-doctor.d.ts.map +1 -1
- package/dist/operations/openclaw-deployment-doctor.js +41 -2
- package/dist/operations/openclaw-deployment-doctor.js.map +1 -1
- package/dist/shared/run-task.d.ts +9 -0
- package/dist/shared/run-task.d.ts.map +1 -1
- package/dist/shared/run-task.js.map +1 -1
- package/dist/tool-vm/tool-vm-lifecycle.d.ts.map +1 -1
- package/dist/tool-vm/tool-vm-lifecycle.js +9 -2
- package/dist/tool-vm/tool-vm-lifecycle.js.map +1 -1
- package/managed-images.json +5 -4
- package/package.json +11 -11
|
@@ -2,9 +2,10 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { buildImageAssetFileNames, hasBuiltImageAssets, } from '@agent-vm/gondolin-adapter';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { buildDockerImage as buildDockerImageDefault } from '../build/docker-image-builder.js';
|
|
5
|
+
import { buildDockerImage as buildDockerImageDefault, resolveDockerRootfsIdentity as resolveDockerRootfsIdentityDefault, } from '../build/docker-image-builder.js';
|
|
6
6
|
import { buildGondolinImage as buildGondolinImageDefault, computeFingerprintFromConfigPath, } from '../build/gondolin-image-builder.js';
|
|
7
7
|
import { generateManagedDockerfile as generateManagedDockerfileDefault, resolveManagedImageRelease as resolveManagedImageReleaseDefault, } from '../build/managed-image-dockerfile.js';
|
|
8
|
+
import { writePreparedGondolinImage } from '../build/prepared-gondolin-image-cache.js';
|
|
8
9
|
import { deleteStaleImageDirectories as deleteStaleImageDirectoriesDefault, findPrunableImageDirectories as findPrunableImageDirectoriesDefault, } from '../build/stale-image-cleaner.js';
|
|
9
10
|
import { assertGondolinZigCompatibility, resolveGondolinCompatibleZigVersion, resolveHostZigVersion, } from '../build/zig-compatibility.js';
|
|
10
11
|
import { loadJsonConfigFile } from '../config/json-config-file.js';
|
|
@@ -16,6 +17,11 @@ const ociImageTagSchema = z.object({
|
|
|
16
17
|
}),
|
|
17
18
|
});
|
|
18
19
|
const RETAIN_STALE_IMAGE_GENERATIONS_PER_PROFILE = 2;
|
|
20
|
+
const DOCKER_BUILD_CONCURRENCY = 2;
|
|
21
|
+
const GONDOLIN_BUILD_CONCURRENCY = 2;
|
|
22
|
+
const BUILD_DETAIL_MAX_LENGTH = 180;
|
|
23
|
+
const GONDOLIN_BUILD_SANDBOX_HELPERS_FROM_SOURCE_ENV = 'GONDOLIN_BUILD_SANDBOX_HELPERS_FROM_SOURCE';
|
|
24
|
+
const TASK_OUTPUT_BUFFER_MAX_LENGTH = 4_096;
|
|
19
25
|
const gatewayRuntimeRecordFileName = 'gateway-runtime.json';
|
|
20
26
|
const openClawManagedPackageConfigSchema = z
|
|
21
27
|
.object({
|
|
@@ -40,8 +46,42 @@ function imageTargetKey(imageTarget) {
|
|
|
40
46
|
function imageTargetDedupeKey(options) {
|
|
41
47
|
return `${path.resolve(options.buildConfigPath)}${imageTargetKeySeparator}${options.fingerprint}`;
|
|
42
48
|
}
|
|
43
|
-
function
|
|
44
|
-
return path.resolve(options.buildConfigPath)
|
|
49
|
+
function imageTargetFingerprintInputKey(options) {
|
|
50
|
+
return `${path.resolve(options.buildConfigPath)}${imageTargetKeySeparator}${JSON.stringify(options.fingerprintInput ?? null)}`;
|
|
51
|
+
}
|
|
52
|
+
async function runWithConcurrency(items, concurrency, fn) {
|
|
53
|
+
let nextIndex = 0;
|
|
54
|
+
const workerCount = Math.min(concurrency, items.length);
|
|
55
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
56
|
+
for (;;) {
|
|
57
|
+
const index = nextIndex;
|
|
58
|
+
nextIndex += 1;
|
|
59
|
+
if (index >= items.length) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const item = items[index];
|
|
63
|
+
if (item === undefined) {
|
|
64
|
+
throw new Error(`Expected build queue item at index ${index}.`);
|
|
65
|
+
}
|
|
66
|
+
// oxlint-disable-next-line no-await-in-loop -- each worker intentionally processes its own queue slot serially while workers run in parallel
|
|
67
|
+
await fn(item);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
await Promise.all(workers);
|
|
71
|
+
}
|
|
72
|
+
function createRunTaskGroupFallback(runTaskStep) {
|
|
73
|
+
return async (tasks, options) => {
|
|
74
|
+
await runWithConcurrency(tasks, options.concurrency, async (task) => {
|
|
75
|
+
await runTaskStep(task.title, task.fn);
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function firstGondolinTargetPlan(targetPlans) {
|
|
80
|
+
const [targetPlan] = targetPlans;
|
|
81
|
+
if (!targetPlan) {
|
|
82
|
+
throw new Error('Expected at least one Gondolin target plan.');
|
|
83
|
+
}
|
|
84
|
+
return targetPlan;
|
|
45
85
|
}
|
|
46
86
|
function createEmptyCurrentImageFingerprints() {
|
|
47
87
|
return {
|
|
@@ -96,6 +136,16 @@ async function materializeGondolinImageAlias(options) {
|
|
|
96
136
|
}
|
|
97
137
|
return targetImagePath;
|
|
98
138
|
}
|
|
139
|
+
async function materializePreparedTargetImage(options) {
|
|
140
|
+
const targetImagePath = path.join(options.targetCacheDirectory, options.fingerprint);
|
|
141
|
+
if (path.resolve(options.sourceImagePath) === path.resolve(targetImagePath)) {
|
|
142
|
+
return targetImagePath;
|
|
143
|
+
}
|
|
144
|
+
if (!(await hasBuiltImageAssets(options.sourceImagePath))) {
|
|
145
|
+
return options.sourceImagePath;
|
|
146
|
+
}
|
|
147
|
+
return await materializeGondolinImageAlias(options);
|
|
148
|
+
}
|
|
99
149
|
async function findZoneIdsWithGatewayRuntimeRecords(systemConfig) {
|
|
100
150
|
const zoneIds = [];
|
|
101
151
|
for (const zone of systemConfig.zones) {
|
|
@@ -120,18 +170,29 @@ async function findZoneIdsWithGatewayRuntimeRecords(systemConfig) {
|
|
|
120
170
|
}
|
|
121
171
|
return zoneIds;
|
|
122
172
|
}
|
|
123
|
-
function
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
173
|
+
function startElapsedStatusController(taskContext, initialStatus) {
|
|
174
|
+
let currentStatus = initialStatus;
|
|
175
|
+
const renderStatus = () => {
|
|
176
|
+
if (taskContext?.interactive === true) {
|
|
177
|
+
const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAtMs) / 1000));
|
|
178
|
+
taskContext.setStatus(`${currentStatus} · ${elapsedSeconds}s elapsed`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
taskContext?.setStatus(currentStatus);
|
|
182
|
+
};
|
|
128
183
|
const startedAtMs = Date.now();
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
184
|
+
taskContext?.setStatus(currentStatus);
|
|
185
|
+
const heartbeatInterval = taskContext?.interactive === true ? setInterval(renderStatus, 8_000) : undefined;
|
|
186
|
+
return {
|
|
187
|
+
setBaseStatus: (status) => {
|
|
188
|
+
currentStatus = status;
|
|
189
|
+
renderStatus();
|
|
190
|
+
},
|
|
191
|
+
stop: () => {
|
|
192
|
+
if (heartbeatInterval) {
|
|
193
|
+
clearInterval(heartbeatInterval);
|
|
194
|
+
}
|
|
195
|
+
},
|
|
135
196
|
};
|
|
136
197
|
}
|
|
137
198
|
async function resolveOciImageTagFromConfig(buildConfigPath) {
|
|
@@ -150,20 +211,31 @@ async function assertZigBuildPrerequisite(resolveRequiredZigVersion, resolveZigV
|
|
|
150
211
|
...(zigVersion ? { installedVersion: zigVersion } : {}),
|
|
151
212
|
});
|
|
152
213
|
}
|
|
214
|
+
function isTruthyEnvironmentFlag(value) {
|
|
215
|
+
const normalizedValue = value?.trim().toLowerCase();
|
|
216
|
+
return (normalizedValue === '1' ||
|
|
217
|
+
normalizedValue === 'true' ||
|
|
218
|
+
normalizedValue === 'yes' ||
|
|
219
|
+
normalizedValue === 'on');
|
|
220
|
+
}
|
|
221
|
+
function shouldAssertZigBuildPrerequisite(env = process.env) {
|
|
222
|
+
return isTruthyEnvironmentFlag(env[GONDOLIN_BUILD_SANDBOX_HELPERS_FROM_SOURCE_ENV]);
|
|
223
|
+
}
|
|
153
224
|
async function assertUniqueDockerImageTags(imageTargets, resolveOciImageTag) {
|
|
154
225
|
const profileByTag = new Map();
|
|
155
|
-
const
|
|
226
|
+
const tagByTargetKey = new Map();
|
|
156
227
|
for (const imageTarget of imageTargets) {
|
|
157
228
|
// oxlint-disable-next-line no-await-in-loop -- collision errors are clearer in stable target order
|
|
158
229
|
const imageTag = await resolveOciImageTag(imageTarget.buildConfigPath);
|
|
159
230
|
const existingProfile = profileByTag.get(imageTag);
|
|
231
|
+
const key = imageTargetKey(imageTarget);
|
|
160
232
|
if (existingProfile) {
|
|
161
|
-
throw new Error(`Docker image tag '${imageTag}' is used by both image profiles '${existingProfile}' and '${
|
|
233
|
+
throw new Error(`Docker image tag '${imageTag}' is used by both image profiles '${existingProfile}' and '${key}'. Give each Docker-backed image profile a unique oci.image tag.`);
|
|
162
234
|
}
|
|
163
|
-
profileByTag.set(imageTag,
|
|
164
|
-
|
|
235
|
+
profileByTag.set(imageTag, key);
|
|
236
|
+
tagByTargetKey.set(key, imageTag);
|
|
165
237
|
}
|
|
166
|
-
return
|
|
238
|
+
return tagByTargetKey;
|
|
167
239
|
}
|
|
168
240
|
const defaultRunTask = async (title, fn) => {
|
|
169
241
|
process.stderr.write(` ${title}...\n`);
|
|
@@ -222,46 +294,177 @@ async function resolveRequiredOpenClawPackagesForTarget(systemConfig, imageTarge
|
|
|
222
294
|
}
|
|
223
295
|
return [...requiredPackageNames].toSorted();
|
|
224
296
|
}
|
|
225
|
-
function
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
297
|
+
function shortenBuildDetail(detail) {
|
|
298
|
+
if (detail.length <= BUILD_DETAIL_MAX_LENGTH) {
|
|
299
|
+
return detail;
|
|
300
|
+
}
|
|
301
|
+
const prefixLength = Math.max(20, Math.floor((BUILD_DETAIL_MAX_LENGTH - 5) * 0.55));
|
|
302
|
+
const suffixLength = BUILD_DETAIL_MAX_LENGTH - prefixLength - 5;
|
|
303
|
+
return `${detail.slice(0, prefixLength)} ... ${detail.slice(-suffixLength)}`;
|
|
304
|
+
}
|
|
305
|
+
function packageNameFromSpec(packageSpec) {
|
|
306
|
+
const versionSeparatorIndex = packageSpec.lastIndexOf('@');
|
|
307
|
+
const unversionedSpec = versionSeparatorIndex > 0 ? packageSpec.slice(0, versionSeparatorIndex) : packageSpec;
|
|
308
|
+
return unversionedSpec.replace(/^@openclaw\//, '').replace(/^@agent-vm\//, '');
|
|
309
|
+
}
|
|
310
|
+
function packageVersionFromSpec(packageSpec) {
|
|
311
|
+
const versionSeparatorIndex = packageSpec.lastIndexOf('@');
|
|
312
|
+
if (versionSeparatorIndex <= 0) {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
return packageSpec.slice(versionSeparatorIndex + 1);
|
|
316
|
+
}
|
|
317
|
+
function formatAgentVmPackageStatus(packages) {
|
|
318
|
+
if (packages.length === 0) {
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
const versions = [
|
|
322
|
+
...new Set(packages
|
|
323
|
+
.map((packageEntry) => packageVersionFromSpec(packageEntry.spec))
|
|
324
|
+
.filter((version) => version !== undefined)),
|
|
231
325
|
];
|
|
232
|
-
|
|
233
|
-
|
|
326
|
+
return versions.length === 1 ? `agent-vm ${versions[0]}` : 'agent-vm packages';
|
|
327
|
+
}
|
|
328
|
+
function formatManagedPackagePlanEntry(packageEntry) {
|
|
329
|
+
return `${packageNameFromSpec(packageEntry.spec)}@${packageVersionFromSpec(packageEntry.spec) ?? 'unversioned'}[${packageEntry.source}]`;
|
|
330
|
+
}
|
|
331
|
+
function formatDockerBaseDetail(options) {
|
|
332
|
+
const details = [];
|
|
333
|
+
const plan = options.managedDockerfilePlan;
|
|
334
|
+
if (!plan) {
|
|
335
|
+
return shortenBuildDetail(`dockerfile ${path.basename(options.dockerfilePath)}`);
|
|
336
|
+
}
|
|
337
|
+
details.push(`base ${plan.base}:${plan.baseImage.tag}`);
|
|
338
|
+
if (options.imageTarget.source?.overlay) {
|
|
339
|
+
details.push(`overlay ${path.basename(options.imageTarget.source.overlay)}`);
|
|
340
|
+
}
|
|
341
|
+
const agentVmPackages = [
|
|
342
|
+
plan.openClawAgentVmPluginPackage,
|
|
343
|
+
plan.openClawMcpPortalPluginPackage,
|
|
344
|
+
plan.mcpPortalPackage,
|
|
345
|
+
].filter((packageEntry) => packageEntry !== undefined);
|
|
346
|
+
const agentVmPackageStatus = formatAgentVmPackageStatus(agentVmPackages);
|
|
347
|
+
if (agentVmPackageStatus) {
|
|
348
|
+
details.push(agentVmPackageStatus);
|
|
234
349
|
}
|
|
235
350
|
if (plan.openClawPackages.length > 0) {
|
|
236
|
-
|
|
237
|
-
for (const packageEntry of plan.openClawPackages) {
|
|
238
|
-
lines.push(` ${packageEntry.spec}`, ` source: ${packageEntry.source}`);
|
|
239
|
-
}
|
|
351
|
+
details.push(`packages ${plan.openClawPackages.map((packageEntry) => formatManagedPackagePlanEntry(packageEntry)).join(',')}`);
|
|
240
352
|
}
|
|
241
353
|
if (plan.warnings.length > 0) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
354
|
+
details.push(`warnings ${plan.warnings.length}`);
|
|
355
|
+
}
|
|
356
|
+
return shortenBuildDetail(details.join(' | '));
|
|
357
|
+
}
|
|
358
|
+
function normalizeDockerOutputLine(line) {
|
|
359
|
+
const normalizedLine = line.trim();
|
|
360
|
+
if (normalizedLine.length === 0) {
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
return normalizedLine.replace(/\s+/g, ' ');
|
|
364
|
+
}
|
|
365
|
+
function createDockerTaskOutput(taskContext, baseDetail) {
|
|
366
|
+
if (taskContext?.interactive !== true) {
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
write: (chunk) => {
|
|
371
|
+
const line = normalizeDockerOutputLine(String(chunk));
|
|
372
|
+
if (!line) {
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
const detailSuffix = baseDetail ? ` | ${baseDetail}` : '';
|
|
376
|
+
taskContext.setOutput(shortenBuildDetail(`${line}${detailSuffix}`));
|
|
377
|
+
return true;
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const gondolinPhasePatterns = [
|
|
382
|
+
{ pattern: /^Extracting OCI rootfs\b/, status: 'extracting OCI rootfs' },
|
|
383
|
+
{
|
|
384
|
+
pattern: /^Creating OCI export container\b/,
|
|
385
|
+
status: 'exporting OCI rootfs',
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
pattern: /^Extracting Alpine minirootfs for rootfs\b/,
|
|
389
|
+
status: 'extracting rootfs',
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
pattern: /^Extracting Alpine minirootfs for initramfs\b/,
|
|
393
|
+
status: 'extracting initramfs',
|
|
394
|
+
},
|
|
395
|
+
{ pattern: /^Installing rootfs packages\b/, status: 'installing rootfs packages' },
|
|
396
|
+
{ pattern: /^Installing initramfs packages\b/, status: 'installing initramfs packages' },
|
|
397
|
+
{ pattern: /^Bootstrapped busybox shell\b/, status: 'bootstrapping rootfs shell' },
|
|
398
|
+
{ pattern: /^Applying post-build copies\b/, status: 'applying post-build copies' },
|
|
399
|
+
{ pattern: /^Running post-build command\b/, status: 'running post-build commands' },
|
|
400
|
+
{ pattern: /^Syncing kernel modules\b/, status: 'copying kernel modules' },
|
|
401
|
+
{ pattern: /^Creating rootfs ext4 image\b/, status: 'creating rootfs image' },
|
|
402
|
+
{ pattern: /^Creating initramfs\b/, status: 'creating initramfs' },
|
|
403
|
+
{ pattern: /^Rootfs image written\b/, status: 'rootfs image ready' },
|
|
404
|
+
{ pattern: /^Fetching kernel\b/, status: 'fetching kernel' },
|
|
405
|
+
{ pattern: /^Fetching libkrunfw-compatible kernel\b/, status: 'fetching libkrunfw kernel' },
|
|
406
|
+
{ pattern: /^Copying assets to output directory\b/, status: 'copying vm assets' },
|
|
407
|
+
{ pattern: /^Generating manifest\b/, status: 'generating vm manifest' },
|
|
408
|
+
{ pattern: /^Build complete\b/, status: 'vm asset build complete' },
|
|
409
|
+
];
|
|
410
|
+
function parseGondolinPhaseStatus(line) {
|
|
411
|
+
const normalizedLine = line.trim();
|
|
412
|
+
for (const phasePattern of gondolinPhasePatterns) {
|
|
413
|
+
if (phasePattern.pattern.test(normalizedLine)) {
|
|
414
|
+
return phasePattern.status;
|
|
245
415
|
}
|
|
246
416
|
}
|
|
247
|
-
return
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
function createGondolinPhaseTaskOutput(taskContext, statusController) {
|
|
420
|
+
if (taskContext?.interactive !== true) {
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
let bufferedOutput = '';
|
|
424
|
+
return {
|
|
425
|
+
write: (chunk) => {
|
|
426
|
+
bufferedOutput += String(chunk);
|
|
427
|
+
let lineBreakIndex = bufferedOutput.indexOf('\n');
|
|
428
|
+
while (lineBreakIndex !== -1) {
|
|
429
|
+
const line = bufferedOutput.slice(0, lineBreakIndex);
|
|
430
|
+
bufferedOutput = bufferedOutput.slice(lineBreakIndex + 1);
|
|
431
|
+
const phaseStatus = parseGondolinPhaseStatus(line);
|
|
432
|
+
if (phaseStatus) {
|
|
433
|
+
statusController.setBaseStatus(phaseStatus);
|
|
434
|
+
}
|
|
435
|
+
lineBreakIndex = bufferedOutput.indexOf('\n');
|
|
436
|
+
}
|
|
437
|
+
if (bufferedOutput.length > TASK_OUTPUT_BUFFER_MAX_LENGTH) {
|
|
438
|
+
bufferedOutput = bufferedOutput.slice(-TASK_OUTPUT_BUFFER_MAX_LENGTH);
|
|
439
|
+
}
|
|
440
|
+
return true;
|
|
441
|
+
},
|
|
442
|
+
};
|
|
248
443
|
}
|
|
249
444
|
export async function runBuildCommand(options, dependencies = {}) {
|
|
250
445
|
const buildDockerImage = dependencies.buildDockerImage ?? buildDockerImageDefault;
|
|
446
|
+
const resolveDockerRootfsIdentity = dependencies.resolveDockerRootfsIdentity ?? resolveDockerRootfsIdentityDefault;
|
|
251
447
|
const buildGondolinImage = dependencies.buildGondolinImage ?? buildGondolinImageDefault;
|
|
252
448
|
const computeGondolinFingerprint = dependencies.computeGondolinFingerprint ??
|
|
253
|
-
(async (fingerprintOptions) =>
|
|
449
|
+
(async (fingerprintOptions) => fingerprintOptions.fingerprintInput === undefined
|
|
450
|
+
? await computeFingerprintFromConfigPath(fingerprintOptions.buildConfigPath)
|
|
451
|
+
: await computeFingerprintFromConfigPath(fingerprintOptions.buildConfigPath, {
|
|
452
|
+
fingerprintInput: fingerprintOptions.fingerprintInput,
|
|
453
|
+
}));
|
|
254
454
|
const deleteStaleImageDirectories = dependencies.deleteStaleImageDirectories ?? deleteStaleImageDirectoriesDefault;
|
|
255
455
|
const findPrunableImageDirectories = dependencies.findPrunableImageDirectories ?? findPrunableImageDirectoriesDefault;
|
|
256
456
|
const resolveOciImageTag = dependencies.resolveOciImageTag ?? resolveOciImageTagFromConfig;
|
|
257
457
|
const resolveRequiredZigVersion = dependencies.resolveRequiredZigVersion ?? resolveGondolinCompatibleZigVersion;
|
|
258
458
|
const resolveZigVersion = dependencies.resolveZigVersion ?? resolveHostZigVersion;
|
|
259
459
|
const runTaskStep = dependencies.runTask ?? defaultRunTask;
|
|
460
|
+
const runTaskGroup = dependencies.runTaskGroup ?? createRunTaskGroupFallback(runTaskStep);
|
|
260
461
|
const resolveProjectRoot = dependencies.resolveProjectRootFromDockerfile ?? resolveProjectRootFromDockerfile;
|
|
261
462
|
const generateManagedDockerfile = dependencies.generateManagedDockerfile ?? generateManagedDockerfileDefault;
|
|
262
463
|
const resolveManagedImageRelease = dependencies.resolveManagedImageRelease ?? resolveManagedImageReleaseDefault;
|
|
263
464
|
const syncBundledOpenClawPlugin = dependencies.syncBundledOpenClawPlugin ?? syncBundledOpenClawPluginBundle;
|
|
264
|
-
|
|
465
|
+
if (shouldAssertZigBuildPrerequisite()) {
|
|
466
|
+
await assertZigBuildPrerequisite(resolveRequiredZigVersion, resolveZigVersion);
|
|
467
|
+
}
|
|
265
468
|
const gatewayImageTargets = Object.entries(options.systemConfig.imageProfiles.gateways).map(([profileName, profile]) => ({
|
|
266
469
|
buildConfigPath: profile.buildConfig,
|
|
267
470
|
cacheDirectory: path.join(options.systemConfig.cacheDir, 'gateway-images', profileName),
|
|
@@ -281,15 +484,17 @@ export async function runBuildCommand(options, dependencies = {}) {
|
|
|
281
484
|
}));
|
|
282
485
|
const imageTargets = [...gatewayImageTargets, ...toolVmImageTargets];
|
|
283
486
|
const dockerImageTargets = imageTargets.filter((imageTarget) => imageTarget.dockerfile !== undefined || imageTarget.source !== undefined);
|
|
284
|
-
const
|
|
487
|
+
const dockerImageTagByTargetKey = await assertUniqueDockerImageTags(dockerImageTargets, resolveOciImageTag);
|
|
285
488
|
const managedImageRelease = dockerImageTargets.some((imageTarget) => imageTarget.source !== undefined)
|
|
286
489
|
? await resolveManagedImageRelease()
|
|
287
490
|
: undefined;
|
|
288
|
-
|
|
491
|
+
const dockerBuildPlans = [];
|
|
492
|
+
const dockerFingerprintInputByTargetKey = new Map();
|
|
493
|
+
// oxlint-disable-next-line no-await-in-loop -- Docker inputs are prepared in stable order before bounded parallel builds start
|
|
289
494
|
for (const imageTarget of dockerImageTargets) {
|
|
290
|
-
const imageTag =
|
|
495
|
+
const imageTag = dockerImageTagByTargetKey.get(imageTargetKey(imageTarget));
|
|
291
496
|
if (!imageTag) {
|
|
292
|
-
throw new Error(`Missing resolved Docker image tag for image profile '${imageTarget
|
|
497
|
+
throw new Error(`Missing resolved Docker image tag for image profile '${imageTargetKey(imageTarget)}'.`);
|
|
293
498
|
}
|
|
294
499
|
let dockerfilePath = imageTarget.dockerfile;
|
|
295
500
|
let managedDockerfilePlan;
|
|
@@ -327,48 +532,73 @@ export async function runBuildCommand(options, dependencies = {}) {
|
|
|
327
532
|
await syncBundledOpenClawPlugin(projectRootDirectory, imageTarget.name);
|
|
328
533
|
});
|
|
329
534
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
535
|
+
dockerBuildPlans.push({
|
|
536
|
+
dockerfilePath,
|
|
537
|
+
imageTag,
|
|
538
|
+
imageTarget,
|
|
539
|
+
...(managedDockerfilePlan ? { managedDockerfilePlan } : {}),
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
await runTaskGroup(dockerBuildPlans.map((dockerBuildPlan) => ({
|
|
543
|
+
title: `Docker: ${dockerBuildPlan.imageTarget.family}/${dockerBuildPlan.imageTarget.name} (${dockerBuildPlan.imageTag})`,
|
|
544
|
+
fn: async (taskContext) => {
|
|
545
|
+
const dockerBaseDetail = formatDockerBaseDetail({
|
|
546
|
+
dockerfilePath: dockerBuildPlan.dockerfilePath,
|
|
547
|
+
imageTarget: dockerBuildPlan.imageTarget,
|
|
548
|
+
...(dockerBuildPlan.managedDockerfilePlan
|
|
549
|
+
? { managedDockerfilePlan: dockerBuildPlan.managedDockerfilePlan }
|
|
550
|
+
: {}),
|
|
551
|
+
});
|
|
552
|
+
taskContext?.setOutput(dockerBaseDetail);
|
|
553
|
+
const dockerTaskOutput = createDockerTaskOutput(taskContext, dockerBaseDetail);
|
|
335
554
|
taskContext?.setStatus('docker build');
|
|
336
555
|
await buildDockerImage({
|
|
337
|
-
dockerfilePath,
|
|
338
|
-
imageTag,
|
|
339
|
-
...(taskContext?.interactive === true
|
|
340
|
-
|
|
341
|
-
|
|
556
|
+
dockerfilePath: dockerBuildPlan.dockerfilePath,
|
|
557
|
+
imageTag: dockerBuildPlan.imageTag,
|
|
558
|
+
...(taskContext?.interactive === true ? { quiet: true } : {}),
|
|
559
|
+
...(dockerTaskOutput ? { streamPreview: dockerTaskOutput } : {}),
|
|
560
|
+
});
|
|
561
|
+
taskContext?.setStatus('inspect layers');
|
|
562
|
+
const dockerRootfsIdentity = await resolveDockerRootfsIdentity(dockerBuildPlan.imageTag);
|
|
563
|
+
if (!dockerRootfsIdentity) {
|
|
564
|
+
throw new Error(`Docker image '${dockerBuildPlan.imageTag}' was built but its rootfs identity could not be inspected.`);
|
|
565
|
+
}
|
|
566
|
+
dockerFingerprintInputByTargetKey.set(imageTargetKey(dockerBuildPlan.imageTarget), {
|
|
567
|
+
dockerRootfsIdentity,
|
|
568
|
+
schemaVersion: 1,
|
|
342
569
|
});
|
|
343
570
|
taskContext?.setStatus('docker image ready');
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
const dockerBackedTargets = new Set(dockerImageTargets.map((imageTarget) => imageTargetKey(imageTarget)));
|
|
571
|
+
},
|
|
572
|
+
})), { concurrency: DOCKER_BUILD_CONCURRENCY });
|
|
347
573
|
const currentFingerprints = createEmptyCurrentImageFingerprints();
|
|
348
574
|
const fingerprintByInputKey = new Map();
|
|
349
575
|
const targetPlans = [];
|
|
350
576
|
const targetPlansByDedupeKey = new Map();
|
|
351
577
|
for (const imageTarget of imageTargets) {
|
|
352
|
-
const
|
|
578
|
+
const key = imageTargetKey(imageTarget);
|
|
579
|
+
const fingerprintInput = dockerFingerprintInputByTargetKey.get(key);
|
|
580
|
+
const fingerprintInputKey = imageTargetFingerprintInputKey({
|
|
353
581
|
buildConfigPath: imageTarget.buildConfigPath,
|
|
582
|
+
fingerprintInput,
|
|
354
583
|
});
|
|
355
584
|
let fingerprint = fingerprintByInputKey.get(fingerprintInputKey);
|
|
356
585
|
if (fingerprint === undefined) {
|
|
357
586
|
// oxlint-disable-next-line no-await-in-loop -- fingerprint errors should identify the matching profile path
|
|
358
587
|
fingerprint = await computeGondolinFingerprint({
|
|
359
588
|
buildConfigPath: imageTarget.buildConfigPath,
|
|
589
|
+
...(fingerprintInput === undefined ? {} : { fingerprintInput }),
|
|
360
590
|
});
|
|
361
591
|
fingerprintByInputKey.set(fingerprintInputKey, fingerprint);
|
|
362
592
|
}
|
|
363
|
-
const key = imageTargetKey(imageTarget);
|
|
364
593
|
const dedupeKey = imageTargetDedupeKey({
|
|
365
594
|
buildConfigPath: imageTarget.buildConfigPath,
|
|
366
595
|
fingerprint,
|
|
367
596
|
});
|
|
368
|
-
const shouldResetGondolinCache = options.forceRebuild === true
|
|
597
|
+
const shouldResetGondolinCache = options.forceRebuild === true;
|
|
369
598
|
const targetPlan = {
|
|
370
599
|
dedupeKey,
|
|
371
600
|
fingerprint,
|
|
601
|
+
fingerprintInput,
|
|
372
602
|
imageTarget,
|
|
373
603
|
key,
|
|
374
604
|
sharedDedupeKey: false,
|
|
@@ -388,46 +618,66 @@ export async function runBuildCommand(options, dependencies = {}) {
|
|
|
388
618
|
}
|
|
389
619
|
}
|
|
390
620
|
const builtImageByDedupeKey = new Map();
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
fullReset: targetPlan.shouldResetGondolinCache,
|
|
399
|
-
sourceImagePath: existingBuild.result.imagePath,
|
|
400
|
-
targetCacheDirectory: targetPlan.imageTarget.cacheDirectory,
|
|
401
|
-
});
|
|
402
|
-
setCurrentImageFingerprint(currentFingerprints, targetPlan.imageTarget, existingBuild.result.fingerprint);
|
|
403
|
-
taskContext?.setStatus(`vm assets reused from ${existingBuild.imageTarget.family}/${existingBuild.imageTarget.name}`);
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
const stopHeartbeat = startElapsedStatusHeartbeat(taskContext, targetPlan.shouldResetGondolinCache ? 'building vm assets' : 'checking vm assets');
|
|
621
|
+
const canonicalTargetPlans = [...targetPlansByDedupeKey.values()].map(firstGondolinTargetPlan);
|
|
622
|
+
const builtImageResults = [];
|
|
623
|
+
await runTaskGroup(canonicalTargetPlans.map((targetPlan) => ({
|
|
624
|
+
title: `Gondolin: ${targetPlan.imageTarget.family}/${targetPlan.imageTarget.name}`,
|
|
625
|
+
fn: async (taskContext) => {
|
|
626
|
+
const statusController = startElapsedStatusController(taskContext, targetPlan.shouldResetGondolinCache ? 'building vm assets' : 'checking vm assets');
|
|
627
|
+
const gondolinTaskOutput = createGondolinPhaseTaskOutput(taskContext, statusController);
|
|
407
628
|
let result;
|
|
408
629
|
try {
|
|
409
630
|
result = await buildGondolinImage({
|
|
410
631
|
buildConfigPath: targetPlan.imageTarget.buildConfigPath,
|
|
411
632
|
cacheDir: targetPlan.imageTarget.cacheDirectory,
|
|
633
|
+
...(targetPlan.fingerprintInput === undefined
|
|
634
|
+
? {}
|
|
635
|
+
: { fingerprintInput: targetPlan.fingerprintInput }),
|
|
412
636
|
...(targetPlan.shouldResetGondolinCache ? { fullReset: true } : {}),
|
|
413
|
-
...(
|
|
414
|
-
? { streamPreview: taskContext.streamPreview }
|
|
415
|
-
: {}),
|
|
637
|
+
...(gondolinTaskOutput ? { streamPreview: gondolinTaskOutput } : {}),
|
|
416
638
|
});
|
|
417
639
|
}
|
|
418
640
|
finally {
|
|
419
|
-
|
|
641
|
+
statusController.stop();
|
|
420
642
|
}
|
|
421
643
|
if (targetPlan.sharedDedupeKey && result.fingerprint !== targetPlan.fingerprint) {
|
|
422
644
|
throw new Error(`Fingerprint mismatch for image profile '${targetPlan.key}': precomputed '${targetPlan.fingerprint}' but build returned '${result.fingerprint}'.`);
|
|
423
645
|
}
|
|
424
|
-
|
|
646
|
+
builtImageResults.push({
|
|
425
647
|
imageTarget: targetPlan.imageTarget,
|
|
426
648
|
result,
|
|
649
|
+
targetPlan,
|
|
427
650
|
});
|
|
428
|
-
setCurrentImageFingerprint(currentFingerprints, targetPlan.imageTarget, result.fingerprint);
|
|
429
651
|
taskContext?.setStatus(result.built ? 'vm assets ready' : 'vm assets cache hit');
|
|
652
|
+
},
|
|
653
|
+
})), { concurrency: GONDOLIN_BUILD_CONCURRENCY });
|
|
654
|
+
for (const builtImageResult of builtImageResults) {
|
|
655
|
+
builtImageByDedupeKey.set(builtImageResult.targetPlan.dedupeKey, {
|
|
656
|
+
imageTarget: builtImageResult.imageTarget,
|
|
657
|
+
result: builtImageResult.result,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
for (const targetPlan of targetPlans) {
|
|
661
|
+
const existingBuild = builtImageByDedupeKey.get(targetPlan.dedupeKey);
|
|
662
|
+
if (!existingBuild) {
|
|
663
|
+
throw new Error(`Missing built image result for image profile '${targetPlan.key}'.`);
|
|
664
|
+
}
|
|
665
|
+
// oxlint-disable-next-line no-await-in-loop -- alias materialization is kept deterministic so duplicate-profile errors name the matching profile
|
|
666
|
+
const imagePath = await materializePreparedTargetImage({
|
|
667
|
+
fingerprint: existingBuild.result.fingerprint,
|
|
668
|
+
fullReset: targetPlan.shouldResetGondolinCache,
|
|
669
|
+
sourceImagePath: existingBuild.result.imagePath,
|
|
670
|
+
targetCacheDirectory: targetPlan.imageTarget.cacheDirectory,
|
|
671
|
+
});
|
|
672
|
+
// oxlint-disable-next-line no-await-in-loop -- prepared records are profile-local and must report the matching profile path on failure
|
|
673
|
+
await writePreparedGondolinImage({
|
|
674
|
+
buildConfigPath: targetPlan.imageTarget.buildConfigPath,
|
|
675
|
+
cacheDir: targetPlan.imageTarget.cacheDirectory,
|
|
676
|
+
fingerprint: existingBuild.result.fingerprint,
|
|
677
|
+
fingerprintInput: targetPlan.fingerprintInput,
|
|
678
|
+
imagePath,
|
|
430
679
|
});
|
|
680
|
+
setCurrentImageFingerprint(currentFingerprints, targetPlan.imageTarget, existingBuild.result.fingerprint);
|
|
431
681
|
}
|
|
432
682
|
await runTaskStep('Cache auto-prune', async (taskContext) => {
|
|
433
683
|
taskContext?.setStatus('checking old image generations');
|