@lazycatcloud/lzc-cli 1.3.13 → 2.0.0-pre.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 +30 -5
- package/changelog.md +16 -0
- package/lib/app/index.js +174 -58
- package/lib/app/lpk_build.js +197 -18
- package/lib/app/lpk_build_images.js +728 -0
- package/lib/app/lpk_create.js +96 -23
- package/lib/app/lpk_create_generator.js +150 -12
- package/lib/app/lpk_devshell.js +35 -21
- package/lib/app/lpk_embed_images.js +257 -0
- package/lib/app/lpk_installer.js +15 -7
- package/lib/app/project_cp.js +64 -0
- package/lib/app/project_deploy.js +33 -0
- package/lib/app/project_exec.js +45 -0
- package/lib/app/project_info.js +106 -0
- package/lib/app/project_log.js +67 -0
- package/lib/app/project_runtime.js +261 -0
- package/lib/app/project_start.js +100 -0
- package/lib/appstore/index.js +56 -16
- package/lib/appstore/publish.js +16 -13
- package/lib/box/index.js +103 -6
- package/lib/box/ssh_remote.js +259 -0
- package/lib/build_remote.js +22 -0
- package/lib/config/index.js +4 -3
- package/lib/debug_bridge.js +837 -44
- package/lib/docker/index.js +30 -10
- package/lib/i18n/index.js +1 -0
- package/lib/i18n/locales/en/translation.json +263 -250
- package/lib/i18n/locales/zh/translation.json +57 -44
- package/lib/lpk/core.js +487 -0
- package/lib/lpk/index.js +210 -0
- package/lib/shellapi.js +5 -5
- package/lib/sig/core.js +254 -0
- package/lib/sig/index.js +88 -0
- package/lib/utils.js +17 -12
- package/package.json +4 -3
- package/scripts/cli.js +4 -0
- package/template/_lpk/README.md +11 -3
- package/template/_lpk/gui-vnc.manifest.yml.in +27 -0
- package/template/_lpk/manifest.yml.in +4 -2
- package/template/_lpk/todolist-golang.manifest.yml.in +16 -0
- package/template/_lpk/todolist-java.manifest.yml.in +15 -0
- package/template/_lpk/todolist-python.manifest.yml.in +15 -0
- package/template/_lpk/vue.lzc-build.yml.in +0 -44
- package/template/blank/_gitignore +1 -0
- package/template/blank/lzc-build.yml +25 -40
- package/template/blank/lzc-manifest.yml +14 -7
- package/template/golang/Dockerfile +19 -0
- package/template/golang/README.md +33 -0
- package/template/golang/_gitignore +3 -0
- package/template/golang/go.mod +3 -0
- package/template/golang/lzc-build.yml +21 -0
- package/template/golang/lzc-icon.png +0 -0
- package/template/golang/main.go +252 -0
- package/template/golang/run.sh +3 -0
- package/template/golang/web/index.html +238 -0
- package/template/gui-vnc/README.md +19 -0
- package/template/gui-vnc/_gitignore +2 -0
- package/template/gui-vnc/images/Dockerfile +30 -0
- package/template/gui-vnc/images/kasmvnc.yaml +33 -0
- package/template/gui-vnc/images/startup-script.desktop +9 -0
- package/template/gui-vnc/images/startup-script.sh +6 -0
- package/template/gui-vnc/lzc-build.yml +23 -0
- package/template/gui-vnc/lzc-icon.png +0 -0
- package/template/python/Dockerfile +15 -0
- package/template/python/README.md +33 -0
- package/template/python/_gitignore +3 -0
- package/template/python/app.py +110 -0
- package/template/python/lzc-build.yml +21 -0
- package/template/python/lzc-icon.png +0 -0
- package/template/python/requirements.txt +1 -0
- package/template/python/run.sh +3 -0
- package/template/python/web/index.html +238 -0
- package/template/springboot/Dockerfile +20 -0
- package/template/springboot/README.md +33 -0
- package/template/springboot/_gitignore +3 -0
- package/template/springboot/lzc-build.yml +21 -0
- package/template/springboot/lzc-icon.png +0 -0
- package/template/springboot/pom.xml +38 -0
- package/template/springboot/run.sh +3 -0
- package/template/springboot/src/main/java/cloud/lazycat/app/Application.java +132 -0
- package/template/springboot/src/main/resources/application.properties +1 -0
- package/template/springboot/src/main/resources/static/index.html +238 -0
- package/template/vue/README.md +17 -7
- package/template/vue/_gitignore +1 -0
- package/template/vue/lzc-build.yml +31 -42
- package/template/vue/src/App.vue +36 -25
- package/template/vue/src/style.css +106 -49
- package/template/vue-minidb/README.md +34 -0
- package/template/vue-minidb/_gitignore +26 -0
- package/template/vue-minidb/index.html +13 -0
- package/template/vue-minidb/lzc-build.yml +48 -0
- package/template/vue-minidb/lzc-icon.png +0 -0
- package/template/vue-minidb/package.json +21 -0
- package/template/vue-minidb/public/vite.svg +1 -0
- package/template/vue-minidb/src/App.vue +206 -0
- package/template/vue-minidb/src/assets/vue.svg +1 -0
- package/template/vue-minidb/src/main.ts +5 -0
- package/template/vue-minidb/src/style.css +136 -0
- package/template/vue-minidb/src/vite-env.d.ts +1 -0
- package/template/vue-minidb/tsconfig.app.json +24 -0
- package/template/vue-minidb/tsconfig.json +7 -0
- package/template/vue-minidb/tsconfig.node.json +22 -0
- package/template/vue-minidb/vite.config.ts +10 -0
- /package/template/{vue → vue-minidb}/src/components/HelloWorld.vue +0 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import logger from 'loglevel';
|
|
6
|
+
import * as tar from 'tar';
|
|
7
|
+
import yaml from 'js-yaml';
|
|
8
|
+
import { DebugBridge } from '../debug_bridge.js';
|
|
9
|
+
import shellApi from '../shellapi.js';
|
|
10
|
+
import { collectContextFromDockerFile } from './lpk_devshell_docker.js';
|
|
11
|
+
import { isFileExist } from '../utils.js';
|
|
12
|
+
import { resolveBuildRemoteFromOptions } from '../build_remote.js';
|
|
13
|
+
|
|
14
|
+
const EMBED_PREFIX = 'embed:';
|
|
15
|
+
const EMBED_TYPO_PREFIXES = ['emebd:'];
|
|
16
|
+
const SHA256_PREFIX = 'sha256:';
|
|
17
|
+
const DEFAULT_UPSTREAM_MATCH = 'registry.lazycat.cloud';
|
|
18
|
+
const IMAGE_BUILD_CACHE_VERSION = 3;
|
|
19
|
+
const IMAGE_PACKAGE_CACHE_VERSION = 1;
|
|
20
|
+
|
|
21
|
+
function isPlainObject(value) {
|
|
22
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sanitizeTagPart(value, fallback) {
|
|
26
|
+
const normalized = String(value ?? '').trim().toLowerCase();
|
|
27
|
+
if (!normalized) {
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
return normalized.replace(/[^a-z0-9._-]/g, '-');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeSha256Digest(digest, fieldName) {
|
|
34
|
+
const value = String(digest ?? '').trim();
|
|
35
|
+
if (!value.startsWith(SHA256_PREFIX)) {
|
|
36
|
+
throw new Error(`${fieldName} is not a sha256 digest: ${value}`);
|
|
37
|
+
}
|
|
38
|
+
const hex = value.slice(SHA256_PREFIX.length);
|
|
39
|
+
if (!/^[0-9a-f]{64}$/i.test(hex)) {
|
|
40
|
+
throw new Error(`${fieldName} has invalid sha256 hex: ${value}`);
|
|
41
|
+
}
|
|
42
|
+
return `${SHA256_PREFIX}${hex.toLowerCase()}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeDigestList(value, fieldName) {
|
|
46
|
+
if (!Array.isArray(value)) {
|
|
47
|
+
throw new Error(`${fieldName} must be a digest array`);
|
|
48
|
+
}
|
|
49
|
+
return value.map((item, index) => normalizeSha256Digest(item, `${fieldName}[${index}]`));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeImageBuildEntries(rawConfig, cwd, manifest) {
|
|
53
|
+
if (!isPlainObject(rawConfig)) {
|
|
54
|
+
throw new Error('images config must be an object');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const packageName = sanitizeTagPart(manifest?.package, 'local-app');
|
|
58
|
+
const version = sanitizeTagPart(manifest?.version, 'latest');
|
|
59
|
+
const entries = [];
|
|
60
|
+
|
|
61
|
+
for (const [alias, configValue] of Object.entries(rawConfig)) {
|
|
62
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(alias)) {
|
|
63
|
+
throw new Error(`Invalid image alias "${alias}", only [a-zA-Z0-9._-] is allowed`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let config = configValue;
|
|
67
|
+
if (typeof configValue === 'string') {
|
|
68
|
+
config = { dockerfile: configValue };
|
|
69
|
+
}
|
|
70
|
+
if (!isPlainObject(config)) {
|
|
71
|
+
throw new Error(`images.${alias} must be an object`);
|
|
72
|
+
}
|
|
73
|
+
if (Object.prototype.hasOwnProperty.call(config, 'upstream_match')) {
|
|
74
|
+
throw new Error(`images.${alias}.upstream_match is invalid, use images.${alias}.upstream-match`);
|
|
75
|
+
}
|
|
76
|
+
if (Object.prototype.hasOwnProperty.call(config, 'dockerfile_content')) {
|
|
77
|
+
throw new Error(`images.${alias}.dockerfile_content is invalid, use images.${alias}.dockerfile-content`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const dockerfileValue = String(config.dockerfile ?? '').trim();
|
|
81
|
+
const hasDockerfileContentField = Object.prototype.hasOwnProperty.call(config, 'dockerfile-content');
|
|
82
|
+
const dockerfileContentValue = hasDockerfileContentField ? String(config['dockerfile-content'] ?? '') : '';
|
|
83
|
+
const dockerfileContent = dockerfileContentValue.trim() === '' ? '' : dockerfileContentValue;
|
|
84
|
+
if (!dockerfileValue && !dockerfileContent) {
|
|
85
|
+
throw new Error(`images.${alias}.dockerfile or images.${alias}.dockerfile-content is required`);
|
|
86
|
+
}
|
|
87
|
+
if (dockerfileValue && dockerfileContent) {
|
|
88
|
+
throw new Error(`images.${alias}.dockerfile and images.${alias}.dockerfile-content cannot be used together`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const contextValue = String(config.context ?? '').trim();
|
|
92
|
+
let dockerfilePath = '';
|
|
93
|
+
let contextDir = '';
|
|
94
|
+
let dockerfileInlineContent = '';
|
|
95
|
+
if (dockerfileValue) {
|
|
96
|
+
dockerfilePath = path.isAbsolute(dockerfileValue) ? dockerfileValue : path.resolve(cwd, dockerfileValue);
|
|
97
|
+
if (!isFileExist(dockerfilePath)) {
|
|
98
|
+
throw new Error(`dockerfile does not exist for alias "${alias}": ${dockerfilePath}`);
|
|
99
|
+
}
|
|
100
|
+
contextDir = contextValue ? (path.isAbsolute(contextValue) ? contextValue : path.resolve(cwd, contextValue)) : path.dirname(dockerfilePath);
|
|
101
|
+
} else {
|
|
102
|
+
dockerfileInlineContent = dockerfileContent;
|
|
103
|
+
contextDir = contextValue ? (path.isAbsolute(contextValue) ? contextValue : path.resolve(cwd, contextValue)) : path.resolve(cwd);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!fs.existsSync(contextDir) || !fs.statSync(contextDir).isDirectory()) {
|
|
107
|
+
throw new Error(`context directory is invalid for alias "${alias}": ${contextDir}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (dockerfilePath) {
|
|
111
|
+
const relativeDockerfile = path.relative(contextDir, dockerfilePath);
|
|
112
|
+
if (!relativeDockerfile || relativeDockerfile.startsWith('..') || path.isAbsolute(relativeDockerfile)) {
|
|
113
|
+
throw new Error(`dockerfile for alias "${alias}" must be inside context`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const upstreamMatchValue = config['upstream-match'];
|
|
118
|
+
let upstreamMatch = DEFAULT_UPSTREAM_MATCH;
|
|
119
|
+
if (upstreamMatchValue !== undefined && upstreamMatchValue !== null) {
|
|
120
|
+
upstreamMatch = String(upstreamMatchValue).trim();
|
|
121
|
+
if (!upstreamMatch) {
|
|
122
|
+
throw new Error(`images.${alias}.upstream-match must not be empty`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
entries.push({
|
|
127
|
+
alias,
|
|
128
|
+
contextDir,
|
|
129
|
+
dockerfilePath,
|
|
130
|
+
dockerfileInlineContent,
|
|
131
|
+
imageLabel: `${packageName}-image-${sanitizeTagPart(alias, 'image')}:${version}`,
|
|
132
|
+
upstreamMatch,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return entries;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function startsWithDigestList(layers, prefix) {
|
|
140
|
+
if (prefix.length > layers.length) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
for (let index = 0; index < prefix.length; index += 1) {
|
|
144
|
+
if (layers[index] !== prefix[index]) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function formatDurationMs(ms) {
|
|
152
|
+
return `${Number(ms || 0).toFixed(0)}ms`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function parseEmbeddedAliasFromRef(rawValue) {
|
|
156
|
+
const trimmed = String(rawValue ?? '').trim();
|
|
157
|
+
if (!trimmed.startsWith(EMBED_PREFIX)) {
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
160
|
+
const rest = trimmed.slice(EMBED_PREFIX.length).trim();
|
|
161
|
+
if (!rest) {
|
|
162
|
+
throw new Error(`Invalid image reference "${trimmed}", alias is required after "${EMBED_PREFIX}"`);
|
|
163
|
+
}
|
|
164
|
+
const at = rest.indexOf('@');
|
|
165
|
+
const alias = (at >= 0 ? rest.slice(0, at) : rest).trim();
|
|
166
|
+
if (!alias) {
|
|
167
|
+
throw new Error(`Invalid image reference "${trimmed}", alias is required after "${EMBED_PREFIX}"`);
|
|
168
|
+
}
|
|
169
|
+
if (at >= 0) {
|
|
170
|
+
const digest = rest.slice(at + 1).trim();
|
|
171
|
+
if (!digest) {
|
|
172
|
+
throw new Error(`Invalid image reference "${trimmed}", digest is required after "@"`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return alias;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function collectManifestEmbedAliases(manifest) {
|
|
179
|
+
const aliases = new Set();
|
|
180
|
+
const walk = (value) => {
|
|
181
|
+
if (typeof value === 'string') {
|
|
182
|
+
const trimmed = value.trim();
|
|
183
|
+
const lowered = trimmed.toLowerCase();
|
|
184
|
+
for (const typoPrefix of EMBED_TYPO_PREFIXES) {
|
|
185
|
+
if (lowered.startsWith(typoPrefix)) {
|
|
186
|
+
throw new Error(`Invalid image reference prefix "${trimmed}", please use "${EMBED_PREFIX}<alias>"`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const alias = parseEmbeddedAliasFromRef(trimmed);
|
|
190
|
+
if (alias) {
|
|
191
|
+
aliases.add(alias);
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (Array.isArray(value)) {
|
|
196
|
+
for (const item of value) {
|
|
197
|
+
walk(item);
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (isPlainObject(value)) {
|
|
202
|
+
for (const item of Object.values(value)) {
|
|
203
|
+
walk(item);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
walk(manifest);
|
|
208
|
+
return [...aliases];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function extractRepoFromRepoDigest(repoDigest) {
|
|
212
|
+
const value = String(repoDigest ?? '').trim();
|
|
213
|
+
const index = value.indexOf('@sha256:');
|
|
214
|
+
if (index < 0) {
|
|
215
|
+
return '';
|
|
216
|
+
}
|
|
217
|
+
return value.slice(0, index);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function imageBuildCacheFilePath(cwd) {
|
|
221
|
+
const pathID = crypto.createHash('sha256').update(path.resolve(cwd)).digest('hex');
|
|
222
|
+
return path.join(os.tmpdir(), 'lzc-cli-image-build', pathID, 'cache.json');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function loadImageBuildCache(cwd) {
|
|
226
|
+
const cachePath = imageBuildCacheFilePath(cwd);
|
|
227
|
+
if (!isFileExist(cachePath)) {
|
|
228
|
+
return {
|
|
229
|
+
cachePath,
|
|
230
|
+
records: {},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
const raw = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
|
235
|
+
if (raw?.version !== IMAGE_BUILD_CACHE_VERSION || !isPlainObject(raw?.records)) {
|
|
236
|
+
return {
|
|
237
|
+
cachePath,
|
|
238
|
+
records: {},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
cachePath,
|
|
243
|
+
records: raw.records,
|
|
244
|
+
};
|
|
245
|
+
} catch (error) {
|
|
246
|
+
logger.debug(`ignore broken image build cache: ${error.message}`);
|
|
247
|
+
return {
|
|
248
|
+
cachePath,
|
|
249
|
+
records: {},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function saveImageBuildCache(cachePath, records) {
|
|
255
|
+
const cacheDir = path.dirname(cachePath);
|
|
256
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
257
|
+
fs.writeFileSync(
|
|
258
|
+
cachePath,
|
|
259
|
+
JSON.stringify(
|
|
260
|
+
{
|
|
261
|
+
version: IMAGE_BUILD_CACHE_VERSION,
|
|
262
|
+
records,
|
|
263
|
+
},
|
|
264
|
+
null,
|
|
265
|
+
2,
|
|
266
|
+
),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function imagePackageCacheRootDir(cwd) {
|
|
271
|
+
const pathID = crypto.createHash('sha256').update(path.resolve(cwd)).digest('hex');
|
|
272
|
+
return path.join(os.tmpdir(), 'lzc-cli-image-build', pathID, 'package-cache');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function normalizeMetaForPackageCache(metaList) {
|
|
276
|
+
return [...metaList]
|
|
277
|
+
.map((meta) => ({
|
|
278
|
+
alias: String(meta?.alias ?? ''),
|
|
279
|
+
imageID: String(meta?.imageID ?? ''),
|
|
280
|
+
upstream: String(meta?.upstream ?? ''),
|
|
281
|
+
embeddedDiffIDs: [...(meta?.embeddedDiffIDs ?? [])].map((item) => String(item ?? '')),
|
|
282
|
+
}))
|
|
283
|
+
.sort((a, b) => a.alias.localeCompare(b.alias));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function computePackageCacheKey(metaList) {
|
|
287
|
+
const normalized = normalizeMetaForPackageCache(metaList);
|
|
288
|
+
return crypto
|
|
289
|
+
.createHash('sha256')
|
|
290
|
+
.update(
|
|
291
|
+
JSON.stringify({
|
|
292
|
+
version: IMAGE_PACKAGE_CACHE_VERSION,
|
|
293
|
+
images: normalized,
|
|
294
|
+
}),
|
|
295
|
+
)
|
|
296
|
+
.digest('hex');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function tryLoadPackageCache(cwd, cacheKey, targetTempDir) {
|
|
300
|
+
if (!cacheKey) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
const cacheRoot = imagePackageCacheRootDir(cwd);
|
|
304
|
+
const cacheDir = path.join(cacheRoot, cacheKey);
|
|
305
|
+
const cachedImagesDir = path.join(cacheDir, 'images');
|
|
306
|
+
const cachedLockPath = path.join(cacheDir, 'images.lock');
|
|
307
|
+
const cachedMetaPath = path.join(cacheDir, 'meta.json');
|
|
308
|
+
|
|
309
|
+
if (!isFileExist(cachedLockPath) || !isFileExist(cachedMetaPath) || !fs.existsSync(cachedImagesDir) || !fs.statSync(cachedImagesDir).isDirectory()) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const meta = JSON.parse(fs.readFileSync(cachedMetaPath, 'utf-8'));
|
|
315
|
+
const targetImagesDir = path.join(targetTempDir, 'images');
|
|
316
|
+
fs.mkdirSync(targetImagesDir, { recursive: true });
|
|
317
|
+
fs.cpSync(cachedImagesDir, targetImagesDir, { recursive: true });
|
|
318
|
+
fs.copyFileSync(cachedLockPath, path.join(targetTempDir, 'images.lock'));
|
|
319
|
+
return {
|
|
320
|
+
embeddedLayerBytes: Number(meta?.embeddedLayerBytes ?? 0),
|
|
321
|
+
embeddedLayerCount: Number(meta?.embeddedLayerCount ?? 0),
|
|
322
|
+
};
|
|
323
|
+
} catch (error) {
|
|
324
|
+
logger.debug(`ignore broken package cache: ${error.message}`);
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function savePackageCache(cwd, cacheKey, sourceTempDir, convertResult) {
|
|
330
|
+
if (!cacheKey) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const cacheRoot = imagePackageCacheRootDir(cwd);
|
|
334
|
+
const cacheDir = path.join(cacheRoot, cacheKey);
|
|
335
|
+
const cacheDirTmp = `${cacheDir}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
336
|
+
try {
|
|
337
|
+
fs.mkdirSync(cacheDirTmp, { recursive: true });
|
|
338
|
+
fs.cpSync(path.join(sourceTempDir, 'images'), path.join(cacheDirTmp, 'images'), { recursive: true });
|
|
339
|
+
fs.copyFileSync(path.join(sourceTempDir, 'images.lock'), path.join(cacheDirTmp, 'images.lock'));
|
|
340
|
+
fs.writeFileSync(
|
|
341
|
+
path.join(cacheDirTmp, 'meta.json'),
|
|
342
|
+
JSON.stringify(
|
|
343
|
+
{
|
|
344
|
+
version: IMAGE_PACKAGE_CACHE_VERSION,
|
|
345
|
+
embeddedLayerBytes: Number(convertResult?.embeddedLayerBytes ?? 0),
|
|
346
|
+
embeddedLayerCount: Number(convertResult?.embeddedLayerCount ?? 0),
|
|
347
|
+
},
|
|
348
|
+
null,
|
|
349
|
+
2,
|
|
350
|
+
),
|
|
351
|
+
);
|
|
352
|
+
fs.mkdirSync(cacheRoot, { recursive: true });
|
|
353
|
+
fs.rmSync(cacheDir, { recursive: true, force: true });
|
|
354
|
+
fs.renameSync(cacheDirTmp, cacheDir);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
logger.debug(`skip save package cache: ${error.message}`);
|
|
357
|
+
fs.rmSync(cacheDirTmp, { recursive: true, force: true });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function compactLockLayers(rawLayers) {
|
|
362
|
+
if (!Array.isArray(rawLayers)) {
|
|
363
|
+
return [];
|
|
364
|
+
}
|
|
365
|
+
const layers = [];
|
|
366
|
+
for (const item of rawLayers) {
|
|
367
|
+
const digest = String(item?.digest ?? '').trim();
|
|
368
|
+
if (!digest) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const source = String(item?.source ?? '').trim().toLowerCase();
|
|
372
|
+
layers.push({
|
|
373
|
+
digest,
|
|
374
|
+
source,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return layers;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function compactLockImages(rawImages) {
|
|
381
|
+
if (!isPlainObject(rawImages)) {
|
|
382
|
+
return {};
|
|
383
|
+
}
|
|
384
|
+
const compacted = {};
|
|
385
|
+
for (const [alias, imageInfo] of Object.entries(rawImages)) {
|
|
386
|
+
if (!isPlainObject(imageInfo)) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const imageID = String(imageInfo.image_id ?? '').trim();
|
|
390
|
+
const upstream = String(imageInfo.upstream ?? '').trim();
|
|
391
|
+
const layers = compactLockLayers(imageInfo.layers);
|
|
392
|
+
compacted[alias] = {
|
|
393
|
+
image_id: imageID,
|
|
394
|
+
upstream,
|
|
395
|
+
layers,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return compacted;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function rewriteImagesLock(tempDir, fallbackLockImages = {}) {
|
|
402
|
+
const lockPath = path.join(tempDir, 'images.lock');
|
|
403
|
+
let sourceImages = {};
|
|
404
|
+
if (isFileExist(lockPath)) {
|
|
405
|
+
try {
|
|
406
|
+
const existingLock = yaml.load(fs.readFileSync(lockPath, 'utf-8'));
|
|
407
|
+
sourceImages = existingLock?.images;
|
|
408
|
+
} catch (error) {
|
|
409
|
+
logger.debug(`ignore broken images.lock before compact: ${error.message}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (!isPlainObject(sourceImages) || Object.keys(sourceImages).length === 0) {
|
|
413
|
+
sourceImages = fallbackLockImages;
|
|
414
|
+
}
|
|
415
|
+
const lock = {
|
|
416
|
+
version: 1,
|
|
417
|
+
images: compactLockImages(sourceImages),
|
|
418
|
+
};
|
|
419
|
+
fs.writeFileSync(lockPath, yaml.dump(lock, { lineWidth: -1 }));
|
|
420
|
+
return lock.images;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function ensureDockerfileForEntry(entry) {
|
|
424
|
+
if (!entry?.dockerfileInlineContent) {
|
|
425
|
+
return {
|
|
426
|
+
dockerfilePath: entry.dockerfilePath,
|
|
427
|
+
cleanup: () => {},
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const dockerfilePath = path.join(entry.contextDir, 'Dockerfile');
|
|
432
|
+
const backupPath = `${dockerfilePath}.lzc-inline-backup-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
433
|
+
const hasOriginalDockerfile = fs.existsSync(dockerfilePath);
|
|
434
|
+
if (hasOriginalDockerfile) {
|
|
435
|
+
fs.copyFileSync(dockerfilePath, backupPath);
|
|
436
|
+
}
|
|
437
|
+
fs.writeFileSync(dockerfilePath, entry.dockerfileInlineContent);
|
|
438
|
+
return {
|
|
439
|
+
dockerfilePath,
|
|
440
|
+
cleanup: () => {
|
|
441
|
+
if (hasOriginalDockerfile && fs.existsSync(backupPath)) {
|
|
442
|
+
fs.copyFileSync(backupPath, dockerfilePath);
|
|
443
|
+
fs.rmSync(backupPath, { force: true });
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
fs.rmSync(dockerfilePath, { force: true });
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function sha256File(filePath) {
|
|
452
|
+
return new Promise((resolve, reject) => {
|
|
453
|
+
const hash = crypto.createHash('sha256');
|
|
454
|
+
let size = 0;
|
|
455
|
+
const stream = fs.createReadStream(filePath);
|
|
456
|
+
stream.on('data', (chunk) => {
|
|
457
|
+
size += chunk.length;
|
|
458
|
+
hash.update(chunk);
|
|
459
|
+
});
|
|
460
|
+
stream.on('error', reject);
|
|
461
|
+
stream.on('end', () => {
|
|
462
|
+
resolve({
|
|
463
|
+
digest: hash.digest('hex'),
|
|
464
|
+
size,
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export async function buildConfiguredImagesToTempDir(rawConfig, manifest, cwd, tempDir, options = {}) {
|
|
471
|
+
const imageEntries = normalizeImageBuildEntries(rawConfig, cwd, manifest);
|
|
472
|
+
if (imageEntries.length === 0) {
|
|
473
|
+
return {
|
|
474
|
+
imageCount: 0,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const manifestAliases = collectManifestEmbedAliases(manifest);
|
|
479
|
+
for (const alias of manifestAliases) {
|
|
480
|
+
const exists = imageEntries.some((item) => item.alias === alias);
|
|
481
|
+
if (!exists) {
|
|
482
|
+
throw new Error(`Found alias "${alias}" in manifest but it is missing in lzc-build.yml images`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const buildRemote = resolveBuildRemoteFromOptions(options, 'lzc-build.yml');
|
|
487
|
+
if (!buildRemote) {
|
|
488
|
+
await shellApi.init();
|
|
489
|
+
}
|
|
490
|
+
const bridge = new DebugBridge(cwd, buildRemote);
|
|
491
|
+
await bridge.init();
|
|
492
|
+
|
|
493
|
+
const imageMetaByRef = new Map();
|
|
494
|
+
const imageRefs = [];
|
|
495
|
+
const buildCache = loadImageBuildCache(cwd);
|
|
496
|
+
const buildCacheRecords = { ...buildCache.records };
|
|
497
|
+
|
|
498
|
+
for (const entry of imageEntries) {
|
|
499
|
+
const profileStartAt = Date.now();
|
|
500
|
+
let contextCollectMs = 0;
|
|
501
|
+
let buildStageMs = 0;
|
|
502
|
+
let resolveStageMs = 0;
|
|
503
|
+
let buildMode = 'build-pack';
|
|
504
|
+
|
|
505
|
+
const dockerfileDesc = entry.dockerfilePath || '(inline dockerfile-content)';
|
|
506
|
+
logger.info(`Build image for alias "${entry.alias}" from ${dockerfileDesc}`);
|
|
507
|
+
const contextStartAt = Date.now();
|
|
508
|
+
const dockerfileSource = ensureDockerfileForEntry(entry);
|
|
509
|
+
let contextTar = '';
|
|
510
|
+
try {
|
|
511
|
+
contextTar = await collectContextFromDockerFile(entry.contextDir, dockerfileSource.dockerfilePath);
|
|
512
|
+
} finally {
|
|
513
|
+
dockerfileSource.cleanup();
|
|
514
|
+
}
|
|
515
|
+
contextCollectMs = Date.now() - contextStartAt;
|
|
516
|
+
const contextHash = await sha256File(contextTar);
|
|
517
|
+
const contextDigest = `${SHA256_PREFIX}${contextHash.digest}`;
|
|
518
|
+
|
|
519
|
+
const cacheRecord = buildCache.records?.[entry.alias];
|
|
520
|
+
let builtImageRef = '';
|
|
521
|
+
let reusedFromCache = false;
|
|
522
|
+
let imageID = '';
|
|
523
|
+
let builtDiffIDs = [];
|
|
524
|
+
let upstream = '';
|
|
525
|
+
let upstreamDiffIDs = [];
|
|
526
|
+
let archiveKey = '';
|
|
527
|
+
|
|
528
|
+
if (
|
|
529
|
+
cacheRecord &&
|
|
530
|
+
cacheRecord.image_label === entry.imageLabel &&
|
|
531
|
+
cacheRecord.context_digest === contextDigest &&
|
|
532
|
+
cacheRecord.upstream_match === entry.upstreamMatch &&
|
|
533
|
+
cacheRecord.build_mode === 'pack' &&
|
|
534
|
+
Array.isArray(cacheRecord.built_diff_ids) &&
|
|
535
|
+
typeof cacheRecord.image_id === 'string' &&
|
|
536
|
+
cacheRecord.image_id.trim() !== ''
|
|
537
|
+
) {
|
|
538
|
+
try {
|
|
539
|
+
imageID = normalizeSha256Digest(cacheRecord.image_id, `cached image id of alias ${entry.alias}`);
|
|
540
|
+
builtDiffIDs = normalizeDigestList(cacheRecord.built_diff_ids, `cached built diff ids of alias ${entry.alias}`);
|
|
541
|
+
if (builtDiffIDs.length === 0) {
|
|
542
|
+
throw new Error('built diff ids is empty');
|
|
543
|
+
}
|
|
544
|
+
upstream = cacheRecord.upstream ? String(cacheRecord.upstream).trim() : '';
|
|
545
|
+
if (upstream && !upstream.includes('@sha256:')) {
|
|
546
|
+
throw new Error(`invalid cached upstream: ${upstream}`);
|
|
547
|
+
}
|
|
548
|
+
upstreamDiffIDs = Array.isArray(cacheRecord.upstream_diff_ids)
|
|
549
|
+
? normalizeDigestList(cacheRecord.upstream_diff_ids, `cached upstream diff ids of alias ${entry.alias}`)
|
|
550
|
+
: [];
|
|
551
|
+
archiveKey = String(cacheRecord.archive_key ?? '').trim();
|
|
552
|
+
builtImageRef = typeof cacheRecord.image_ref === 'string' && cacheRecord.image_ref.trim() !== '' ? cacheRecord.image_ref : `debug.bridge/${entry.imageLabel}`;
|
|
553
|
+
reusedFromCache = true;
|
|
554
|
+
buildMode = 'build-pack-cache';
|
|
555
|
+
logger.info(`Reuse cached build-pack metadata for alias "${entry.alias}"`);
|
|
556
|
+
} catch (error) {
|
|
557
|
+
logger.info(`Cached build-pack metadata is invalid for alias "${entry.alias}", rebuild image`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const buildStartAt = Date.now();
|
|
562
|
+
if (!reusedFromCache) {
|
|
563
|
+
const buildPackResult = await bridge.buildImageForPack(entry.imageLabel, contextTar);
|
|
564
|
+
builtImageRef = String(buildPackResult?.tag ?? '').trim() || `debug.bridge/${entry.imageLabel}`;
|
|
565
|
+
buildMode = 'build-pack';
|
|
566
|
+
archiveKey = String(buildPackResult?.archiveKey ?? '').trim();
|
|
567
|
+
imageID = normalizeSha256Digest(buildPackResult?.imageID, `build-pack image id of alias ${entry.alias}`);
|
|
568
|
+
builtDiffIDs = normalizeDigestList(buildPackResult?.diffIDs ?? [], `build-pack diff ids of alias ${entry.alias}`);
|
|
569
|
+
if (builtDiffIDs.length === 0) {
|
|
570
|
+
throw new Error(`No rootfs layer found in build-pack output for alias "${entry.alias}"`);
|
|
571
|
+
}
|
|
572
|
+
const baseRepoDigest = String(buildPackResult?.baseRepoDigest ?? '').trim();
|
|
573
|
+
if (baseRepoDigest) {
|
|
574
|
+
if (!baseRepoDigest.includes('@sha256:')) {
|
|
575
|
+
throw new Error(`Invalid baseRepoDigest from build-pack of alias "${entry.alias}": ${baseRepoDigest}`);
|
|
576
|
+
}
|
|
577
|
+
const baseRepo = extractRepoFromRepoDigest(baseRepoDigest);
|
|
578
|
+
if (!entry.upstreamMatch || baseRepo.startsWith(entry.upstreamMatch)) {
|
|
579
|
+
upstream = baseRepoDigest;
|
|
580
|
+
if (Array.isArray(buildPackResult?.baseDiffIDs) && buildPackResult.baseDiffIDs.length > 0) {
|
|
581
|
+
upstreamDiffIDs = normalizeDigestList(buildPackResult.baseDiffIDs, `build-pack base diff ids of alias ${entry.alias}`);
|
|
582
|
+
}
|
|
583
|
+
if (upstreamDiffIDs.length > 0 && !startsWithDigestList(builtDiffIDs, upstreamDiffIDs)) {
|
|
584
|
+
logger.warn(`Ignore invalid upstream layer prefix for alias "${entry.alias}" from build-pack metadata`);
|
|
585
|
+
upstream = '';
|
|
586
|
+
upstreamDiffIDs = [];
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
fs.rmSync(contextTar, { force: true });
|
|
592
|
+
}
|
|
593
|
+
buildStageMs = Date.now() - buildStartAt;
|
|
594
|
+
imageRefs.push(builtImageRef);
|
|
595
|
+
|
|
596
|
+
const resolveStartAt = Date.now();
|
|
597
|
+
if (!imageID || builtDiffIDs.length === 0) {
|
|
598
|
+
throw new Error(`No rootfs layer metadata found in build-pack output for alias "${entry.alias}"`);
|
|
599
|
+
}
|
|
600
|
+
resolveStageMs = Date.now() - resolveStartAt;
|
|
601
|
+
|
|
602
|
+
if (upstreamDiffIDs.length > 0 && !startsWithDigestList(builtDiffIDs, upstreamDiffIDs)) {
|
|
603
|
+
throw new Error(`Failed to derive mixed layers for alias "${entry.alias}", built layers do not start with upstream layers`);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const embeddedDiffIDs = upstreamDiffIDs.length === 0 ? builtDiffIDs : builtDiffIDs.slice(upstreamDiffIDs.length);
|
|
607
|
+
if (embeddedDiffIDs.length === 0 && !upstream) {
|
|
608
|
+
throw new Error(`Alias "${entry.alias}" has no embed layers and no upstream image`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
imageMetaByRef.set(builtImageRef, {
|
|
612
|
+
alias: entry.alias,
|
|
613
|
+
imageID,
|
|
614
|
+
upstream: upstream || '',
|
|
615
|
+
embeddedDiffIDs,
|
|
616
|
+
archiveKey: archiveKey || '',
|
|
617
|
+
});
|
|
618
|
+
buildCacheRecords[entry.alias] = {
|
|
619
|
+
image_label: entry.imageLabel,
|
|
620
|
+
context_digest: contextDigest,
|
|
621
|
+
upstream_match: entry.upstreamMatch,
|
|
622
|
+
image_ref: builtImageRef,
|
|
623
|
+
image_id: imageID,
|
|
624
|
+
build_mode: 'pack',
|
|
625
|
+
built_diff_ids: builtDiffIDs,
|
|
626
|
+
upstream: upstream || '',
|
|
627
|
+
upstream_diff_ids: upstreamDiffIDs,
|
|
628
|
+
archive_key: archiveKey || '',
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
logger.info(`Image alias "${entry.alias}" is ready: ${builtImageRef}`);
|
|
632
|
+
logger.info(
|
|
633
|
+
`[profile] alias=${entry.alias} mode=${buildMode} context=${formatDurationMs(contextCollectMs)} build=${formatDurationMs(
|
|
634
|
+
buildStageMs,
|
|
635
|
+
)} resolve=${formatDurationMs(resolveStageMs)} total=${formatDurationMs(Date.now() - profileStartAt)}`,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
saveImageBuildCache(buildCache.cachePath, buildCacheRecords);
|
|
639
|
+
|
|
640
|
+
const packageCacheKey = computePackageCacheKey([...imageMetaByRef.values()]);
|
|
641
|
+
const cachedPackage = tryLoadPackageCache(cwd, packageCacheKey, tempDir);
|
|
642
|
+
let convertResult = null;
|
|
643
|
+
if (cachedPackage) {
|
|
644
|
+
logger.info('Reuse cached OCI image package');
|
|
645
|
+
convertResult = {
|
|
646
|
+
lockImages: Object.fromEntries(
|
|
647
|
+
[...imageMetaByRef.values()].map((meta) => [
|
|
648
|
+
meta.alias,
|
|
649
|
+
{
|
|
650
|
+
image_id: meta.imageID,
|
|
651
|
+
upstream: meta.upstream ?? '',
|
|
652
|
+
},
|
|
653
|
+
]),
|
|
654
|
+
),
|
|
655
|
+
embeddedLayerBytes: cachedPackage.embeddedLayerBytes,
|
|
656
|
+
embeddedLayerCount: cachedPackage.embeddedLayerCount,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (!convertResult) {
|
|
661
|
+
const packBridgeStartAt = Date.now();
|
|
662
|
+
const packedArchivePath = path.join(tempDir, 'images.packed.tar');
|
|
663
|
+
const packSpecs = imageRefs.map((ref) => {
|
|
664
|
+
const meta = imageMetaByRef.get(ref);
|
|
665
|
+
if (!meta) {
|
|
666
|
+
throw new Error(`Missing image meta for ref: ${ref}`);
|
|
667
|
+
}
|
|
668
|
+
return {
|
|
669
|
+
ref,
|
|
670
|
+
alias: meta.alias,
|
|
671
|
+
imageID: meta.imageID,
|
|
672
|
+
upstream: meta.upstream ?? '',
|
|
673
|
+
embeddedDiffIDs: meta.embeddedDiffIDs,
|
|
674
|
+
archiveKey: meta.archiveKey || undefined,
|
|
675
|
+
};
|
|
676
|
+
});
|
|
677
|
+
logger.info('Pack built images to OCI layout in debug bridge');
|
|
678
|
+
await bridge.packImages(packSpecs, packedArchivePath);
|
|
679
|
+
const packBridgeMs = Date.now() - packBridgeStartAt;
|
|
680
|
+
const extractStartAt = Date.now();
|
|
681
|
+
await tar.x({
|
|
682
|
+
file: packedArchivePath,
|
|
683
|
+
cwd: tempDir,
|
|
684
|
+
});
|
|
685
|
+
const extractMs = Date.now() - extractStartAt;
|
|
686
|
+
fs.rmSync(packedArchivePath, { force: true });
|
|
687
|
+
const parseStartAt = Date.now();
|
|
688
|
+
const packedResultPath = path.join(tempDir, 'pack-result.json');
|
|
689
|
+
if (!isFileExist(packedResultPath)) {
|
|
690
|
+
throw new Error('pack-result.json not found after pack-images');
|
|
691
|
+
}
|
|
692
|
+
const packedResult = JSON.parse(fs.readFileSync(packedResultPath, 'utf-8'));
|
|
693
|
+
fs.rmSync(packedResultPath, { force: true });
|
|
694
|
+
if (!isPlainObject(packedResult?.lockImages)) {
|
|
695
|
+
throw new Error('Invalid pack-result.json: lockImages is missing');
|
|
696
|
+
}
|
|
697
|
+
convertResult = {
|
|
698
|
+
lockImages: packedResult.lockImages,
|
|
699
|
+
embeddedLayerBytes: Number(packedResult?.embeddedLayerBytes ?? 0),
|
|
700
|
+
embeddedLayerCount: Number(packedResult?.embeddedLayerCount ?? 0),
|
|
701
|
+
};
|
|
702
|
+
const parseMs = Date.now() - parseStartAt;
|
|
703
|
+
logger.info(
|
|
704
|
+
`[profile] pack-images bridge=${formatDurationMs(packBridgeMs)} extract=${formatDurationMs(extractMs)} parse=${formatDurationMs(parseMs)} total=${formatDurationMs(
|
|
705
|
+
packBridgeMs + extractMs + parseMs,
|
|
706
|
+
)}`,
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
convertResult.lockImages = rewriteImagesLock(tempDir, convertResult.lockImages);
|
|
710
|
+
if (!cachedPackage) {
|
|
711
|
+
savePackageCache(cwd, packageCacheKey, tempDir, convertResult);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const upstreamByAlias = {};
|
|
715
|
+
const resolvedImageByAlias = {};
|
|
716
|
+
for (const meta of imageMetaByRef.values()) {
|
|
717
|
+
upstreamByAlias[meta.alias] = meta.upstream || '';
|
|
718
|
+
resolvedImageByAlias[meta.alias] = meta.imageID;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
imageCount: Object.keys(convertResult.lockImages).length,
|
|
723
|
+
upstreamByAlias,
|
|
724
|
+
resolvedImageByAlias,
|
|
725
|
+
embeddedLayerBytes: convertResult.embeddedLayerBytes,
|
|
726
|
+
embeddedLayerCount: convertResult.embeddedLayerCount,
|
|
727
|
+
};
|
|
728
|
+
}
|