@hyperbrowser/sdk 0.92.7 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -0
- package/dist/client.d.ts +4 -20
- package/dist/client.js +7 -17
- package/dist/error.d.ts +27 -0
- package/dist/error.js +32 -0
- package/dist/image-builds.d.ts +6 -0
- package/dist/image-builds.js +12 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +12 -1
- package/dist/retry.d.ts +14 -0
- package/dist/retry.js +49 -0
- package/dist/sandbox/base.d.ts +27 -0
- package/dist/sandbox/base.js +298 -99
- package/dist/sandbox/files.d.ts +24 -25
- package/dist/sandbox/files.js +146 -37
- package/dist/sandbox/image-build/artifacts.d.ts +16 -0
- package/dist/sandbox/image-build/artifacts.js +16 -0
- package/dist/sandbox/image-build/blake2b.d.ts +2 -0
- package/dist/sandbox/image-build/blake2b.js +89 -0
- package/dist/sandbox/image-build/common.d.ts +19 -0
- package/dist/sandbox/image-build/common.js +109 -0
- package/dist/sandbox/image-build/context.d.ts +43 -0
- package/dist/sandbox/image-build/context.js +599 -0
- package/dist/sandbox/image-build/docker-image.d.ts +34 -0
- package/dist/sandbox/image-build/docker-image.js +472 -0
- package/dist/sandbox/image-build/dockerfile-analysis.d.ts +17 -0
- package/dist/sandbox/image-build/dockerfile-analysis.js +409 -0
- package/dist/sandbox/image-build/dockerignore.d.ts +18 -0
- package/dist/sandbox/image-build/dockerignore.js +275 -0
- package/dist/sandbox/image-build/gzip.d.ts +11 -0
- package/dist/sandbox/image-build/gzip.js +91 -0
- package/dist/sandbox/image-build/image-init.d.ts +5 -0
- package/dist/sandbox/image-build/image-init.js +97 -0
- package/dist/sandbox/image-build/index.d.ts +13 -0
- package/dist/sandbox/image-build/index.js +36 -0
- package/dist/sandbox/image-build/local-docker.d.ts +11 -0
- package/dist/sandbox/image-build/local-docker.js +72 -0
- package/dist/sandbox/image-build/resolution.d.ts +27 -0
- package/dist/sandbox/image-build/resolution.js +137 -0
- package/dist/sandbox/image-build/tar.d.ts +41 -0
- package/dist/sandbox/image-build/tar.js +378 -0
- package/dist/sandbox/image-build/upload.d.ts +10 -0
- package/dist/sandbox/image-build/upload.js +143 -0
- package/dist/sandbox/index.d.ts +2 -1
- package/dist/sandbox/index.js +4 -1
- package/dist/sandbox/process-output.d.ts +36 -0
- package/dist/sandbox/process-output.js +117 -0
- package/dist/sandbox/process.d.ts +25 -1
- package/dist/sandbox/process.js +197 -29
- package/dist/sandbox/terminal.js +9 -5
- package/dist/sandbox/ws.d.ts +4 -1
- package/dist/sandbox/ws.js +33 -7
- package/dist/services/agents/jev-computer-use.d.ts +30 -0
- package/dist/services/agents/jev-computer-use.js +103 -0
- package/dist/services/base.d.ts +2 -0
- package/dist/services/base.js +46 -30
- package/dist/services/normalize.d.ts +3 -0
- package/dist/services/normalize.js +21 -0
- package/dist/services/sandboxes.d.ts +40 -3
- package/dist/services/sandboxes.js +569 -53
- package/dist/services/volumes.js +29 -14
- package/dist/types/agents/jev-computer-use.d.ts +43 -0
- package/dist/types/agents/jev-computer-use.js +2 -0
- package/dist/types/constants.d.ts +5 -2
- package/dist/types/index.d.ts +4 -2
- package/dist/types/sandbox.d.ts +246 -30
- package/dist/types/volume.d.ts +7 -7
- package/package.json +24 -5
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** Remote Dockerfile build context selection, fingerprinting, and packaging. */
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
20
|
+
if (mod && mod.__esModule) return mod;
|
|
21
|
+
var result = {};
|
|
22
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
23
|
+
__setModuleDefault(result, mod);
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.packageDockerBuildContextManifest = exports.writeManifestArtifact = exports.canonicalManifestJson = exports.dockerBuildContextFingerprint = exports.collectContextEntries = exports.loadDockerignore = exports.DockerBuildContextChangedError = void 0;
|
|
28
|
+
const crypto_1 = require("crypto");
|
|
29
|
+
const fs_1 = require("fs");
|
|
30
|
+
const os_1 = require("os");
|
|
31
|
+
const path = __importStar(require("path"));
|
|
32
|
+
const artifacts_1 = require("./artifacts");
|
|
33
|
+
const common_1 = require("./common");
|
|
34
|
+
const dockerfile_analysis_1 = require("./dockerfile-analysis");
|
|
35
|
+
const dockerignore_1 = require("./dockerignore");
|
|
36
|
+
const gzip_1 = require("./gzip");
|
|
37
|
+
const MAX_CONTEXT_SOURCE_GROUPS = 511;
|
|
38
|
+
const MAX_CONTEXT_ENTRIES = 1000000;
|
|
39
|
+
/** The packaged context no longer matches the caller's cache fingerprint. */
|
|
40
|
+
class DockerBuildContextChangedError extends Error {
|
|
41
|
+
constructor(message) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "DockerBuildContextChangedError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.DockerBuildContextChangedError = DockerBuildContextChangedError;
|
|
47
|
+
const selectionFingerprint = (selection, bundleHashes) => {
|
|
48
|
+
// Hash entry metadata and contents, not transport encoding: cache identity
|
|
49
|
+
// must not depend on tar headers or a compression library.
|
|
50
|
+
const identity = {
|
|
51
|
+
version: 1,
|
|
52
|
+
dockerfile: selection.dockerfile,
|
|
53
|
+
contextMode: selection.mode,
|
|
54
|
+
bundles: Array.from(new Set(bundleHashes)).sort(common_1.compareCodePoints),
|
|
55
|
+
};
|
|
56
|
+
return (0, crypto_1.createHash)("sha256")
|
|
57
|
+
.update((0, common_1.compactJson)(identity, { sortKeys: true }))
|
|
58
|
+
.digest("hex");
|
|
59
|
+
};
|
|
60
|
+
const expandUser = (value) => {
|
|
61
|
+
if (value === "~" || value.startsWith("~/")) {
|
|
62
|
+
return path.join((0, os_1.homedir)(), value.slice(1));
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
};
|
|
66
|
+
const lstatOrNull = (target) => {
|
|
67
|
+
try {
|
|
68
|
+
return (0, fs_1.lstatSync)(target);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const statOrNull = (target) => {
|
|
75
|
+
try {
|
|
76
|
+
return (0, fs_1.statSync)(target);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const pathExistsOrSymlink = (target) => lstatOrNull(target) !== null;
|
|
83
|
+
const pathIsWithin = (parent, candidate) => {
|
|
84
|
+
const relative = path.relative(parent, candidate);
|
|
85
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
86
|
+
};
|
|
87
|
+
const cleanContextRelativePath = (value, fallback) => {
|
|
88
|
+
let normalized = (value ?? "").trim() || fallback;
|
|
89
|
+
normalized = normalized.split(path.sep).join("/");
|
|
90
|
+
if (normalized.includes("\\") ||
|
|
91
|
+
normalized.includes("\x00") ||
|
|
92
|
+
normalized.includes("\r") ||
|
|
93
|
+
normalized.includes("\n") ||
|
|
94
|
+
normalized.startsWith("/")) {
|
|
95
|
+
throw new Error("Dockerfile path must be relative to the build context");
|
|
96
|
+
}
|
|
97
|
+
if (normalized.split("/").includes("..")) {
|
|
98
|
+
throw new Error("Dockerfile path must be a relative path inside the build context");
|
|
99
|
+
}
|
|
100
|
+
const cleaned = (0, common_1.posixNormpath)(normalized);
|
|
101
|
+
if (cleaned === "." || cleaned === ".." || cleaned.startsWith("../")) {
|
|
102
|
+
throw new Error("Dockerfile path must be a relative path inside the build context");
|
|
103
|
+
}
|
|
104
|
+
return cleaned;
|
|
105
|
+
};
|
|
106
|
+
const selectDockerignore = (contextRoot, dockerfileRelative) => {
|
|
107
|
+
const dockerfileIgnore = `${dockerfileRelative}.dockerignore`;
|
|
108
|
+
const candidate = path.join(contextRoot, dockerfileIgnore);
|
|
109
|
+
if ((0, fs_1.existsSync)(candidate)) {
|
|
110
|
+
if (!(0, fs_1.statSync)(candidate).isFile()) {
|
|
111
|
+
throw new Error(`Dockerfile-specific ignore file "${dockerfileIgnore}" is not a regular file`);
|
|
112
|
+
}
|
|
113
|
+
return dockerfileIgnore;
|
|
114
|
+
}
|
|
115
|
+
return ".dockerignore";
|
|
116
|
+
};
|
|
117
|
+
const loadDockerignore = (ignorePath) => {
|
|
118
|
+
if (!(0, fs_1.existsSync)(ignorePath)) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
return dockerignore_1.DockerIgnoreMatcher.fromFile(ignorePath);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
throw new Error(`parse .dockerignore: ${error instanceof Error ? error.message : error}`);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
exports.loadDockerignore = loadDockerignore;
|
|
129
|
+
const normalizeContextSource = (source) => {
|
|
130
|
+
if (source.includes("\x00")) {
|
|
131
|
+
throw new Error("Dockerfile source path contains a NUL byte");
|
|
132
|
+
}
|
|
133
|
+
const normalized = String(source).trim().replace(/\\/g, "/");
|
|
134
|
+
if (normalized === "" || normalized === "." || normalized === "/") {
|
|
135
|
+
return ".";
|
|
136
|
+
}
|
|
137
|
+
return (0, common_1.posixNormpath)("/" + normalized).replace(/^\/+/, "") || ".";
|
|
138
|
+
};
|
|
139
|
+
const deduplicateSourceGroups = (groups) => {
|
|
140
|
+
const result = [];
|
|
141
|
+
const seen = new Set();
|
|
142
|
+
for (const group of groups) {
|
|
143
|
+
const normalized = group.map(normalizeContextSource).sort(common_1.compareCodePoints);
|
|
144
|
+
const key = JSON.stringify(normalized);
|
|
145
|
+
if (!seen.has(key)) {
|
|
146
|
+
seen.add(key);
|
|
147
|
+
result.push(normalized);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
};
|
|
152
|
+
const isIgnored = (relative, matcher) => matcher !== null && matcher.matches(relative);
|
|
153
|
+
const addContextEntryWithParents = (entries, relative) => {
|
|
154
|
+
let current = relative;
|
|
155
|
+
while (current !== "" && current !== ".") {
|
|
156
|
+
entries.add(current);
|
|
157
|
+
current = (0, common_1.posixDirname)(current);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const toRelative = (contextRoot, target) => {
|
|
161
|
+
const relative = path.relative(contextRoot, target).split(path.sep).join("/");
|
|
162
|
+
return relative === "" ? "." : relative;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Return a sparse source plus symlinks and their in-context targets.
|
|
166
|
+
*
|
|
167
|
+
* This mirrors fsutil `FollowPaths`: symlink targets are interpreted inside
|
|
168
|
+
* the context root, including absolute or `..` targets, and cycles terminate
|
|
169
|
+
* after retaining every symlink encountered.
|
|
170
|
+
*/
|
|
171
|
+
const followContextSourceSymlinks = (contextRoot, relativeSource) => {
|
|
172
|
+
const pending = [normalizeContextSource(relativeSource)];
|
|
173
|
+
const resolved = [];
|
|
174
|
+
const seen = new Set();
|
|
175
|
+
while (pending.length > 0) {
|
|
176
|
+
const relative = pending.pop();
|
|
177
|
+
if (seen.has(relative)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
seen.add(relative);
|
|
181
|
+
const parts = relative === "." ? [] : relative.split("/");
|
|
182
|
+
const currentParts = [];
|
|
183
|
+
let followed = false;
|
|
184
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
185
|
+
currentParts.push(parts[index]);
|
|
186
|
+
const currentRelative = currentParts.join("/");
|
|
187
|
+
const currentPath = path.join(contextRoot, currentRelative);
|
|
188
|
+
const metadata = lstatOrNull(currentPath);
|
|
189
|
+
if (metadata === null || !metadata.isSymbolicLink()) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
resolved.push(currentRelative);
|
|
193
|
+
let target;
|
|
194
|
+
try {
|
|
195
|
+
target = (0, fs_1.readlinkSync)(currentPath);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
throw new Error(`read build context symlink "${currentRelative}": ${error instanceof Error ? error.message : error}`);
|
|
199
|
+
}
|
|
200
|
+
if (!target || target.includes("\x00")) {
|
|
201
|
+
throw new Error(`build context symlink "${currentRelative}" has an invalid target`);
|
|
202
|
+
}
|
|
203
|
+
const remainder = parts.slice(index + 1);
|
|
204
|
+
const combined = target.startsWith("/")
|
|
205
|
+
? (0, common_1.posixJoin)(target, ...remainder)
|
|
206
|
+
: (0, common_1.posixJoin)((0, common_1.posixDirname)(currentRelative), target, ...remainder);
|
|
207
|
+
const normalizedTarget = (0, common_1.posixNormpath)("/" + combined).replace(/^\/+/, "");
|
|
208
|
+
pending.push(normalizedTarget || ".");
|
|
209
|
+
followed = true;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
if (!followed) {
|
|
213
|
+
resolved.push(relative);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return resolved;
|
|
217
|
+
};
|
|
218
|
+
const walkDirectory = (contextRoot, directory, entries, ignoreMatcher, canPruneIgnoredDirectories) => {
|
|
219
|
+
const children = (0, fs_1.readdirSync)(directory, { withFileTypes: true });
|
|
220
|
+
const directories = children
|
|
221
|
+
.filter((child) => child.isDirectory())
|
|
222
|
+
.map((child) => child.name)
|
|
223
|
+
.sort(common_1.compareCodePoints);
|
|
224
|
+
const files = children
|
|
225
|
+
.filter((child) => !child.isDirectory())
|
|
226
|
+
.map((child) => child.name)
|
|
227
|
+
.sort(common_1.compareCodePoints);
|
|
228
|
+
const retainedDirectories = [];
|
|
229
|
+
for (const name of directories) {
|
|
230
|
+
const childRelative = toRelative(contextRoot, path.join(directory, name));
|
|
231
|
+
if (isIgnored(childRelative, ignoreMatcher)) {
|
|
232
|
+
if (!canPruneIgnoredDirectories) {
|
|
233
|
+
retainedDirectories.push(name);
|
|
234
|
+
}
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
addContextEntryWithParents(entries, childRelative);
|
|
238
|
+
retainedDirectories.push(name);
|
|
239
|
+
}
|
|
240
|
+
for (const name of files) {
|
|
241
|
+
const childRelative = toRelative(contextRoot, path.join(directory, name));
|
|
242
|
+
if (isIgnored(childRelative, ignoreMatcher)) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
addContextEntryWithParents(entries, childRelative);
|
|
246
|
+
}
|
|
247
|
+
for (const name of retainedDirectories) {
|
|
248
|
+
walkDirectory(contextRoot, path.join(directory, name), entries, ignoreMatcher, canPruneIgnoredDirectories);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
const collectContextPath = (contextRoot, target, entries, ignoreMatcher) => {
|
|
252
|
+
const relative = toRelative(contextRoot, target);
|
|
253
|
+
const pathIsIgnored = relative !== "." && isIgnored(relative, ignoreMatcher);
|
|
254
|
+
const canPruneIgnoredDirectories = !(ignoreMatcher?.hasNegations ?? false);
|
|
255
|
+
if (relative !== "." && !pathIsIgnored) {
|
|
256
|
+
addContextEntryWithParents(entries, relative);
|
|
257
|
+
}
|
|
258
|
+
const metadata = lstatOrNull(target);
|
|
259
|
+
if (metadata === null || metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (pathIsIgnored && canPruneIgnoredDirectories) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
walkDirectory(contextRoot, target, entries, ignoreMatcher, canPruneIgnoredDirectories);
|
|
266
|
+
};
|
|
267
|
+
const collectContextEntries = (contextRoot, sources, options) => {
|
|
268
|
+
const entries = new Set();
|
|
269
|
+
for (const rawSource of sources) {
|
|
270
|
+
const source = normalizeContextSource(rawSource);
|
|
271
|
+
const candidate = source === "." ? contextRoot : path.join(contextRoot, source);
|
|
272
|
+
const existingMatches = pathExistsOrSymlink(candidate) ? [candidate] : [];
|
|
273
|
+
if (options.required && existingMatches.length === 0) {
|
|
274
|
+
throw new Error(`Docker build context source not found: "${source}"`);
|
|
275
|
+
}
|
|
276
|
+
for (const match of existingMatches) {
|
|
277
|
+
const relativeMatch = toRelative(contextRoot, match);
|
|
278
|
+
for (const followedRelative of followContextSourceSymlinks(contextRoot, relativeMatch)) {
|
|
279
|
+
const followedPath = followedRelative === "." ? contextRoot : path.join(contextRoot, followedRelative);
|
|
280
|
+
if (pathExistsOrSymlink(followedPath)) {
|
|
281
|
+
collectContextPath(contextRoot, followedPath, entries, options.ignoreMatcher);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return entries;
|
|
287
|
+
};
|
|
288
|
+
exports.collectContextEntries = collectContextEntries;
|
|
289
|
+
const isSubset = (left, right) => {
|
|
290
|
+
for (const item of left) {
|
|
291
|
+
if (!right.has(item)) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return true;
|
|
296
|
+
};
|
|
297
|
+
const removeSubsumedEntryGroups = (groups) => {
|
|
298
|
+
const result = [];
|
|
299
|
+
groups.forEach((entries, index) => {
|
|
300
|
+
const subsumed = groups.some((candidate, candidateIndex) => {
|
|
301
|
+
if (index === candidateIndex || !isSubset(entries, candidate)) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
const equal = entries.size === candidate.size;
|
|
305
|
+
return !equal || index > candidateIndex;
|
|
306
|
+
});
|
|
307
|
+
if (!subsumed) {
|
|
308
|
+
result.push(entries);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
return result;
|
|
312
|
+
};
|
|
313
|
+
const selectDockerBuildContext = (contextPath, options) => {
|
|
314
|
+
const dockerfile = options.dockerfile ?? "Dockerfile";
|
|
315
|
+
const contextRoot = (0, fs_1.realpathSync)(path.resolve(expandUser(contextPath)));
|
|
316
|
+
if (!(0, fs_1.statSync)(contextRoot).isDirectory()) {
|
|
317
|
+
throw new Error("Docker build context must be a directory");
|
|
318
|
+
}
|
|
319
|
+
const dockerfileRelative = cleanContextRelativePath(dockerfile, "Dockerfile");
|
|
320
|
+
const dockerfilePath = path.join(contextRoot, dockerfileRelative);
|
|
321
|
+
const dockerfileMetadata = lstatOrNull(dockerfilePath);
|
|
322
|
+
const dockerfileStat = statOrNull(dockerfilePath);
|
|
323
|
+
if (dockerfileMetadata === null ||
|
|
324
|
+
!((dockerfileStat?.isFile() ?? false) || dockerfileMetadata.isSymbolicLink())) {
|
|
325
|
+
throw new Error(`Dockerfile "${dockerfileRelative}" must be a regular file or symlink`);
|
|
326
|
+
}
|
|
327
|
+
let resolvedDockerfile;
|
|
328
|
+
try {
|
|
329
|
+
resolvedDockerfile = (0, fs_1.realpathSync)(dockerfilePath);
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
throw new Error(`Dockerfile "${dockerfileRelative}" must resolve to a regular file inside the build context`);
|
|
333
|
+
}
|
|
334
|
+
if (!(0, fs_1.statSync)(resolvedDockerfile).isFile() || !pathIsWithin(contextRoot, resolvedDockerfile)) {
|
|
335
|
+
throw new Error(`Dockerfile "${dockerfileRelative}" must resolve to a regular file inside the build context`);
|
|
336
|
+
}
|
|
337
|
+
const dockerfileBytes = (0, fs_1.readFileSync)(dockerfilePath);
|
|
338
|
+
const ignoreRelative = selectDockerignore(contextRoot, dockerfileRelative);
|
|
339
|
+
const ignoreMatcher = (0, exports.loadDockerignore)(path.join(contextRoot, ignoreRelative));
|
|
340
|
+
let { groups: sourceGroups, fallbackReason } = (0, dockerfile_analysis_1.analyzeDockerfileSources)(dockerfileBytes);
|
|
341
|
+
if (options.forceFullContext) {
|
|
342
|
+
sourceGroups = [];
|
|
343
|
+
fallbackReason = "requested_full_context";
|
|
344
|
+
}
|
|
345
|
+
sourceGroups = deduplicateSourceGroups(sourceGroups);
|
|
346
|
+
if (sourceGroups.length > MAX_CONTEXT_SOURCE_GROUPS) {
|
|
347
|
+
sourceGroups = [];
|
|
348
|
+
fallbackReason = "too_many_context_source_groups";
|
|
349
|
+
}
|
|
350
|
+
const usesWholeContext = sourceGroups.some((group) => group.includes("."));
|
|
351
|
+
let contextMode = fallbackReason || usesWholeContext ? "full" : "sparse";
|
|
352
|
+
const controlSources = [
|
|
353
|
+
dockerfileRelative,
|
|
354
|
+
ignoreRelative,
|
|
355
|
+
toRelative(contextRoot, resolvedDockerfile),
|
|
356
|
+
];
|
|
357
|
+
const collectFullContext = () => [
|
|
358
|
+
new Set([
|
|
359
|
+
...(0, exports.collectContextEntries)(contextRoot, ["."], { ignoreMatcher, required: false }),
|
|
360
|
+
...(0, exports.collectContextEntries)(contextRoot, controlSources, {
|
|
361
|
+
ignoreMatcher: null,
|
|
362
|
+
required: false,
|
|
363
|
+
}),
|
|
364
|
+
]),
|
|
365
|
+
];
|
|
366
|
+
let entryGroups;
|
|
367
|
+
if (contextMode === "full") {
|
|
368
|
+
entryGroups = collectFullContext();
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
entryGroups = [
|
|
372
|
+
(0, exports.collectContextEntries)(contextRoot, controlSources, { ignoreMatcher: null, required: false }),
|
|
373
|
+
];
|
|
374
|
+
for (const group of sourceGroups) {
|
|
375
|
+
entryGroups.push((0, exports.collectContextEntries)(contextRoot, group, { ignoreMatcher, required: true }));
|
|
376
|
+
}
|
|
377
|
+
const total = entryGroups.reduce((sum, entries) => sum + entries.size, 0);
|
|
378
|
+
if (total > MAX_CONTEXT_ENTRIES) {
|
|
379
|
+
contextMode = "full";
|
|
380
|
+
fallbackReason = "context_selection_too_large";
|
|
381
|
+
entryGroups = collectFullContext();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
root: contextRoot,
|
|
386
|
+
dockerfile: dockerfileRelative,
|
|
387
|
+
mode: contextMode,
|
|
388
|
+
fallbackReason: fallbackReason || null,
|
|
389
|
+
entryGroups: removeSubsumedEntryGroups(entryGroups),
|
|
390
|
+
};
|
|
391
|
+
};
|
|
392
|
+
const validateArchiveRelativePath = (relative) => {
|
|
393
|
+
if (!relative ||
|
|
394
|
+
relative.startsWith("/") ||
|
|
395
|
+
relative.includes("\x00") ||
|
|
396
|
+
(0, common_1.posixNormpath)(relative) !== relative ||
|
|
397
|
+
relative === ".." ||
|
|
398
|
+
relative.startsWith("../")) {
|
|
399
|
+
throw new Error(`invalid build context path "${relative}"`);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
const contextEntryInfo = (contextRoot, relative) => {
|
|
403
|
+
validateArchiveRelativePath(relative);
|
|
404
|
+
const absolute = path.join(contextRoot, relative);
|
|
405
|
+
// Every regular path must be present with its own contents (no hard-link
|
|
406
|
+
// entries); preserve symlinks without following them.
|
|
407
|
+
const metadata = (0, fs_1.lstatSync)(absolute);
|
|
408
|
+
const mode = metadata.mode & 0o7777;
|
|
409
|
+
if (metadata.isFile()) {
|
|
410
|
+
return { name: relative, type: "file", mode, size: metadata.size, linkname: "" };
|
|
411
|
+
}
|
|
412
|
+
if (metadata.isDirectory()) {
|
|
413
|
+
return { name: `${relative}/`, type: "directory", mode, size: 0, linkname: "" };
|
|
414
|
+
}
|
|
415
|
+
if (metadata.isSymbolicLink()) {
|
|
416
|
+
const linkname = (0, fs_1.readlinkSync)(absolute);
|
|
417
|
+
if (!linkname) {
|
|
418
|
+
throw new Error(`build context symlink "${relative}" has an invalid target`);
|
|
419
|
+
}
|
|
420
|
+
return { name: relative, type: "symlink", mode, size: 0, linkname };
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
};
|
|
424
|
+
const hashContextEntryMetadata = (hasher, info) => {
|
|
425
|
+
const metadata = Buffer.from((0, common_1.compactJson)([
|
|
426
|
+
info.type === "directory" ? info.name.replace(/\/+$/, "") : info.name,
|
|
427
|
+
info.type,
|
|
428
|
+
info.mode,
|
|
429
|
+
info.size,
|
|
430
|
+
info.linkname,
|
|
431
|
+
]), "utf8");
|
|
432
|
+
const length = Buffer.alloc(8);
|
|
433
|
+
length.writeBigUInt64BE(BigInt(metadata.length));
|
|
434
|
+
hasher.update(length);
|
|
435
|
+
hasher.update(metadata);
|
|
436
|
+
};
|
|
437
|
+
async function* hashingFileChunks(filePath, hasher, size) {
|
|
438
|
+
let remaining = size;
|
|
439
|
+
if (remaining === 0) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const stream = (0, fs_1.createReadStream)(filePath, { highWaterMark: 64 * 1024 });
|
|
443
|
+
try {
|
|
444
|
+
for await (const chunk of stream) {
|
|
445
|
+
const buffer = chunk;
|
|
446
|
+
if (buffer.length > remaining) {
|
|
447
|
+
throw new Error(`build context file "${filePath}" changed size while reading`);
|
|
448
|
+
}
|
|
449
|
+
remaining -= buffer.length;
|
|
450
|
+
hasher.update(buffer);
|
|
451
|
+
yield buffer;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
finally {
|
|
455
|
+
stream.destroy();
|
|
456
|
+
}
|
|
457
|
+
if (remaining !== 0) {
|
|
458
|
+
throw new Error(`build context file "${filePath}" was truncated`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Fingerprint the effective remote context without compressing or staging it.
|
|
463
|
+
*
|
|
464
|
+
* Uses the same Dockerfile source selection, ignore rules, and normalized tar
|
|
465
|
+
* entries as remote packaging. Pass the result as `expectedContextFingerprint`
|
|
466
|
+
* when building to detect context changes. Build options outside the context
|
|
467
|
+
* (e.g. platform or imageInit) must also be included in the caller's cache
|
|
468
|
+
* key. Mutable base tags and network resources are not resolved.
|
|
469
|
+
*/
|
|
470
|
+
const dockerBuildContextFingerprint = async (contextPath, options = {}) => {
|
|
471
|
+
const selection = selectDockerBuildContext(contextPath, options);
|
|
472
|
+
const hashes = [];
|
|
473
|
+
for (const entries of selection.entryGroups) {
|
|
474
|
+
const hasher = (0, crypto_1.createHash)("sha256");
|
|
475
|
+
for (const relative of Array.from(entries).sort(common_1.compareCodePoints)) {
|
|
476
|
+
const info = contextEntryInfo(selection.root, relative);
|
|
477
|
+
if (info === null) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
hashContextEntryMetadata(hasher, info);
|
|
481
|
+
if (info.type === "file") {
|
|
482
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
483
|
+
for await (const _chunk of hashingFileChunks(path.join(selection.root, relative), hasher, info.size)) {
|
|
484
|
+
// hashing only
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
hashes.push(hasher.digest("hex"));
|
|
489
|
+
}
|
|
490
|
+
return selectionFingerprint(selection, hashes);
|
|
491
|
+
};
|
|
492
|
+
exports.dockerBuildContextFingerprint = dockerBuildContextFingerprint;
|
|
493
|
+
const packageContextBundle = async (contextRoot, entries, workspace, index) => {
|
|
494
|
+
const bundlePath = path.join(workspace, `bundle-${String(index).padStart(4, "0")}.tar.gz`);
|
|
495
|
+
const contextHasher = (0, crypto_1.createHash)("sha256");
|
|
496
|
+
let entryCount = 0;
|
|
497
|
+
let uncompressedSize = 0;
|
|
498
|
+
const result = await (0, gzip_1.writeGzipTar)(bundlePath, async (writer) => {
|
|
499
|
+
for (const relative of entries) {
|
|
500
|
+
const info = contextEntryInfo(contextRoot, relative);
|
|
501
|
+
if (info === null) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
hashContextEntryMetadata(contextHasher, info);
|
|
505
|
+
if (info.type === "file") {
|
|
506
|
+
await writer.addEntry(info, hashingFileChunks(path.join(contextRoot, relative), contextHasher, info.size));
|
|
507
|
+
uncompressedSize += info.size;
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
await writer.addEntry(info);
|
|
511
|
+
}
|
|
512
|
+
entryCount += 1;
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
const artifact = {
|
|
516
|
+
path: bundlePath,
|
|
517
|
+
sha256Hex: result.sha256Hex,
|
|
518
|
+
sizeBytes: result.sizeBytes,
|
|
519
|
+
inputFormat: artifacts_1.CONTEXT_MANIFEST_INPUT_FORMAT,
|
|
520
|
+
sourcePlatform: artifacts_1.IMAGE_BUILD_SOURCE_PLATFORM,
|
|
521
|
+
imageConfigUser: "",
|
|
522
|
+
};
|
|
523
|
+
const descriptor = {
|
|
524
|
+
sha256: result.sha256Hex,
|
|
525
|
+
sizeBytes: result.sizeBytes,
|
|
526
|
+
uncompressedSizeBytes: uncompressedSize,
|
|
527
|
+
entryCount,
|
|
528
|
+
};
|
|
529
|
+
return [artifact, descriptor, contextHasher.digest("hex")];
|
|
530
|
+
};
|
|
531
|
+
/** Serialize a manifest exactly as the Python SDK does (field order, no nulls). */
|
|
532
|
+
const canonicalManifestJson = (value) => Buffer.from((0, common_1.compactJson)(value, { ensureAscii: false }), "utf8");
|
|
533
|
+
exports.canonicalManifestJson = canonicalManifestJson;
|
|
534
|
+
const writeManifestArtifact = (workspace, filename, data, inputFormat, extras = {
|
|
535
|
+
imageConfigUser: "",
|
|
536
|
+
}) => {
|
|
537
|
+
const filePath = path.join(workspace, filename);
|
|
538
|
+
(0, fs_1.writeFileSync)(filePath, data, { flag: "wx" });
|
|
539
|
+
return {
|
|
540
|
+
path: filePath,
|
|
541
|
+
sha256Hex: (0, crypto_1.createHash)("sha256").update(data).digest("hex"),
|
|
542
|
+
sizeBytes: data.length,
|
|
543
|
+
inputFormat,
|
|
544
|
+
sourcePlatform: artifacts_1.IMAGE_BUILD_SOURCE_PLATFORM,
|
|
545
|
+
imageConfigUser: extras.imageConfigUser,
|
|
546
|
+
imageInit: extras.imageInit,
|
|
547
|
+
};
|
|
548
|
+
};
|
|
549
|
+
exports.writeManifestArtifact = writeManifestArtifact;
|
|
550
|
+
const packageDockerBuildContextManifest = async (contextPath, options = {}) => {
|
|
551
|
+
const expected = options.expectedContextFingerprint;
|
|
552
|
+
if (expected !== undefined && !common_1.SHA256_HEX_PATTERN.test(expected)) {
|
|
553
|
+
throw new Error("expectedContextFingerprint must be a SHA-256 hex digest");
|
|
554
|
+
}
|
|
555
|
+
const selection = selectDockerBuildContext(contextPath, options);
|
|
556
|
+
const workspace = (0, fs_1.mkdtempSync)(path.join(options.tempDir ?? (0, os_1.tmpdir)(), "hb-docker-context-"));
|
|
557
|
+
try {
|
|
558
|
+
const bundles = {};
|
|
559
|
+
const descriptors = [];
|
|
560
|
+
const bundleHashes = [];
|
|
561
|
+
for (const [index, entries] of selection.entryGroups.entries()) {
|
|
562
|
+
const [artifact, descriptor, bundleHash] = await packageContextBundle(selection.root, Array.from(entries).sort(common_1.compareCodePoints), workspace, index);
|
|
563
|
+
bundleHashes.push(bundleHash);
|
|
564
|
+
if (descriptor.sha256 in bundles) {
|
|
565
|
+
(0, artifacts_1.removeArtifact)(artifact);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
bundles[descriptor.sha256] = artifact;
|
|
569
|
+
descriptors.push(descriptor);
|
|
570
|
+
}
|
|
571
|
+
descriptors.sort((left, right) => (0, common_1.compareCodePoints)(left.sha256, right.sha256));
|
|
572
|
+
const fingerprint = selectionFingerprint(selection, bundleHashes);
|
|
573
|
+
if (expected !== undefined && fingerprint !== expected) {
|
|
574
|
+
throw new DockerBuildContextChangedError("Docker build context changed after its cache fingerprint was computed. " +
|
|
575
|
+
"Retry the build with a fresh fingerprint.");
|
|
576
|
+
}
|
|
577
|
+
const manifest = {
|
|
578
|
+
version: 1,
|
|
579
|
+
dockerfilePath: selection.dockerfile,
|
|
580
|
+
contextMode: selection.mode,
|
|
581
|
+
...(selection.fallbackReason ? { fallbackReason: selection.fallbackReason } : {}),
|
|
582
|
+
bundles: descriptors,
|
|
583
|
+
};
|
|
584
|
+
const artifact = (0, exports.writeManifestArtifact)(workspace, "context-manifest.json", (0, exports.canonicalManifestJson)(manifest), artifacts_1.CONTEXT_MANIFEST_INPUT_FORMAT);
|
|
585
|
+
return {
|
|
586
|
+
artifact,
|
|
587
|
+
manifest,
|
|
588
|
+
bundles,
|
|
589
|
+
workspace,
|
|
590
|
+
fingerprint,
|
|
591
|
+
cleanup: () => (0, artifacts_1.removeWorkspace)(workspace),
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
(0, artifacts_1.removeWorkspace)(workspace);
|
|
596
|
+
throw error;
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
exports.packageDockerBuildContextManifest = packageDockerBuildContextManifest;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Local Docker image inspection and layer-manifest packaging for prebuilt imports. */
|
|
2
|
+
import { SandboxDockerImageManifest, SandboxImageInit } from "../../types/sandbox";
|
|
3
|
+
import { DockerImageBuildArtifact } from "./artifacts";
|
|
4
|
+
export interface DockerImageManifestSource {
|
|
5
|
+
imageDigest: string;
|
|
6
|
+
config: Record<string, unknown>;
|
|
7
|
+
imageConfigUser: string;
|
|
8
|
+
imageInit?: SandboxImageInit;
|
|
9
|
+
cleanup(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export interface PackagedDockerImage {
|
|
12
|
+
artifact: DockerImageBuildArtifact;
|
|
13
|
+
manifest: SandboxDockerImageManifest;
|
|
14
|
+
layers: Record<string, DockerImageBuildArtifact>;
|
|
15
|
+
workspace: string;
|
|
16
|
+
cleanup(): void;
|
|
17
|
+
}
|
|
18
|
+
export declare class DockerCommandError extends Error {
|
|
19
|
+
constructor(message: string);
|
|
20
|
+
}
|
|
21
|
+
export declare const runDockerCommand: (args: string[]) => Promise<string>;
|
|
22
|
+
/** Inspect platform identity with Docker API 1.49+, without temporary resources. */
|
|
23
|
+
export declare const dockerImageDigest: (dockerImage: string, options?: {
|
|
24
|
+
platform?: string;
|
|
25
|
+
}) => Promise<string>;
|
|
26
|
+
export declare const prepareDockerImageManifestSource: (dockerImage: string, options?: {
|
|
27
|
+
platform?: string;
|
|
28
|
+
}) => Promise<DockerImageManifestSource>;
|
|
29
|
+
export interface PackageDockerImageManifestOptions {
|
|
30
|
+
platform?: string;
|
|
31
|
+
tempDir?: string;
|
|
32
|
+
}
|
|
33
|
+
/** Stream `docker image save` into verified layer artifacts plus a manifest. */
|
|
34
|
+
export declare const packageDockerImageManifest: (dockerImage: string, imageDigest: string, config: Record<string, unknown>, options?: PackageDockerImageManifestOptions) => Promise<PackagedDockerImage>;
|