@lazycatcloud/lzc-cli 2.0.0-pre.0 → 2.0.0-pre.2
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 +46 -7
- package/changelog.md +56 -19
- package/lib/app/apkshell.js +7 -44
- package/lib/app/index.js +5 -1
- package/lib/app/lpk_build.js +266 -56
- package/lib/app/lpk_build_images.js +424 -229
- package/lib/app/lpk_build_images_local.js +425 -0
- package/lib/app/lpk_build_images_pack_local.js +409 -0
- package/lib/app/lpk_create.js +158 -83
- package/lib/app/lpk_create_generator.js +35 -42
- package/lib/app/lpk_devshell.js +6 -2
- package/lib/app/lpk_installer.js +4 -3
- package/lib/app/manifest_build.js +259 -0
- package/lib/app/project_cp.js +5 -10
- package/lib/app/project_deploy.js +80 -11
- package/lib/app/project_exec.js +48 -11
- package/lib/app/project_info.js +59 -59
- package/lib/app/project_log.js +5 -10
- package/lib/app/project_runtime.js +113 -18
- package/lib/app/project_start.js +6 -11
- package/lib/app/project_sync.js +499 -0
- package/lib/appstore/apkshell.js +50 -0
- package/lib/appstore/publish.js +54 -15
- package/lib/build_remote.js +0 -1
- package/lib/config/index.js +1 -1
- package/lib/debug_bridge.js +217 -47
- package/lib/i18n/locales/en/translation.json +262 -262
- package/lib/i18n/locales/zh/translation.json +262 -262
- package/lib/lpk/core.js +2 -1
- package/lib/migrate/index.js +52 -0
- package/lib/package_info.js +135 -0
- package/lib/shellapi.js +35 -1
- package/lib/sig/core.js +2 -2
- package/lib/utils.js +92 -15
- package/package.json +89 -89
- package/scripts/cli.js +2 -0
- package/scripts/smoke/frontend-dev-entry.mjs +104 -0
- package/scripts/smoke/template-project.mjs +311 -0
- package/template/_lpk/README.md +6 -3
- package/template/_lpk/gui-vnc.manifest.yml.in +0 -9
- package/template/_lpk/hello-vue.manifest.yml.in +38 -0
- package/template/_lpk/manifest.yml.in +0 -9
- package/template/_lpk/package.yml.in +7 -0
- package/template/_lpk/todolist-golang.manifest.yml.in +23 -9
- package/template/_lpk/todolist-java.manifest.yml.in +23 -9
- package/template/_lpk/todolist-python.manifest.yml.in +31 -9
- package/template/_lpk/todolist-serverless.manifest.yml.in +38 -0
- package/template/blank/lzc-build.dev.yml +4 -0
- package/template/blank/lzc-build.yml +0 -2
- package/template/blank/lzc-manifest.yml +3 -12
- package/template/blank/package.yml +7 -0
- package/template/golang/Dockerfile +1 -1
- package/template/golang/Dockerfile.dev +20 -0
- package/template/golang/README.md +22 -11
- package/template/golang/_lzcdevignore +21 -0
- package/template/golang/lzc-build.dev.yml +12 -0
- package/template/golang/lzc-build.yml +0 -5
- package/template/golang/main.go +1 -1
- package/template/golang/manifest.dev.page.js +24 -0
- package/template/golang/run.sh +7 -0
- package/template/gui-vnc/README.md +5 -1
- package/template/gui-vnc/lzc-build.dev.yml +4 -0
- package/template/gui-vnc/lzc-build.yml +0 -5
- package/template/python/Dockerfile +2 -2
- package/template/python/Dockerfile.dev +18 -0
- package/template/python/README.md +28 -11
- package/template/python/_lzcdevignore +21 -0
- package/template/python/app.py +1 -1
- package/template/python/lzc-build.dev.yml +12 -0
- package/template/python/lzc-build.yml +0 -5
- package/template/python/manifest.dev.page.js +25 -0
- package/template/python/run.sh +12 -1
- package/template/springboot/Dockerfile +1 -1
- package/template/springboot/Dockerfile.dev +20 -0
- package/template/springboot/README.md +22 -11
- package/template/springboot/_lzcdevignore +21 -0
- package/template/springboot/lzc-build.dev.yml +12 -0
- package/template/springboot/lzc-build.yml +0 -5
- package/template/springboot/manifest.dev.page.js +24 -0
- package/template/springboot/run.sh +7 -0
- package/template/vue/README.md +14 -27
- package/template/vue/_gitignore +0 -1
- package/template/vue/lzc-build.dev.yml +7 -0
- package/template/vue/lzc-build.yml +0 -2
- package/template/vue/manifest.dev.page.js +50 -0
- package/template/vue/src/App.vue +1 -1
- package/template/vue-minidb/README.md +11 -19
- package/template/vue-minidb/_gitignore +0 -1
- package/template/vue-minidb/lzc-build.dev.yml +7 -0
- package/template/vue-minidb/lzc-build.yml +0 -2
- package/template/vue-minidb/manifest.dev.page.js +50 -0
- package/template/blank/_gitignore +0 -1
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import zlib from 'node:zlib';
|
|
6
|
+
import { Transform } from 'node:stream';
|
|
7
|
+
import { pipeline } from 'node:stream/promises';
|
|
8
|
+
import * as tar from 'tar';
|
|
9
|
+
|
|
10
|
+
const SHA256_PREFIX = 'sha256:';
|
|
11
|
+
const MAX_INLINE_CONFIG_BLOB_SIZE = 2 * 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
function normalizeSha256Digest(value, fieldName) {
|
|
14
|
+
const raw = String(value ?? '').trim();
|
|
15
|
+
if (!raw.startsWith(SHA256_PREFIX)) {
|
|
16
|
+
throw new Error(`${fieldName} must start with sha256:, got: ${raw}`);
|
|
17
|
+
}
|
|
18
|
+
const hex = raw.slice(SHA256_PREFIX.length);
|
|
19
|
+
if (!/^[0-9a-f]{64}$/i.test(hex)) {
|
|
20
|
+
throw new Error(`${fieldName} is not a valid sha256 digest: ${raw}`);
|
|
21
|
+
}
|
|
22
|
+
return `${SHA256_PREFIX}${hex.toLowerCase()}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeArchivePath(name) {
|
|
26
|
+
return String(name ?? '').trim().replace(/^\.\//, '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isRegularEntry(entry) {
|
|
30
|
+
return entry && (entry.type === 'File' || entry.type === 'OldFile' || entry.type === 'ContiguousFile');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function readEntryBuffer(entry) {
|
|
34
|
+
const chunks = [];
|
|
35
|
+
for await (const chunk of entry) {
|
|
36
|
+
chunks.push(chunk);
|
|
37
|
+
}
|
|
38
|
+
return Buffer.concat(chunks);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function walkDockerArchive(archivePath, onEntry) {
|
|
42
|
+
const tasks = [];
|
|
43
|
+
await tar.t({
|
|
44
|
+
file: archivePath,
|
|
45
|
+
onReadEntry: (entry) => {
|
|
46
|
+
const task = (async () => {
|
|
47
|
+
if (!isRegularEntry(entry)) {
|
|
48
|
+
entry.resume();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
await onEntry(entry);
|
|
52
|
+
})();
|
|
53
|
+
tasks.push(task);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
await Promise.all(tasks);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function collectArchiveManifestAndEntrySizes(archivePath) {
|
|
60
|
+
const entrySizeByPath = {};
|
|
61
|
+
const jsonFileByPath = {};
|
|
62
|
+
let manifestRaw = null;
|
|
63
|
+
|
|
64
|
+
await walkDockerArchive(archivePath, async (entry) => {
|
|
65
|
+
const name = normalizeArchivePath(entry.path);
|
|
66
|
+
entrySizeByPath[name] = Number(entry.size ?? 0);
|
|
67
|
+
const shouldRead = name === 'manifest.json' || name.endsWith('.json') || (name.startsWith('blobs/sha256/') && Number(entry.size ?? 0) <= MAX_INLINE_CONFIG_BLOB_SIZE);
|
|
68
|
+
if (!shouldRead) {
|
|
69
|
+
entry.resume();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const raw = await readEntryBuffer(entry);
|
|
73
|
+
jsonFileByPath[name] = raw;
|
|
74
|
+
if (name === 'manifest.json') {
|
|
75
|
+
manifestRaw = raw;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!manifestRaw) {
|
|
80
|
+
throw new Error('manifest.json not found in docker archive');
|
|
81
|
+
}
|
|
82
|
+
return { manifestRaw, entrySizeByPath, jsonFileByPath };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function readArchiveFiles(archivePath, wantedSet) {
|
|
86
|
+
const files = {};
|
|
87
|
+
if (!wantedSet || wantedSet.size === 0) {
|
|
88
|
+
return files;
|
|
89
|
+
}
|
|
90
|
+
await walkDockerArchive(archivePath, async (entry) => {
|
|
91
|
+
const name = normalizeArchivePath(entry.path);
|
|
92
|
+
if (!wantedSet.has(name)) {
|
|
93
|
+
entry.resume();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
files[name] = await readEntryBuffer(entry);
|
|
97
|
+
});
|
|
98
|
+
for (const name of wantedSet) {
|
|
99
|
+
if (!Object.prototype.hasOwnProperty.call(files, name)) {
|
|
100
|
+
throw new Error(`entry ${name} not found in docker archive`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return files;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function copyBytesToBlob(content, blobsDir) {
|
|
107
|
+
const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content);
|
|
108
|
+
const digest = crypto.createHash('sha256').update(buffer).digest('hex');
|
|
109
|
+
const targetPath = path.join(blobsDir, digest);
|
|
110
|
+
if (!fs.existsSync(targetPath)) {
|
|
111
|
+
fs.writeFileSync(targetPath, buffer);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
digest,
|
|
115
|
+
size: buffer.length,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function gzipEntryToBlob(entry, blobsDir) {
|
|
120
|
+
const tmpDir = fs.mkdtempSync(path.join(blobsDir, '.lzc-cli-embed-layer-'));
|
|
121
|
+
const tmpPath = path.join(tmpDir, 'layer.tar.gz');
|
|
122
|
+
const out = fs.createWriteStream(tmpPath);
|
|
123
|
+
const hash = crypto.createHash('sha256');
|
|
124
|
+
let size = 0;
|
|
125
|
+
const tee = new Transform({
|
|
126
|
+
transform(chunk, _encoding, callback) {
|
|
127
|
+
hash.update(chunk);
|
|
128
|
+
size += chunk.length;
|
|
129
|
+
callback(null, chunk);
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
try {
|
|
133
|
+
await pipeline(entry, zlib.createGzip({ mtime: 0 }), tee, out);
|
|
134
|
+
const digest = hash.digest('hex');
|
|
135
|
+
const targetPath = path.join(blobsDir, digest);
|
|
136
|
+
if (!fs.existsSync(targetPath)) {
|
|
137
|
+
fs.renameSync(tmpPath, targetPath);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
digest,
|
|
141
|
+
size,
|
|
142
|
+
};
|
|
143
|
+
} finally {
|
|
144
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function compactLockLayers(rawLayers) {
|
|
149
|
+
if (!Array.isArray(rawLayers)) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
return rawLayers
|
|
153
|
+
.map((item) => ({
|
|
154
|
+
digest: String(item?.digest ?? '').trim(),
|
|
155
|
+
source: String(item?.source ?? '').trim().toLowerCase(),
|
|
156
|
+
}))
|
|
157
|
+
.filter((item) => item.digest);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function writeOciLayout(stageDir, state) {
|
|
161
|
+
const imageRootDir = path.join(stageDir, 'images');
|
|
162
|
+
const blobsDir = path.join(imageRootDir, 'blobs', 'sha256');
|
|
163
|
+
fs.mkdirSync(blobsDir, { recursive: true });
|
|
164
|
+
state.indexDescriptors.sort((a, b) => String(a?.annotations?.['org.opencontainers.image.ref.name'] ?? '').localeCompare(String(b?.annotations?.['org.opencontainers.image.ref.name'] ?? '')));
|
|
165
|
+
fs.writeFileSync(path.join(imageRootDir, 'oci-layout'), JSON.stringify({ imageLayoutVersion: '1.0.0' }));
|
|
166
|
+
fs.writeFileSync(
|
|
167
|
+
path.join(imageRootDir, 'index.json'),
|
|
168
|
+
JSON.stringify(
|
|
169
|
+
{
|
|
170
|
+
schemaVersion: 2,
|
|
171
|
+
manifests: state.indexDescriptors,
|
|
172
|
+
},
|
|
173
|
+
null,
|
|
174
|
+
2,
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
fs.writeFileSync(
|
|
178
|
+
path.join(stageDir, 'pack-result.json'),
|
|
179
|
+
JSON.stringify({
|
|
180
|
+
lockImages: state.lockImages,
|
|
181
|
+
embeddedLayerBytes: state.embeddedLayerBytes,
|
|
182
|
+
embeddedLayerCount: state.embeddedDigestSet.size,
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function finalizeSelectedPackImage(selected, blobsDir, state) {
|
|
188
|
+
const layerDescriptors = [];
|
|
189
|
+
const lockLayers = [];
|
|
190
|
+
for (const layerPlan of selected.layerPlans) {
|
|
191
|
+
let finalDigest = layerPlan.rawDigest;
|
|
192
|
+
let finalSize = layerPlan.layerSize;
|
|
193
|
+
let mediaType = 'application/vnd.oci.image.layer.v1.tar';
|
|
194
|
+
let source = 'upstream';
|
|
195
|
+
|
|
196
|
+
if (layerPlan.shouldEmbed) {
|
|
197
|
+
const compressedBlob = state.compressedByRaw.get(layerPlan.rawDigest);
|
|
198
|
+
if (!compressedBlob) {
|
|
199
|
+
throw new Error(`compressed layer not found for ${layerPlan.rawDigest}`);
|
|
200
|
+
}
|
|
201
|
+
finalDigest = `${SHA256_PREFIX}${compressedBlob.digest}`;
|
|
202
|
+
finalSize = compressedBlob.size;
|
|
203
|
+
mediaType = 'application/vnd.oci.image.layer.v1.tar+gzip';
|
|
204
|
+
source = 'embed';
|
|
205
|
+
if (!state.embeddedDigestSet.has(finalDigest)) {
|
|
206
|
+
state.embeddedDigestSet.add(finalDigest);
|
|
207
|
+
state.embeddedLayerBytes += compressedBlob.size;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
layerDescriptors.push({
|
|
212
|
+
mediaType,
|
|
213
|
+
digest: finalDigest,
|
|
214
|
+
size: finalSize,
|
|
215
|
+
});
|
|
216
|
+
lockLayers.push({
|
|
217
|
+
digest: finalDigest,
|
|
218
|
+
source,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const manifestContent = Buffer.from(
|
|
223
|
+
JSON.stringify({
|
|
224
|
+
schemaVersion: 2,
|
|
225
|
+
mediaType: 'application/vnd.oci.image.manifest.v1+json',
|
|
226
|
+
config: {
|
|
227
|
+
mediaType: 'application/vnd.oci.image.config.v1+json',
|
|
228
|
+
digest: `${SHA256_PREFIX}${selected.configBlob.digest}`,
|
|
229
|
+
size: selected.configBlob.size,
|
|
230
|
+
},
|
|
231
|
+
layers: layerDescriptors,
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
const manifestBlob = copyBytesToBlob(manifestContent, blobsDir);
|
|
235
|
+
state.indexDescriptors.push({
|
|
236
|
+
mediaType: 'application/vnd.oci.image.manifest.v1+json',
|
|
237
|
+
digest: `${SHA256_PREFIX}${manifestBlob.digest}`,
|
|
238
|
+
size: manifestBlob.size,
|
|
239
|
+
annotations: {
|
|
240
|
+
'org.opencontainers.image.ref.name': selected.spec.alias,
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
state.lockImages[selected.spec.alias] = {
|
|
244
|
+
image_id: selected.normalizedImageID,
|
|
245
|
+
upstream: String(selected.spec.upstream ?? '').trim(),
|
|
246
|
+
layers: compactLockLayers(lockLayers),
|
|
247
|
+
};
|
|
248
|
+
state.matchedAlias.add(selected.spec.alias);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function packLocalArchiveToStageDir(archivePath, packSpecs, stageDir) {
|
|
252
|
+
if (!Array.isArray(packSpecs) || packSpecs.length === 0) {
|
|
253
|
+
throw new Error('packSpecs cannot be empty');
|
|
254
|
+
}
|
|
255
|
+
const imageRootDir = path.join(stageDir, 'images');
|
|
256
|
+
const blobsDir = path.join(imageRootDir, 'blobs', 'sha256');
|
|
257
|
+
fs.mkdirSync(blobsDir, { recursive: true });
|
|
258
|
+
|
|
259
|
+
const { manifestRaw, entrySizeByPath, jsonFileByPath } = await collectArchiveManifestAndEntrySizes(archivePath);
|
|
260
|
+
const manifests = JSON.parse(String(manifestRaw));
|
|
261
|
+
if (!Array.isArray(manifests) || manifests.length === 0) {
|
|
262
|
+
throw new Error('manifest.json is empty in docker archive');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const specByRef = new Map();
|
|
266
|
+
for (const item of packSpecs) {
|
|
267
|
+
const ref = String(item?.ref ?? '').trim();
|
|
268
|
+
if (!specByRef.has(ref)) {
|
|
269
|
+
specByRef.set(ref, []);
|
|
270
|
+
}
|
|
271
|
+
specByRef.get(ref).push(item);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const selectedByAlias = new Map();
|
|
275
|
+
for (const manifestItem of manifests) {
|
|
276
|
+
const repoTags = Array.isArray(manifestItem?.RepoTags) ? manifestItem.RepoTags.map((item) => String(item ?? '').trim()).filter((item) => item) : [];
|
|
277
|
+
const normalizedManifest = {
|
|
278
|
+
config: normalizeArchivePath(manifestItem?.Config),
|
|
279
|
+
layers: Array.isArray(manifestItem?.Layers) ? manifestItem.Layers.map((item) => normalizeArchivePath(item)) : [],
|
|
280
|
+
repoTags,
|
|
281
|
+
};
|
|
282
|
+
for (const repoTag of repoTags) {
|
|
283
|
+
for (const candidate of specByRef.get(repoTag) ?? []) {
|
|
284
|
+
if (!selectedByAlias.has(candidate.alias)) {
|
|
285
|
+
selectedByAlias.set(candidate.alias, {
|
|
286
|
+
spec: candidate,
|
|
287
|
+
manifest: normalizedManifest,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
for (const item of packSpecs) {
|
|
295
|
+
if (!selectedByAlias.has(item.alias)) {
|
|
296
|
+
throw new Error(`alias ${item.alias} not found in archive ${archivePath}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const missingConfigs = new Set();
|
|
301
|
+
for (const selected of selectedByAlias.values()) {
|
|
302
|
+
if (!Object.prototype.hasOwnProperty.call(jsonFileByPath, selected.manifest.config)) {
|
|
303
|
+
missingConfigs.add(selected.manifest.config);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (missingConfigs.size > 0) {
|
|
307
|
+
const extraConfigByPath = await readArchiveFiles(archivePath, missingConfigs);
|
|
308
|
+
for (const [name, content] of Object.entries(extraConfigByPath)) {
|
|
309
|
+
jsonFileByPath[name] = content;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const embedLayerPathToRawDigest = new Map();
|
|
314
|
+
const requiredEmbedRawDigestSet = new Set();
|
|
315
|
+
for (const [alias, selected] of selectedByAlias.entries()) {
|
|
316
|
+
const configRaw = jsonFileByPath[selected.manifest.config];
|
|
317
|
+
if (!configRaw) {
|
|
318
|
+
throw new Error(`config ${selected.manifest.config} not found for alias ${alias}`);
|
|
319
|
+
}
|
|
320
|
+
selected.configBlob = copyBytesToBlob(configRaw, blobsDir);
|
|
321
|
+
selected.normalizedImageID = normalizeSha256Digest(String(selected.spec.imageID ?? ''), `imageID for alias ${selected.spec.alias}`);
|
|
322
|
+
const actualImageID = `${SHA256_PREFIX}${selected.configBlob.digest}`;
|
|
323
|
+
if (actualImageID !== selected.normalizedImageID) {
|
|
324
|
+
throw new Error(`image id mismatch for alias ${selected.spec.alias}, got ${actualImageID} want ${selected.normalizedImageID}`);
|
|
325
|
+
}
|
|
326
|
+
const config = JSON.parse(String(configRaw));
|
|
327
|
+
const diffIDs = Array.isArray(config?.rootfs?.diff_ids) ? config.rootfs.diff_ids : [];
|
|
328
|
+
if (diffIDs.length !== selected.manifest.layers.length) {
|
|
329
|
+
throw new Error(`layer count mismatch for alias ${selected.spec.alias}`);
|
|
330
|
+
}
|
|
331
|
+
const embeddedSet = new Set((selected.spec.embeddedDiffIDs ?? []).map((item, index) => normalizeSha256Digest(item, `embeddedDiffID for alias ${selected.spec.alias}[${index}]`)));
|
|
332
|
+
selected.layerPlans = selected.manifest.layers.map((layerPath, index) => {
|
|
333
|
+
const layerSize = Number(entrySizeByPath[layerPath] ?? -1);
|
|
334
|
+
if (layerSize < 0) {
|
|
335
|
+
throw new Error(`layer file missing in docker archive: ${layerPath}`);
|
|
336
|
+
}
|
|
337
|
+
const rawDigest = normalizeSha256Digest(diffIDs[index], `diffID for alias ${selected.spec.alias}[${index}]`);
|
|
338
|
+
const shouldEmbed = embeddedSet.has(rawDigest);
|
|
339
|
+
if (!shouldEmbed && !String(selected.spec.upstream ?? '').trim()) {
|
|
340
|
+
throw new Error(`alias ${selected.spec.alias} has upstream layer ${rawDigest} but upstream is empty`);
|
|
341
|
+
}
|
|
342
|
+
if (shouldEmbed) {
|
|
343
|
+
requiredEmbedRawDigestSet.add(rawDigest);
|
|
344
|
+
const existed = embedLayerPathToRawDigest.get(layerPath);
|
|
345
|
+
if (existed && existed !== rawDigest) {
|
|
346
|
+
throw new Error(`layer ${layerPath} maps to different diff ids: ${existed} vs ${rawDigest}`);
|
|
347
|
+
}
|
|
348
|
+
embedLayerPathToRawDigest.set(layerPath, rawDigest);
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
layerPath,
|
|
352
|
+
rawDigest,
|
|
353
|
+
layerSize,
|
|
354
|
+
shouldEmbed,
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const state = {
|
|
360
|
+
lockImages: {},
|
|
361
|
+
indexDescriptors: [],
|
|
362
|
+
matchedAlias: new Set(),
|
|
363
|
+
compressedByRaw: new Map(),
|
|
364
|
+
embeddedDigestSet: new Set(),
|
|
365
|
+
embeddedLayerBytes: 0,
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
if (embedLayerPathToRawDigest.size > 0) {
|
|
369
|
+
await walkDockerArchive(archivePath, async (entry) => {
|
|
370
|
+
const name = normalizeArchivePath(entry.path);
|
|
371
|
+
const rawDigest = embedLayerPathToRawDigest.get(name);
|
|
372
|
+
if (!rawDigest) {
|
|
373
|
+
entry.resume();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (state.compressedByRaw.has(rawDigest)) {
|
|
377
|
+
entry.resume();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const compressedBlob = await gzipEntryToBlob(entry, blobsDir);
|
|
381
|
+
state.compressedByRaw.set(rawDigest, compressedBlob);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (const rawDigest of requiredEmbedRawDigestSet) {
|
|
386
|
+
if (!state.compressedByRaw.has(rawDigest)) {
|
|
387
|
+
throw new Error(`embed layer digest ${rawDigest} not found in docker archive`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
for (const item of packSpecs) {
|
|
392
|
+
const selected = selectedByAlias.get(item.alias);
|
|
393
|
+
if (!selected) {
|
|
394
|
+
throw new Error(`alias ${item.alias} not found in selected image set`);
|
|
395
|
+
}
|
|
396
|
+
finalizeSelectedPackImage(selected, blobsDir, state);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
writeOciLayout(stageDir, state);
|
|
400
|
+
return {
|
|
401
|
+
lockImages: state.lockImages,
|
|
402
|
+
embeddedLayerBytes: state.embeddedLayerBytes,
|
|
403
|
+
embeddedLayerCount: state.embeddedDigestSet.size,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export const __test__ = {
|
|
408
|
+
normalizeSha256Digest,
|
|
409
|
+
};
|