@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,425 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { Writable } from 'node:stream';
|
|
3
|
+
import spawn from 'cross-spawn';
|
|
4
|
+
import logger from 'loglevel';
|
|
5
|
+
import { DockerfileParser } from 'dockerfile-ast';
|
|
6
|
+
|
|
7
|
+
function createNullWritable() {
|
|
8
|
+
return new Writable({
|
|
9
|
+
write(_chunk, _encoding, callback) {
|
|
10
|
+
callback();
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function runLocalCommand(command, args, options = {}) {
|
|
16
|
+
const cwd = options.cwd || process.cwd();
|
|
17
|
+
const env = options.env ? { ...process.env, ...options.env } : process.env;
|
|
18
|
+
const stdinFile = String(options.stdinFile ?? '').trim();
|
|
19
|
+
const stdoutFile = String(options.stdoutFile ?? '').trim();
|
|
20
|
+
const quietStdout = !!options.quietStdout;
|
|
21
|
+
const quietStderr = !!options.quietStderr;
|
|
22
|
+
const stdoutBuffer = [];
|
|
23
|
+
const stderrBuffer = [];
|
|
24
|
+
|
|
25
|
+
logger.debug(`run local command: ${command} ${args.join(' ')}`);
|
|
26
|
+
const child = spawn(command, args, {
|
|
27
|
+
cwd,
|
|
28
|
+
env,
|
|
29
|
+
shell: false,
|
|
30
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
let done = false;
|
|
35
|
+
const finish = (error, output = {}) => {
|
|
36
|
+
if (done) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
done = true;
|
|
40
|
+
if (error) {
|
|
41
|
+
reject(error);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
resolve(output);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
let stdoutStream = null;
|
|
48
|
+
let stderrMirror = null;
|
|
49
|
+
|
|
50
|
+
if (stdinFile) {
|
|
51
|
+
const stdinStream = fs.createReadStream(stdinFile);
|
|
52
|
+
stdinStream.on('error', (error) => finish(error));
|
|
53
|
+
stdinStream.pipe(child.stdin);
|
|
54
|
+
} else {
|
|
55
|
+
child.stdin.end();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (stdoutFile) {
|
|
59
|
+
stdoutStream = fs.createWriteStream(stdoutFile);
|
|
60
|
+
stdoutStream.on('error', (error) => finish(error));
|
|
61
|
+
child.stdout.pipe(stdoutStream);
|
|
62
|
+
} else {
|
|
63
|
+
child.stdout.on('data', (chunk) => {
|
|
64
|
+
stdoutBuffer.push(chunk);
|
|
65
|
+
if (!quietStdout) {
|
|
66
|
+
process.stdout.write(chunk);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (quietStderr) {
|
|
72
|
+
stderrMirror = createNullWritable();
|
|
73
|
+
child.stderr.pipe(stderrMirror);
|
|
74
|
+
} else {
|
|
75
|
+
child.stderr.on('data', (chunk) => {
|
|
76
|
+
stderrBuffer.push(chunk);
|
|
77
|
+
process.stderr.write(chunk);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
child.on('error', (error) => finish(error));
|
|
82
|
+
child.on('close', (code) => {
|
|
83
|
+
const finalize = () => {
|
|
84
|
+
const stdout = Buffer.concat(stdoutBuffer).toString('utf-8');
|
|
85
|
+
const stderr = Buffer.concat(stderrBuffer).toString('utf-8');
|
|
86
|
+
if (code === 0) {
|
|
87
|
+
finish(null, { stdout, stderr });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
finish(new Error(`${command} ${args.join(' ')} failed with code ${code}\n${stderr || stdout}`));
|
|
91
|
+
};
|
|
92
|
+
if (stdoutStream) {
|
|
93
|
+
stdoutStream.end(finalize);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
finalize();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeSha256Digest(value, fieldName) {
|
|
102
|
+
const raw = String(value ?? '').trim();
|
|
103
|
+
const digest = /^[0-9a-f]{64}$/i.test(raw) ? `sha256:${raw}` : raw;
|
|
104
|
+
if (!digest.startsWith('sha256:')) {
|
|
105
|
+
throw new Error(`${fieldName} is not a sha256 digest: ${raw}`);
|
|
106
|
+
}
|
|
107
|
+
const hex = digest.slice('sha256:'.length);
|
|
108
|
+
if (!/^[0-9a-f]{64}$/i.test(hex)) {
|
|
109
|
+
throw new Error(`${fieldName} has invalid sha256 hex: ${digest}`);
|
|
110
|
+
}
|
|
111
|
+
return `sha256:${hex.toLowerCase()}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function stripRepoDigest(value) {
|
|
115
|
+
const text = String(value ?? '').trim();
|
|
116
|
+
const index = text.indexOf('@sha256:');
|
|
117
|
+
if (index < 0) {
|
|
118
|
+
return text;
|
|
119
|
+
}
|
|
120
|
+
return text.slice(0, index);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function repoStartsWith(repoDigest, upstreamMatch) {
|
|
124
|
+
const match = String(upstreamMatch ?? '').trim();
|
|
125
|
+
if (!match) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return stripRepoDigest(repoDigest).startsWith(match);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function startsWithDigestList(layers, prefix) {
|
|
132
|
+
if (prefix.length > layers.length) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
for (let index = 0; index < prefix.length; index += 1) {
|
|
136
|
+
if (layers[index] !== prefix[index]) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseInspectArray(stdout, imageRef, commandName) {
|
|
144
|
+
let parsed;
|
|
145
|
+
try {
|
|
146
|
+
parsed = JSON.parse(stdout);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw new Error(`failed to parse ${commandName} output for ${imageRef}: ${error.message}`);
|
|
149
|
+
}
|
|
150
|
+
if (!Array.isArray(parsed) || parsed.length === 0 || !parsed[0] || typeof parsed[0] !== 'object') {
|
|
151
|
+
throw new Error(`invalid ${commandName} output for ${imageRef}`);
|
|
152
|
+
}
|
|
153
|
+
return parsed[0];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function pickRepoDigest(repoDigests, upstreamMatch, expectedDigest = '') {
|
|
157
|
+
const normalizedExpected = expectedDigest ? normalizeSha256Digest(expectedDigest, 'expected upstream digest') : '';
|
|
158
|
+
const candidates = Array.isArray(repoDigests) ? repoDigests.map((item) => String(item ?? '').trim()).filter((item) => item) : [];
|
|
159
|
+
const matched = candidates.filter((item) => repoStartsWith(item, upstreamMatch));
|
|
160
|
+
if (matched.length === 0) {
|
|
161
|
+
return '';
|
|
162
|
+
}
|
|
163
|
+
if (normalizedExpected) {
|
|
164
|
+
const exact = matched.find((item) => item.endsWith(`@${normalizedExpected}`));
|
|
165
|
+
if (exact) {
|
|
166
|
+
return exact;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return matched[0];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function parseBuildahInspectOutput(stdout, imageRef) {
|
|
173
|
+
let inspect;
|
|
174
|
+
try {
|
|
175
|
+
inspect = JSON.parse(stdout);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw new Error(`failed to parse buildah inspect output for ${imageRef}: ${error.message}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const configRaw = typeof inspect?.Config === 'string' ? inspect.Config : JSON.stringify(inspect?.Config ?? {});
|
|
181
|
+
let config;
|
|
182
|
+
try {
|
|
183
|
+
config = JSON.parse(configRaw);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
throw new Error(`failed to parse buildah inspect config for ${imageRef}: ${error.message}`);
|
|
186
|
+
}
|
|
187
|
+
const diffIDs = Array.isArray(config?.rootfs?.diff_ids)
|
|
188
|
+
? config.rootfs.diff_ids.map((item, index) => normalizeSha256Digest(item, `buildah inspect diff_ids[${index}] of ${imageRef}`))
|
|
189
|
+
: [];
|
|
190
|
+
|
|
191
|
+
const manifestRaw = typeof inspect?.Manifest === 'string' ? inspect.Manifest : JSON.stringify(inspect?.Manifest ?? {});
|
|
192
|
+
let manifest;
|
|
193
|
+
try {
|
|
194
|
+
manifest = JSON.parse(manifestRaw);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
throw new Error(`failed to parse buildah inspect manifest for ${imageRef}: ${error.message}`);
|
|
197
|
+
}
|
|
198
|
+
const imageID = normalizeSha256Digest(manifest?.config?.digest, `buildah inspect image id of ${imageRef}`);
|
|
199
|
+
|
|
200
|
+
let baseName = '';
|
|
201
|
+
let baseDigest = '';
|
|
202
|
+
const imageAnnotations = inspect?.ImageAnnotations && typeof inspect.ImageAnnotations === 'object' ? inspect.ImageAnnotations : {};
|
|
203
|
+
const manifestAnnotations = manifest?.annotations && typeof manifest.annotations === 'object' ? manifest.annotations : {};
|
|
204
|
+
baseName = String(imageAnnotations['org.opencontainers.image.base.name'] ?? '').trim();
|
|
205
|
+
baseDigest = String(imageAnnotations['org.opencontainers.image.base.digest'] ?? '').trim();
|
|
206
|
+
if (!baseName) {
|
|
207
|
+
baseName = String(manifestAnnotations['org.opencontainers.image.base.name'] ?? '').trim();
|
|
208
|
+
}
|
|
209
|
+
if (!baseDigest) {
|
|
210
|
+
baseDigest = String(manifestAnnotations['org.opencontainers.image.base.digest'] ?? '').trim();
|
|
211
|
+
}
|
|
212
|
+
if (!baseName) {
|
|
213
|
+
baseName = String(inspect?.FromImage ?? '').trim();
|
|
214
|
+
}
|
|
215
|
+
if (!baseDigest) {
|
|
216
|
+
baseDigest = String(inspect?.FromImageDigest ?? '').trim();
|
|
217
|
+
}
|
|
218
|
+
if (baseDigest) {
|
|
219
|
+
baseDigest = normalizeSha256Digest(baseDigest, `buildah inspect base digest of ${imageRef}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
imageID,
|
|
224
|
+
diffIDs,
|
|
225
|
+
baseName,
|
|
226
|
+
baseDigest,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function resolveFinalExternalImageFromDockerfile(dockerfilePath) {
|
|
231
|
+
const ast = DockerfileParser.parse(fs.readFileSync(dockerfilePath, 'utf8'));
|
|
232
|
+
const stages = [];
|
|
233
|
+
const stageIndexByName = new Map();
|
|
234
|
+
for (const instruction of ast.getInstructions()) {
|
|
235
|
+
if (instruction.getInstruction() !== 'FROM') {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const image = String(instruction.getImage() ?? '').trim();
|
|
239
|
+
if (!image) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const stageName = String(instruction.getBuildStage() ?? '').trim().toLowerCase();
|
|
243
|
+
const stage = {
|
|
244
|
+
image,
|
|
245
|
+
stageName,
|
|
246
|
+
};
|
|
247
|
+
stages.push(stage);
|
|
248
|
+
if (stageName) {
|
|
249
|
+
stageIndexByName.set(stageName, stage);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (stages.length === 0) {
|
|
253
|
+
return '';
|
|
254
|
+
}
|
|
255
|
+
let current = stages.at(-1)?.image ?? '';
|
|
256
|
+
const visited = new Set();
|
|
257
|
+
while (current) {
|
|
258
|
+
const key = current.trim().toLowerCase();
|
|
259
|
+
if (!key || visited.has(key)) {
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
visited.add(key);
|
|
263
|
+
const stage = stageIndexByName.get(key);
|
|
264
|
+
if (!stage) {
|
|
265
|
+
return current;
|
|
266
|
+
}
|
|
267
|
+
current = stage.image;
|
|
268
|
+
}
|
|
269
|
+
return current;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function inspectLocalDockerImage(imageRef) {
|
|
273
|
+
const { stdout } = await runLocalCommand('docker', ['image', 'inspect', imageRef], {
|
|
274
|
+
quietStdout: true,
|
|
275
|
+
quietStderr: true,
|
|
276
|
+
});
|
|
277
|
+
const item = parseInspectArray(stdout, imageRef, 'docker image inspect');
|
|
278
|
+
const imageID = normalizeSha256Digest(item.Id, `docker image inspect id of ${imageRef}`);
|
|
279
|
+
const diffIDs = Array.isArray(item?.RootFS?.Layers)
|
|
280
|
+
? item.RootFS.Layers.map((layer, index) => normalizeSha256Digest(layer, `docker image inspect rootfs layer ${index} of ${imageRef}`))
|
|
281
|
+
: [];
|
|
282
|
+
if (diffIDs.length === 0) {
|
|
283
|
+
throw new Error(`docker image inspect rootfs layers is empty for ${imageRef}`);
|
|
284
|
+
}
|
|
285
|
+
const repoDigests = Array.isArray(item?.RepoDigests) ? item.RepoDigests.map((item) => String(item ?? '').trim()).filter((item) => item) : [];
|
|
286
|
+
return {
|
|
287
|
+
imageID,
|
|
288
|
+
diffIDs,
|
|
289
|
+
repoDigests,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function tryInspectLocalBuildahImage(imageRef) {
|
|
294
|
+
try {
|
|
295
|
+
const { stdout } = await runLocalCommand('buildah', ['inspect', imageRef], {
|
|
296
|
+
quietStdout: true,
|
|
297
|
+
quietStderr: true,
|
|
298
|
+
});
|
|
299
|
+
return parseBuildahInspectOutput(stdout, imageRef);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
logger.debug(`skip buildah inspect for ${imageRef}: ${error.message}`);
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function deriveLocalUpstreamFromBuildah(imageRef, upstreamMatch, builtDiffIDs) {
|
|
307
|
+
const inspected = await tryInspectLocalBuildahImage(imageRef);
|
|
308
|
+
if (!inspected?.baseName || !inspected?.baseDigest) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
const baseRef = `${stripRepoDigest(inspected.baseName)}@${inspected.baseDigest}`;
|
|
312
|
+
const baseInfo = await inspectLocalDockerImage(baseRef);
|
|
313
|
+
const upstream = pickRepoDigest(baseInfo.repoDigests, upstreamMatch, inspected.baseDigest);
|
|
314
|
+
if (!upstream) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
if (!startsWithDigestList(builtDiffIDs, baseInfo.diffIDs)) {
|
|
318
|
+
logger.warn(`ignore local upstream derived from buildah, built layers do not start with upstream layers: ${imageRef}`);
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
upstream,
|
|
323
|
+
upstreamDiffIDs: baseInfo.diffIDs,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function deriveLocalUpstreamFromDockerfile(dockerfilePath, upstreamMatch, builtDiffIDs) {
|
|
328
|
+
const baseRef = resolveFinalExternalImageFromDockerfile(dockerfilePath);
|
|
329
|
+
if (!baseRef) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
const baseInfo = await inspectLocalDockerImage(baseRef);
|
|
333
|
+
const upstream = pickRepoDigest(baseInfo.repoDigests, upstreamMatch);
|
|
334
|
+
if (!upstream) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
if (!startsWithDigestList(builtDiffIDs, baseInfo.diffIDs)) {
|
|
338
|
+
logger.warn(`ignore local upstream derived from Dockerfile, built layers do not start with upstream layers: ${baseRef}`);
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
upstream,
|
|
343
|
+
upstreamDiffIDs: baseInfo.diffIDs,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function normalizeTargetPlatform(value) {
|
|
348
|
+
const normalized = String(value ?? '').trim().toLowerCase();
|
|
349
|
+
if (!/^[a-z0-9]+\/[a-z0-9]+$/.test(normalized)) {
|
|
350
|
+
throw new Error(`invalid target platform: ${value}`);
|
|
351
|
+
}
|
|
352
|
+
return normalized;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export async function ensureDockerBuildxAvailable() {
|
|
356
|
+
await runLocalCommand('docker', ['buildx', 'version'], {
|
|
357
|
+
quietStdout: true,
|
|
358
|
+
quietStderr: true,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function buildLocalImageForPack(entry, targetPlatform, dockerfilePath = '') {
|
|
363
|
+
const platform = normalizeTargetPlatform(targetPlatform);
|
|
364
|
+
await ensureDockerBuildxAvailable();
|
|
365
|
+
const imageRef = `debug.bridge/${entry.imageLabel}`;
|
|
366
|
+
await runLocalCommand(
|
|
367
|
+
'docker',
|
|
368
|
+
['buildx', 'build', '--platform', platform, '--load', '--tag', imageRef, '--file', 'Dockerfile', '.'],
|
|
369
|
+
{
|
|
370
|
+
cwd: entry.contextDir,
|
|
371
|
+
env: {
|
|
372
|
+
DOCKER_BUILDKIT: '1',
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
);
|
|
376
|
+
const inspected = await inspectLocalDockerImage(imageRef);
|
|
377
|
+
let upstream = '';
|
|
378
|
+
let upstreamDiffIDs = [];
|
|
379
|
+
if (String(entry?.upstreamMatch ?? '').trim()) {
|
|
380
|
+
const derivedFromBuildah = await deriveLocalUpstreamFromBuildah(imageRef, entry.upstreamMatch, inspected.diffIDs);
|
|
381
|
+
const derived = derivedFromBuildah ?? (dockerfilePath ? await deriveLocalUpstreamFromDockerfile(dockerfilePath, entry.upstreamMatch, inspected.diffIDs) : null);
|
|
382
|
+
if (derived) {
|
|
383
|
+
upstream = derived.upstream;
|
|
384
|
+
upstreamDiffIDs = derived.upstreamDiffIDs;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
ref: imageRef,
|
|
389
|
+
imageID: inspected.imageID,
|
|
390
|
+
builtDiffIDs: inspected.diffIDs,
|
|
391
|
+
upstream,
|
|
392
|
+
upstreamDiffIDs,
|
|
393
|
+
archiveKey: '',
|
|
394
|
+
platform,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export async function saveLocalDockerImagesArchive(imageRefs, archivePath) {
|
|
399
|
+
const refs = [...new Set(imageRefs.map((item) => String(item ?? '').trim()).filter((item) => item))];
|
|
400
|
+
if (refs.length === 0) {
|
|
401
|
+
throw new Error('local image refs is empty');
|
|
402
|
+
}
|
|
403
|
+
await runLocalCommand('docker', ['image', 'save', ...refs], {
|
|
404
|
+
stdoutFile: archivePath,
|
|
405
|
+
quietStderr: true,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export async function removeLocalDockerImages(imageRefs) {
|
|
410
|
+
const refs = [...new Set(imageRefs.map((item) => String(item ?? '').trim()).filter((item) => item))];
|
|
411
|
+
for (const ref of refs) {
|
|
412
|
+
try {
|
|
413
|
+
await runLocalCommand('docker', ['image', 'rm', '-f', ref], {
|
|
414
|
+
quietStdout: true,
|
|
415
|
+
quietStderr: true,
|
|
416
|
+
});
|
|
417
|
+
} catch (error) {
|
|
418
|
+
logger.debug(`ignore local docker image cleanup failure for ${ref}: ${error.message}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export const __test__ = {
|
|
424
|
+
resolveFinalExternalImageFromDockerfile,
|
|
425
|
+
};
|