@izykitten/ce 0.0.3-dev.3 → 0.0.3-dev.5
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 +60 -23
- package/dist/ce.js +948 -88
- package/package.json +1 -1
package/dist/ce.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
// src/command.ts
|
|
2
2
|
var VALUE_OPTIONS = {
|
|
3
3
|
docker: {
|
|
4
|
-
long: new Set([
|
|
4
|
+
long: new Set([
|
|
5
|
+
"config",
|
|
6
|
+
"context",
|
|
7
|
+
"host",
|
|
8
|
+
"log-level",
|
|
9
|
+
"tlscacert",
|
|
10
|
+
"tlscert",
|
|
11
|
+
"tlskey",
|
|
12
|
+
"builder"
|
|
13
|
+
]),
|
|
5
14
|
short: "cHl"
|
|
6
15
|
},
|
|
7
16
|
podman: {
|
|
@@ -86,7 +95,11 @@ var COMMANDS = {
|
|
|
86
95
|
{ words: ["buildx", "b"] },
|
|
87
96
|
{ words: ["buildx", "imagetools", "create"], tagsOnly: true }
|
|
88
97
|
],
|
|
89
|
-
podman: [
|
|
98
|
+
podman: [
|
|
99
|
+
{ words: ["build"] },
|
|
100
|
+
{ words: ["image", "build"] },
|
|
101
|
+
{ words: ["buildx", "build"] }
|
|
102
|
+
],
|
|
90
103
|
buildah: [{ words: ["build"] }, { words: ["bud"] }]
|
|
91
104
|
};
|
|
92
105
|
function findBuildCommand(engine, argv) {
|
|
@@ -111,6 +124,10 @@ function findBuildCommand(engine, argv) {
|
|
|
111
124
|
return null;
|
|
112
125
|
}
|
|
113
126
|
|
|
127
|
+
// src/containerfile.ts
|
|
128
|
+
import fs from "node:fs";
|
|
129
|
+
import path from "node:path";
|
|
130
|
+
|
|
114
131
|
// src/errors.ts
|
|
115
132
|
class CeError extends Error {
|
|
116
133
|
name = "CeError";
|
|
@@ -125,16 +142,770 @@ function errorMessage(exc) {
|
|
|
125
142
|
return exc instanceof Error ? exc.message : String(exc);
|
|
126
143
|
}
|
|
127
144
|
|
|
145
|
+
// src/containerfile.ts
|
|
146
|
+
class Unreadable extends Error {
|
|
147
|
+
located = false;
|
|
148
|
+
}
|
|
149
|
+
var fail = (message) => {
|
|
150
|
+
throw new Unreadable(message);
|
|
151
|
+
};
|
|
152
|
+
var STANDARD_SYNTAX = /^(?:docker\.io\/)?docker\/dockerfile(?:-upstream)?(?::(?:1|latest|labs|master|1-labs|master-labs|1\.(?:2[1-9]|[3-9]\d|\d{3,})(?:\.\d+)?(?:-[\w.]+)?(?:@sha256:[0-9a-f]{64})?))?$/u;
|
|
153
|
+
var isRemote = (value) => value === "-" || value.includes("://") || /^(?:git@|github\.com\/)/u.test(value);
|
|
154
|
+
var familiar = (name) => name.replace(/^(?:(?:index\.)?docker\.io\/)?(?:library\/(?=[^/]+$))?/u, "").replace(/:latest$/u, "");
|
|
155
|
+
function findContainerfile(files, cwd = process.cwd()) {
|
|
156
|
+
try {
|
|
157
|
+
const file = locate(files, cwd);
|
|
158
|
+
if (files.engine !== "docker" && file.endsWith(".in"))
|
|
159
|
+
fail(`${files.engine} runs ${JSON.stringify(file)} through cpp, which ce does not`);
|
|
160
|
+
return file;
|
|
161
|
+
} catch (exc) {
|
|
162
|
+
if (exc instanceof Unreadable)
|
|
163
|
+
throw exc;
|
|
164
|
+
return fail(`ce cannot look for the Containerfile: ${errorMessage(exc)}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function locate({ engine, files, positionals }, cwd) {
|
|
168
|
+
const docker = engine === "docker";
|
|
169
|
+
if (positionals.length > 1)
|
|
170
|
+
fail(`ce cannot tell which of ${positionals.map((p) => JSON.stringify(p)).join(", ")} is the build context`);
|
|
171
|
+
const context = positionals[0] ?? (docker ? fail("the command has no build context") : ".");
|
|
172
|
+
if (isRemote(context))
|
|
173
|
+
fail(`the build context ${JSON.stringify(context)} is not a local directory`);
|
|
174
|
+
const contextDir = path.resolve(cwd, context);
|
|
175
|
+
if (!docker && (files.length > 1 || files.some((f) => f.includes(","))))
|
|
176
|
+
fail(`ce does not read ${engine}'s several -f files`);
|
|
177
|
+
const file = files.at(-1) || undefined;
|
|
178
|
+
if (file === undefined && files.length && !docker)
|
|
179
|
+
fail("the -f is empty");
|
|
180
|
+
if (docker) {
|
|
181
|
+
if (file !== undefined && isRemote(file))
|
|
182
|
+
fail(`-f ${JSON.stringify(file)} is not a local file`);
|
|
183
|
+
return lowercaseDockerfile(file === undefined ? path.join(contextDir, "Dockerfile") : path.resolve(cwd, file));
|
|
184
|
+
}
|
|
185
|
+
if (file !== undefined) {
|
|
186
|
+
if (isRemote(file))
|
|
187
|
+
fail(`-f ${JSON.stringify(file)} is not a local file`);
|
|
188
|
+
const given = path.resolve(cwd, file);
|
|
189
|
+
return fs.existsSync(given) ? given : path.resolve(contextDir, file);
|
|
190
|
+
}
|
|
191
|
+
if (fs.statSync(contextDir, { throwIfNoEntry: false })?.isFile())
|
|
192
|
+
return contextDir;
|
|
193
|
+
const containerfile = path.join(contextDir, "Containerfile");
|
|
194
|
+
return fs.existsSync(containerfile) ? containerfile : path.join(contextDir, "Dockerfile");
|
|
195
|
+
}
|
|
196
|
+
function lowercaseDockerfile(file) {
|
|
197
|
+
if (path.basename(file) !== "Dockerfile")
|
|
198
|
+
return file;
|
|
199
|
+
let names;
|
|
200
|
+
try {
|
|
201
|
+
names = fs.readdirSync(path.dirname(file));
|
|
202
|
+
} catch {
|
|
203
|
+
return file;
|
|
204
|
+
}
|
|
205
|
+
return !names.includes("Dockerfile") && names.includes("dockerfile") ? path.join(path.dirname(file), "dockerfile") : file;
|
|
206
|
+
}
|
|
207
|
+
var SPACE = "\\t\\n\\v\\f\\r \\u0085\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000";
|
|
208
|
+
var IS_SPACE = new RegExp(`[${SPACE}]`, "u");
|
|
209
|
+
var TRIM_START = new RegExp(`^[${SPACE}]+`, "u");
|
|
210
|
+
var UNUSUAL_SPACE = /[\u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/u;
|
|
211
|
+
var NON_ASCII = /[^\0-\x7f]/u;
|
|
212
|
+
function instructionOf(word, line) {
|
|
213
|
+
if (NON_ASCII.test(word))
|
|
214
|
+
fail(`ce cannot read the instruction ${JSON.stringify(word)} on line ${line}`);
|
|
215
|
+
return word.toUpperCase();
|
|
216
|
+
}
|
|
217
|
+
var stageKey = (name) => NON_ASCII.test(name) ? fail(`ce reads only ASCII stage names, not ${JSON.stringify(name)}`) : name.toLowerCase();
|
|
218
|
+
function trimEnd(text, space) {
|
|
219
|
+
let end = text.length;
|
|
220
|
+
while (end > 0 && space.test(text[end - 1]))
|
|
221
|
+
end--;
|
|
222
|
+
return text.slice(0, end);
|
|
223
|
+
}
|
|
224
|
+
var trim = (text) => trimEnd(text.replace(TRIM_START, ""), IS_SPACE);
|
|
225
|
+
var COMMAND = /^([^\t\v\f\r ]+)[\t\v\f\r ]*([\s\S]*)$/u;
|
|
226
|
+
function words(text, escape, heredoc = false) {
|
|
227
|
+
const out = [];
|
|
228
|
+
let word = "";
|
|
229
|
+
let quote = "";
|
|
230
|
+
let inWord = false;
|
|
231
|
+
for (let i = 0;i < text.length; i++) {
|
|
232
|
+
const ch = text[i];
|
|
233
|
+
if (!quote && IS_SPACE.test(ch)) {
|
|
234
|
+
if (inWord)
|
|
235
|
+
out.push(word);
|
|
236
|
+
[word, inWord] = ["", false];
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
inWord = true;
|
|
240
|
+
if (ch === escape && quote !== "'" && i + 1 === text.length)
|
|
241
|
+
continue;
|
|
242
|
+
word += ch;
|
|
243
|
+
if (heredoc && !quote && ch === "<" && text[i + 1] === "<") {
|
|
244
|
+
word += text[++i];
|
|
245
|
+
while (/[ \t\r]/u.test(text[i + 1] ?? ""))
|
|
246
|
+
word += text[++i];
|
|
247
|
+
} else if (ch === escape && quote !== "'" && i + 1 < text.length)
|
|
248
|
+
word += text[++i];
|
|
249
|
+
else if (ch === quote)
|
|
250
|
+
quote = "";
|
|
251
|
+
else if (!quote && (ch === '"' || ch === "'"))
|
|
252
|
+
quote = ch;
|
|
253
|
+
}
|
|
254
|
+
if (quote)
|
|
255
|
+
fail(`unterminated ${quote} in ${JSON.stringify(text)}`);
|
|
256
|
+
if (inWord)
|
|
257
|
+
out.push(word);
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
var HEREDOC_INSTRUCTIONS = ["RUN", "COPY", "ADD"];
|
|
261
|
+
function heredocs(keyword, rest, line, buildkit) {
|
|
262
|
+
while (keyword === "ONBUILD") {
|
|
263
|
+
const [, inner = "", innerRest = ""] = COMMAND.exec(trim(rest)) ?? [];
|
|
264
|
+
[keyword, rest] = [instructionOf(inner, line), innerRest];
|
|
265
|
+
}
|
|
266
|
+
if (!HEREDOC_INSTRUCTIONS.includes(keyword) || !rest.includes("<<"))
|
|
267
|
+
return [];
|
|
268
|
+
const body = rest.replace(/^(?:--\S+\s+)*/u, "").replace(TRIM_START, "");
|
|
269
|
+
if (body.startsWith("[")) {
|
|
270
|
+
const json = buildkit ? body : jsonPrefix(body);
|
|
271
|
+
try {
|
|
272
|
+
const array = json === undefined ? undefined : JSON.parse(json);
|
|
273
|
+
if (Array.isArray(array) && array.every((v) => typeof v === "string"))
|
|
274
|
+
return [];
|
|
275
|
+
} catch {}
|
|
276
|
+
}
|
|
277
|
+
const unsure = () => fail(`ce cannot tell which heredocs the << on line ${line} starts`);
|
|
278
|
+
if (UNUSUAL_SPACE.test(rest) || !plainBraces(rest))
|
|
279
|
+
unsure();
|
|
280
|
+
const found = [];
|
|
281
|
+
let split;
|
|
282
|
+
try {
|
|
283
|
+
split = words(rest, "\\", true);
|
|
284
|
+
} catch {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
for (const word of split) {
|
|
288
|
+
const match = /^\d*<<(-?)[\t\n\f\r ]*([^<]*)$/u.exec(word);
|
|
289
|
+
if (!match?.[2])
|
|
290
|
+
continue;
|
|
291
|
+
const delimiter = /^(["']?)([^\s"'\\`$]+)\1$/u.exec(match[2]);
|
|
292
|
+
found.push({ name: delimiter?.[2] ?? unsure(), tabs: match[1] === "-" });
|
|
293
|
+
}
|
|
294
|
+
return found;
|
|
295
|
+
}
|
|
296
|
+
var PLAIN_BRACES = /\$\{[A-Za-z_][A-Za-z0-9_]*(?:(?::[-+?]|[-+?#%])[^\s"'\\`<${}]*)?\}/uy;
|
|
297
|
+
var QUOTED_BRACES = /\$\{[A-Za-z_][A-Za-z0-9_]*(?:(?::[-+?]|[-+?#%])[^\n"'\\`<${}]*)?\}/uy;
|
|
298
|
+
function plainBraces(text) {
|
|
299
|
+
let quote = "";
|
|
300
|
+
for (let i = 0;i < text.length; i++) {
|
|
301
|
+
const ch = text[i];
|
|
302
|
+
if (ch === "\\" && quote !== "'")
|
|
303
|
+
i++;
|
|
304
|
+
else if (quote && ch === quote)
|
|
305
|
+
quote = "";
|
|
306
|
+
else if (!quote && (ch === '"' || ch === "'"))
|
|
307
|
+
quote = ch;
|
|
308
|
+
else if (quote !== "'" && text.startsWith("${", i)) {
|
|
309
|
+
const braces = quote ? QUOTED_BRACES : PLAIN_BRACES;
|
|
310
|
+
braces.lastIndex = i;
|
|
311
|
+
const match = braces.exec(text);
|
|
312
|
+
if (!match)
|
|
313
|
+
return false;
|
|
314
|
+
i += match[0].length - 1;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
function jsonPrefix(text) {
|
|
320
|
+
let depth = 0;
|
|
321
|
+
for (let i = 0;i < text.length; i++) {
|
|
322
|
+
const ch = text[i];
|
|
323
|
+
if (ch === '"') {
|
|
324
|
+
for (i++;i < text.length && text[i] !== '"'; i++)
|
|
325
|
+
if (text[i] === "\\")
|
|
326
|
+
i++;
|
|
327
|
+
} else if (ch === "[" || ch === "{")
|
|
328
|
+
depth++;
|
|
329
|
+
else if ((ch === "]" || ch === "}") && --depth === 0)
|
|
330
|
+
return text.slice(0, i + 1);
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
var DIRECTIVE_SPACE = /[\t\n\f\r ]/u;
|
|
335
|
+
function directive(text) {
|
|
336
|
+
const match = /^([a-zA-Z][a-zA-Z0-9]*)[\t\n\f\r ]*=[\t\n\f\r ]*([^\n]+)$/u.exec(trimEnd(text, DIRECTIVE_SPACE));
|
|
337
|
+
return match ? { key: match[1].toLowerCase(), value: match[2] } : undefined;
|
|
338
|
+
}
|
|
339
|
+
var DIRECTIVES = ["syntax", "escape", "check"];
|
|
340
|
+
function detectSyntax(lines) {
|
|
341
|
+
if (lines[0]?.startsWith("#!"))
|
|
342
|
+
lines = lines.slice(1);
|
|
343
|
+
for (const prefix of ["#", "//"]) {
|
|
344
|
+
const seen = new Set;
|
|
345
|
+
for (const line of lines) {
|
|
346
|
+
if (!line.startsWith(prefix))
|
|
347
|
+
break;
|
|
348
|
+
const match = directive(line.slice(prefix.length).replace(TRIM_START, ""));
|
|
349
|
+
if (!match || !DIRECTIVES.includes(match.key) || seen.has(match.key))
|
|
350
|
+
break;
|
|
351
|
+
seen.add(match.key);
|
|
352
|
+
if (match.key === "syntax")
|
|
353
|
+
return match.value.split(" ")[0];
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
function pair(word) {
|
|
359
|
+
const eq = word.indexOf("=");
|
|
360
|
+
return eq < 0 ? { name: word, value: undefined } : { name: word.slice(0, eq), value: word.slice(eq + 1) };
|
|
361
|
+
}
|
|
362
|
+
function parseContainerfile(text, model = "buildkit") {
|
|
363
|
+
const buildkit = model === "buildkit";
|
|
364
|
+
const lines = text.replace(/^\ufeff/u, "").split(`
|
|
365
|
+
`).map((line, at) => line.endsWith("\r\r") ? fail(`ce reads only LF and CRLF line ends, not line ${at + 1}'s`) : line.replace(/\r$/u, ""));
|
|
366
|
+
let escape = "\\";
|
|
367
|
+
let i = 0;
|
|
368
|
+
for (let seen = false;!buildkit && i < lines.length; i++) {
|
|
369
|
+
const directive = /^#[ \t]*escape[ \t]*=[ \t]*([^\n])/u.exec(lines[i].replace(TRIM_START, "").toLowerCase());
|
|
370
|
+
if (!directive)
|
|
371
|
+
break;
|
|
372
|
+
if (seen)
|
|
373
|
+
fail("there is more than one escape directive");
|
|
374
|
+
seen = true;
|
|
375
|
+
escape = directive[1];
|
|
376
|
+
if (escape !== "\\" && escape !== "`")
|
|
377
|
+
fail(`the escape directive ${JSON.stringify(escape)} is not \\ or \``);
|
|
378
|
+
}
|
|
379
|
+
for (const seen = new Set;buildkit && i < lines.length; i++) {
|
|
380
|
+
const line = lines[i].replace(TRIM_START, "");
|
|
381
|
+
const found = line.startsWith("#") ? directive(line.slice(1).replace(TRIM_START, "")) : undefined;
|
|
382
|
+
if (!found || !DIRECTIVES.includes(found.key) || seen.has(found.key))
|
|
383
|
+
break;
|
|
384
|
+
seen.add(found.key);
|
|
385
|
+
if (found.key === "escape") {
|
|
386
|
+
const { value } = found;
|
|
387
|
+
if (value !== "\\" && value !== "`")
|
|
388
|
+
fail(`the escape directive ${JSON.stringify(value)} is not \\ or \``);
|
|
389
|
+
escape = value;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const e = escape === "\\" ? "\\\\" : escape;
|
|
393
|
+
const continued = buildkit ? new RegExp(`([^${e}])${e}[ \\t]*$|^${e}[ \\t]*$`, "u") : new RegExp(`()${e}[ \\t]*$`, "u");
|
|
394
|
+
const parsed = {
|
|
395
|
+
escape,
|
|
396
|
+
syntax: detectSyntax(lines),
|
|
397
|
+
globals: [],
|
|
398
|
+
stages: []
|
|
399
|
+
};
|
|
400
|
+
while (i < lines.length) {
|
|
401
|
+
let start = 0;
|
|
402
|
+
let line = "";
|
|
403
|
+
for (;i < lines.length; i++) {
|
|
404
|
+
const physical = lines[i];
|
|
405
|
+
if (/^(?:#|$)/u.test(trim(physical)))
|
|
406
|
+
continue;
|
|
407
|
+
start ||= i + 1;
|
|
408
|
+
if (continued.test(physical)) {
|
|
409
|
+
line += physical.replace(continued, "$1");
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
line += physical;
|
|
413
|
+
i++;
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
line = trim(line);
|
|
417
|
+
if (!line)
|
|
418
|
+
continue;
|
|
419
|
+
const [instruction = "", rest = ""] = COMMAND.exec(line).slice(1);
|
|
420
|
+
const keyword = instructionOf(instruction, start);
|
|
421
|
+
for (const { name, tabs } of heredocs(keyword, rest, start, buildkit)) {
|
|
422
|
+
for (;; i++) {
|
|
423
|
+
if (i >= lines.length)
|
|
424
|
+
fail(`the heredoc ${name} from line ${start} never ends`);
|
|
425
|
+
const end = lines[i];
|
|
426
|
+
if ((tabs ? end.replace(/^\t+/u, "") : end) === name) {
|
|
427
|
+
i++;
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const [, onbuild = "", onbuildRest = ""] = keyword === "ONBUILD" ? COMMAND.exec(trim(rest)) ?? [] : [];
|
|
433
|
+
const kind = onbuild ? instructionOf(onbuild, start) : keyword;
|
|
434
|
+
const args = onbuild ? onbuildRest : rest;
|
|
435
|
+
if (keyword !== "FROM" && kind !== "ARG" && kind !== "ENV")
|
|
436
|
+
continue;
|
|
437
|
+
if (UNUSUAL_SPACE.test(line))
|
|
438
|
+
fail(`ce reads only ASCII space in the ${kind} on line ${start}`);
|
|
439
|
+
const split = onbuild && buildkit ? "\\" : escape;
|
|
440
|
+
const stage = parsed.stages.at(-1);
|
|
441
|
+
if (keyword === "FROM") {
|
|
442
|
+
const all = words(rest, buildkit ? escape : "\\");
|
|
443
|
+
let flags = 0;
|
|
444
|
+
while (all[flags]?.startsWith("--"))
|
|
445
|
+
if (all[flags++] === "--")
|
|
446
|
+
break;
|
|
447
|
+
const parts = all.slice(flags);
|
|
448
|
+
if (!(parts.length === 1 || parts.length === 3 && /^[Aa][Ss]$/u.test(parts[1])) || parts.some((part) => IS_SPACE.test(part)))
|
|
449
|
+
fail(`ce cannot read the FROM on line ${start}`);
|
|
450
|
+
parsed.stages.push({
|
|
451
|
+
name: parts[2] ?? "",
|
|
452
|
+
line: start,
|
|
453
|
+
base: parts[0],
|
|
454
|
+
instructions: []
|
|
455
|
+
});
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const set = words(args, split);
|
|
459
|
+
if (set[0]?.startsWith("--"))
|
|
460
|
+
fail(`ce does not read the flags of the ${kind} on line ${start}`);
|
|
461
|
+
const from = {
|
|
462
|
+
line: start,
|
|
463
|
+
...onbuild !== "" && { onbuild: true }
|
|
464
|
+
};
|
|
465
|
+
if (kind === "ARG" && (stage || !onbuild)) {
|
|
466
|
+
if (!set.length)
|
|
467
|
+
fail(`the ARG on line ${start} names nothing`);
|
|
468
|
+
(stage?.instructions ?? parsed.globals).push({
|
|
469
|
+
kind: "ARG",
|
|
470
|
+
pairs: set.map(pair),
|
|
471
|
+
...from
|
|
472
|
+
});
|
|
473
|
+
} else if (kind === "ENV" && stage) {
|
|
474
|
+
const legacy = set.length > 0 && !set[0].includes("=");
|
|
475
|
+
stage.instructions.push({
|
|
476
|
+
kind: "ENV",
|
|
477
|
+
pairs: legacy ? [{ name: set[0], value: undefined }] : set.map(pair),
|
|
478
|
+
...from
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return parsed;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
class Unknown {
|
|
486
|
+
why;
|
|
487
|
+
reason;
|
|
488
|
+
constructor(why, reason = "") {
|
|
489
|
+
this.why = why;
|
|
490
|
+
this.reason = reason;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
var BASE_ENV = new Unknown("base");
|
|
494
|
+
var BUILTINS = [
|
|
495
|
+
"BUILDPLATFORM",
|
|
496
|
+
"BUILDOS",
|
|
497
|
+
"BUILDOSVERSION",
|
|
498
|
+
"BUILDARCH",
|
|
499
|
+
"BUILDVARIANT",
|
|
500
|
+
"TARGETPLATFORM",
|
|
501
|
+
"TARGETOS",
|
|
502
|
+
"TARGETOSVERSION",
|
|
503
|
+
"TARGETARCH",
|
|
504
|
+
"TARGETVARIANT",
|
|
505
|
+
"TARGETSTAGE"
|
|
506
|
+
];
|
|
507
|
+
var INJECTED = /^(?:SOURCE_DATE_EPOCH|(?:HTTPS?|FTP|NO|ALL)_PROXY)$/iu;
|
|
508
|
+
var PROXIES = /^(?:(?:HTTPS?|FTP|NO)_PROXY|(?:https?|ftp|no)_proxy)$/u;
|
|
509
|
+
var NON_ENV = ["BUILDKIT_SBOM_SCAN_CONTEXT", "BUILDKIT_SBOM_SCAN_STAGE"];
|
|
510
|
+
function expandWord(word, escape, model, lookup) {
|
|
511
|
+
const buildkit = model === "buildkit";
|
|
512
|
+
let i = 0;
|
|
513
|
+
let depth = 0;
|
|
514
|
+
let unused = 0;
|
|
515
|
+
const value = (name) => {
|
|
516
|
+
if (unused)
|
|
517
|
+
return;
|
|
518
|
+
const v = lookup(name);
|
|
519
|
+
if (v instanceof Unknown) {
|
|
520
|
+
if (v.why !== "error")
|
|
521
|
+
fail(`ce cannot tell what $${name} is there`);
|
|
522
|
+
const exc = new Unreadable(v.reason);
|
|
523
|
+
exc.located = true;
|
|
524
|
+
throw exc;
|
|
525
|
+
}
|
|
526
|
+
return v;
|
|
527
|
+
};
|
|
528
|
+
const readName = () => {
|
|
529
|
+
const rest = word.slice(i);
|
|
530
|
+
const name = (buildkit ? /^(?:[0-9]+|[@*#?\-$!]|[A-Za-z_][A-Za-z0-9_]*)/u.exec(rest)?.[0] : /^(?:[0-9]|[A-Za-z0-9_]+)/u.exec(rest)?.[0]) ?? "";
|
|
531
|
+
const after = rest.slice(name.length);
|
|
532
|
+
if (NON_ASCII.test(after[0] ?? "") && /^[\p{L}\p{Nd}]/u.test(after))
|
|
533
|
+
fail(`ce reads only ASCII names after a $, not in ${JSON.stringify(word)}`);
|
|
534
|
+
i += name.length;
|
|
535
|
+
return name;
|
|
536
|
+
};
|
|
537
|
+
const unsupported = () => fail(`ce does not evaluate the \${} in ${JSON.stringify(word)}`);
|
|
538
|
+
const dollar = () => {
|
|
539
|
+
i++;
|
|
540
|
+
if (word[i] !== "{") {
|
|
541
|
+
const name = readName();
|
|
542
|
+
return name ? value(name) ?? "" : "$";
|
|
543
|
+
}
|
|
544
|
+
i++;
|
|
545
|
+
const name = readName();
|
|
546
|
+
if (!name)
|
|
547
|
+
unsupported();
|
|
548
|
+
if (word[i] === "}") {
|
|
549
|
+
i++;
|
|
550
|
+
return value(name) ?? "";
|
|
551
|
+
}
|
|
552
|
+
const op = (buildkit ? /^:?[-+]/u : /^:[-+]/u).exec(word.slice(i))?.[0];
|
|
553
|
+
if (!op)
|
|
554
|
+
unsupported();
|
|
555
|
+
i += op.length;
|
|
556
|
+
const v = value(name);
|
|
557
|
+
const set = op.startsWith(":") ? v !== undefined && v !== "" : v !== undefined;
|
|
558
|
+
const used = op.endsWith("-") ? !set : set;
|
|
559
|
+
if (++depth > 100)
|
|
560
|
+
fail("ce does not evaluate ${} nested this deeply");
|
|
561
|
+
if (!used)
|
|
562
|
+
unused++;
|
|
563
|
+
const alternative = until("}");
|
|
564
|
+
if (!used)
|
|
565
|
+
unused--;
|
|
566
|
+
depth--;
|
|
567
|
+
return used ? alternative : set ? v : "";
|
|
568
|
+
};
|
|
569
|
+
const until = (end) => {
|
|
570
|
+
let out = "";
|
|
571
|
+
while (i < word.length) {
|
|
572
|
+
const ch = word[i];
|
|
573
|
+
if (end && ch === end) {
|
|
574
|
+
i++;
|
|
575
|
+
return out;
|
|
576
|
+
}
|
|
577
|
+
if (ch === escape) {
|
|
578
|
+
out += word[i + 1] ?? "";
|
|
579
|
+
i += 2;
|
|
580
|
+
} else if (ch === "'") {
|
|
581
|
+
const close = word.indexOf("'", i + 1);
|
|
582
|
+
if (close < 0)
|
|
583
|
+
fail(`unterminated ' in ${JSON.stringify(word)}`);
|
|
584
|
+
out += word.slice(i + 1, close);
|
|
585
|
+
i = close + 1;
|
|
586
|
+
} else if (ch === '"') {
|
|
587
|
+
i++;
|
|
588
|
+
for (;; ) {
|
|
589
|
+
if (i >= word.length)
|
|
590
|
+
fail(`unterminated " in ${JSON.stringify(word)}`);
|
|
591
|
+
const c = word[i];
|
|
592
|
+
if (c === '"') {
|
|
593
|
+
i++;
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
if (c === escape && ['"', "$", escape].includes(word[i + 1] ?? "")) {
|
|
597
|
+
out += word[i + 1];
|
|
598
|
+
i += 2;
|
|
599
|
+
} else if (c === "$")
|
|
600
|
+
out += dollar();
|
|
601
|
+
else {
|
|
602
|
+
out += c;
|
|
603
|
+
i++;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
} else if (ch === "$")
|
|
607
|
+
out += dollar();
|
|
608
|
+
else {
|
|
609
|
+
out += ch;
|
|
610
|
+
i++;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (end)
|
|
614
|
+
fail(`missing ${end} in ${JSON.stringify(word)}`);
|
|
615
|
+
return out;
|
|
616
|
+
};
|
|
617
|
+
return until("");
|
|
618
|
+
}
|
|
619
|
+
function argDefault(parsed, name, target, buildArg, model = "buildkit", { contexts = [], envs = [] } = {}) {
|
|
620
|
+
const buildkit = model === "buildkit";
|
|
621
|
+
const escape = buildkit ? parsed.escape : "\\";
|
|
622
|
+
const evaluate = (word, line, lookup) => {
|
|
623
|
+
try {
|
|
624
|
+
return expandWord(word, escape, model, lookup);
|
|
625
|
+
} catch (exc) {
|
|
626
|
+
if (!(exc instanceof Unreadable))
|
|
627
|
+
throw exc;
|
|
628
|
+
return new Unknown("error", exc.located ? exc.message : `${exc.message}, on line ${line}`);
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
const isBuiltin = (arg) => BUILTINS.includes(arg);
|
|
632
|
+
const plain = (name) => ![...`$'"`, escape].some((ch) => name.includes(ch));
|
|
633
|
+
const named = (kind, name, line) => plain(name) ? name : fail(`ce cannot tell what name the ${kind} on line ${line} sets`);
|
|
634
|
+
const globals = new Map;
|
|
635
|
+
for (const builtin of BUILTINS)
|
|
636
|
+
globals.set(builtin, buildArg(builtin) ?? new Unknown("builtin"));
|
|
637
|
+
for (const { pairs, line } of parsed.globals) {
|
|
638
|
+
const before = new Map(globals);
|
|
639
|
+
const seen = (ref) => {
|
|
640
|
+
const known = buildkit ? globals : before;
|
|
641
|
+
if (known.has(ref) || buildkit || !PROXIES.test(ref))
|
|
642
|
+
return known.get(ref);
|
|
643
|
+
return buildArg(ref) ?? new Unknown("injected");
|
|
644
|
+
};
|
|
645
|
+
for (const { name, value } of pairs) {
|
|
646
|
+
const arg = named("ARG", name, line);
|
|
647
|
+
const given = buildArg(arg);
|
|
648
|
+
if (given !== undefined)
|
|
649
|
+
globals.set(arg, given);
|
|
650
|
+
else if (INJECTED.test(arg))
|
|
651
|
+
globals.set(arg, new Unknown("injected"));
|
|
652
|
+
else if (value !== undefined && !(isBuiltin(arg) && !buildkit))
|
|
653
|
+
globals.set(arg, evaluate(value, line, seen));
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const findStage = (wanted, before, target = false) => {
|
|
657
|
+
const at = parsed.stages.findLastIndex((s, i) => i < before && (buildkit ? stageKey(s.name) === stageKey(wanted) : (s.name || String(i)) === wanted || !target && String(i) === wanted));
|
|
658
|
+
if (at >= 0 || buildkit || !target || !/^\d+$/u.test(wanted))
|
|
659
|
+
return at;
|
|
660
|
+
const position = Number(wanted);
|
|
661
|
+
return position < before ? position : -1;
|
|
662
|
+
};
|
|
663
|
+
const replaced = (index) => {
|
|
664
|
+
const stage = parsed.stages[index].name;
|
|
665
|
+
return buildkit && stage ? contexts.find((c) => stageKey(familiar(c)) === stageKey(stage)) : undefined;
|
|
666
|
+
};
|
|
667
|
+
for (const stage of parsed.stages) {
|
|
668
|
+
if (!buildkit && !plain(stage.name))
|
|
669
|
+
fail(`ce cannot tell what the stage ${JSON.stringify(stage.name)} is named`);
|
|
670
|
+
}
|
|
671
|
+
const source = (index) => {
|
|
672
|
+
const stage = parsed.stages[index];
|
|
673
|
+
const base = evaluate(stage.base, stage.line, (ref) => globals.get(ref));
|
|
674
|
+
let parent = typeof base === "string" ? findStage(base, index) : -1;
|
|
675
|
+
const swapped = buildkit ? parent >= 0 ? replaced(parent) : undefined : contexts.find((c) => c === stage.base || c === base);
|
|
676
|
+
if (swapped !== undefined)
|
|
677
|
+
parent = -1;
|
|
678
|
+
return { base, parent, swapped };
|
|
679
|
+
};
|
|
680
|
+
const states = new Map;
|
|
681
|
+
const run = (index) => {
|
|
682
|
+
const known = states.get(index);
|
|
683
|
+
if (known)
|
|
684
|
+
return known;
|
|
685
|
+
const stage = parsed.stages[index];
|
|
686
|
+
const { base, parent, swapped } = source(index);
|
|
687
|
+
const from = parent >= 0 ? run(parent) : undefined;
|
|
688
|
+
const state = {
|
|
689
|
+
env: new Map,
|
|
690
|
+
args: new Map,
|
|
691
|
+
image: new Map(from?.image),
|
|
692
|
+
opaque: from?.opaque ?? (swapped !== undefined || base !== "scratch")
|
|
693
|
+
};
|
|
694
|
+
const lost = (name) => new Unknown("error", `--build-context ${name} replaces what the FROM on line ${stage.line} builds from, so ce cannot tell what it sets`);
|
|
695
|
+
let triggers = [];
|
|
696
|
+
if (buildkit) {
|
|
697
|
+
if (base instanceof Unknown)
|
|
698
|
+
state.undeclared = new Unknown("error", `ce cannot tell what the FROM on line ${stage.line} is built from`);
|
|
699
|
+
else if (swapped !== undefined)
|
|
700
|
+
state.undeclared = lost(swapped);
|
|
701
|
+
else if (from) {
|
|
702
|
+
state.args = new Map(from.args);
|
|
703
|
+
state.undeclared = from.undeclared;
|
|
704
|
+
for (const [k, v] of from.env)
|
|
705
|
+
state.env.set(k, from.opaque || k === "PATH" ? BASE_ENV : v);
|
|
706
|
+
for (const [k, v] of from.image)
|
|
707
|
+
state.env.set(k, v);
|
|
708
|
+
triggers = parsed.stages[parent].instructions.filter((i) => i.onbuild);
|
|
709
|
+
}
|
|
710
|
+
} else {
|
|
711
|
+
if (from)
|
|
712
|
+
state.env = new Map(from.env);
|
|
713
|
+
for (const flag of envs) {
|
|
714
|
+
const spec = flag.slice(flag.indexOf(" ") + 1);
|
|
715
|
+
const names = flag.startsWith("--unsetenv ") ? spec.includes('"') ? ["*"] : spec.split(",") : [spec.split("=")[0].replace(/-$/u, "")];
|
|
716
|
+
const set = (k) => state.env.set(k, new Unknown("error", `${k} may be changed by ${flag}`));
|
|
717
|
+
for (const env of names) {
|
|
718
|
+
const prefix = flag.startsWith("--env ") && env.endsWith("*") ? env.slice(0, -1) : "";
|
|
719
|
+
if (env.includes("*"))
|
|
720
|
+
[...state.env.keys()].filter((k) => k.startsWith(prefix)).forEach(set);
|
|
721
|
+
else
|
|
722
|
+
set(env);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const written = findStage(stage.base, index);
|
|
726
|
+
if (swapped !== undefined)
|
|
727
|
+
state.undeclared = lost(swapped);
|
|
728
|
+
else if (written >= 0)
|
|
729
|
+
state.args = inheritedArgs(written);
|
|
730
|
+
for (const { kind, pairs, line } of parent >= 0 ? parsed.stages[parent].instructions.filter((i) => i.onbuild) : []) {
|
|
731
|
+
for (const { name: arg } of pairs) {
|
|
732
|
+
if (!plain(arg))
|
|
733
|
+
fail(`ce cannot tell what name the ONBUILD on line ${line} sets`);
|
|
734
|
+
(kind === "ENV" ? state.env : state.args).set(arg, new Unknown("error", `${arg} may be set by the ONBUILD on line ${line}, which ${model} runs only for some image formats`));
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
const see = (env) => (ref) => env.has(ref) ? env.get(ref) : !buildkit || state.opaque || ref === "PATH" ? BASE_ENV : undefined;
|
|
739
|
+
const own = stage.instructions.filter((i) => !i.onbuild);
|
|
740
|
+
for (const { kind, pairs, line } of [...triggers, ...own]) {
|
|
741
|
+
const before = new Map(state.env);
|
|
742
|
+
for (const { name, value } of pairs) {
|
|
743
|
+
const arg = named(kind, name, line);
|
|
744
|
+
const seen = buildkit && kind === "ARG" ? state.env : before;
|
|
745
|
+
const evaluated = (word) => evaluate(word, line, see(seen));
|
|
746
|
+
if (kind === "ENV") {
|
|
747
|
+
const v = value === undefined ? new Unknown("error", `ce does not read the ENV ${arg} on line ${line}`) : evaluated(value);
|
|
748
|
+
state.env.set(arg, v);
|
|
749
|
+
state.image.set(arg, v);
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
const given = buildArg(arg);
|
|
753
|
+
let v;
|
|
754
|
+
if (given !== undefined)
|
|
755
|
+
v = given;
|
|
756
|
+
else if (INJECTED.test(arg))
|
|
757
|
+
v = new Unknown("injected");
|
|
758
|
+
else if (isBuiltin(arg) && (!buildkit || value === undefined))
|
|
759
|
+
v = globals.get(arg);
|
|
760
|
+
else if (value !== undefined)
|
|
761
|
+
v = evaluated(value);
|
|
762
|
+
else if (globals.get(arg) !== undefined)
|
|
763
|
+
v = globals.get(arg);
|
|
764
|
+
else {
|
|
765
|
+
if (!state.args.has(arg))
|
|
766
|
+
state.args.set(arg, state.undeclared);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
state.args.set(arg, v);
|
|
770
|
+
if (buildkit && !NON_ENV.includes(arg))
|
|
771
|
+
state.env.set(arg, v);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
states.set(index, state);
|
|
775
|
+
return state;
|
|
776
|
+
};
|
|
777
|
+
const inherited = new Map;
|
|
778
|
+
const inheritedArgs = (index) => {
|
|
779
|
+
const todo = [];
|
|
780
|
+
let i = index;
|
|
781
|
+
while (i >= 0 && !inherited.has(i)) {
|
|
782
|
+
todo.unshift(i);
|
|
783
|
+
i = findStage(parsed.stages[i].base, i);
|
|
784
|
+
}
|
|
785
|
+
let args = new Map(inherited.get(i));
|
|
786
|
+
for (const at of todo) {
|
|
787
|
+
for (const { kind, pairs, line, onbuild } of parsed.stages[at].instructions) {
|
|
788
|
+
if (kind !== "ARG" || onbuild)
|
|
789
|
+
continue;
|
|
790
|
+
for (const { name: arg, value } of pairs) {
|
|
791
|
+
named("ARG", arg, line);
|
|
792
|
+
const given = buildArg(arg);
|
|
793
|
+
if (given !== undefined)
|
|
794
|
+
args.set(arg, given);
|
|
795
|
+
else if (INJECTED.test(arg))
|
|
796
|
+
args.set(arg, new Unknown("injected"));
|
|
797
|
+
else if (isBuiltin(arg))
|
|
798
|
+
args.set(arg, globals.get(arg));
|
|
799
|
+
else if (value?.includes("$"))
|
|
800
|
+
args.set(arg, new Unknown("error", `ce does not work out the $ in the ARG ${arg} on line ${line}, which a stage built from it gets`));
|
|
801
|
+
else if (value !== undefined)
|
|
802
|
+
args.set(arg, evaluate(value, line, () => {
|
|
803
|
+
return;
|
|
804
|
+
}));
|
|
805
|
+
else if (globals.get(arg) !== undefined)
|
|
806
|
+
args.set(arg, globals.get(arg));
|
|
807
|
+
else if (!args.has(arg))
|
|
808
|
+
args.set(arg, undefined);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
inherited.set(at, args);
|
|
812
|
+
args = new Map(args);
|
|
813
|
+
}
|
|
814
|
+
return args;
|
|
815
|
+
};
|
|
816
|
+
let index = parsed.stages.length - 1;
|
|
817
|
+
if (index < 0)
|
|
818
|
+
fail("it has no FROM");
|
|
819
|
+
if (target) {
|
|
820
|
+
index = findStage(target, parsed.stages.length, true);
|
|
821
|
+
if (index < 0)
|
|
822
|
+
fail(`it has no stage named ${JSON.stringify(target)}`);
|
|
823
|
+
}
|
|
824
|
+
const swapped = replaced(index);
|
|
825
|
+
if (swapped !== undefined)
|
|
826
|
+
fail(`--build-context ${swapped} replaces the stage built`);
|
|
827
|
+
const ancestors = [];
|
|
828
|
+
for (let i = index;i >= 0; i = source(i).parent)
|
|
829
|
+
ancestors.unshift(i);
|
|
830
|
+
for (const i of ancestors)
|
|
831
|
+
run(i);
|
|
832
|
+
const state = run(index);
|
|
833
|
+
const v = state.args.has(name) ? state.args.get(name) : state.undeclared ?? (!globals.has(name) && INJECTED.test(name) && buildArg(name) === undefined ? new Unknown("injected") : globals.get(name));
|
|
834
|
+
if (v instanceof Unknown) {
|
|
835
|
+
if (v.why === "builtin")
|
|
836
|
+
fail(`${name} is set by the builder`);
|
|
837
|
+
if (v.why === "injected")
|
|
838
|
+
fail(`${name} may be set by the builder`);
|
|
839
|
+
fail(v.why === "error" ? v.reason : `ce cannot tell what ${name} is`);
|
|
840
|
+
}
|
|
841
|
+
return v;
|
|
842
|
+
}
|
|
843
|
+
function readContainerfile(file, model) {
|
|
844
|
+
let text;
|
|
845
|
+
try {
|
|
846
|
+
text = fs.readFileSync(file, "utf8");
|
|
847
|
+
} catch (exc) {
|
|
848
|
+
return fail(`ce cannot read ${JSON.stringify(file)}: ${errorMessage(exc)}`);
|
|
849
|
+
}
|
|
850
|
+
try {
|
|
851
|
+
return parseContainerfile(text, model);
|
|
852
|
+
} catch (exc) {
|
|
853
|
+
if (exc instanceof Unreadable)
|
|
854
|
+
fail(`${JSON.stringify(file)}: ${exc.message}`);
|
|
855
|
+
throw exc;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
function containerfileDefaults(files, buildArg) {
|
|
859
|
+
const docker = files.engine === "docker";
|
|
860
|
+
const model = docker ? "buildkit" : "imagebuilder";
|
|
861
|
+
let read;
|
|
862
|
+
const load = () => {
|
|
863
|
+
let file;
|
|
864
|
+
let parsed;
|
|
865
|
+
try {
|
|
866
|
+
file = findContainerfile(files);
|
|
867
|
+
parsed = readContainerfile(file, model);
|
|
868
|
+
} catch (exc) {
|
|
869
|
+
if (exc instanceof Unreadable)
|
|
870
|
+
return { error: exc };
|
|
871
|
+
throw exc;
|
|
872
|
+
}
|
|
873
|
+
const frontend = docker ? buildArg("BUILDKIT_SYNTAX")?.trim().split(" ")[0] ?? parsed.syntax : undefined;
|
|
874
|
+
if (frontend !== undefined && !STANDARD_SYNTAX.test(frontend))
|
|
875
|
+
return {
|
|
876
|
+
file,
|
|
877
|
+
error: new Unreadable(`it is read by the frontend ${JSON.stringify(frontend)}, which may read it differently`)
|
|
878
|
+
};
|
|
879
|
+
return { file, parsed };
|
|
880
|
+
};
|
|
881
|
+
return (name) => {
|
|
882
|
+
read ??= load();
|
|
883
|
+
try {
|
|
884
|
+
if (read.error)
|
|
885
|
+
throw read.error;
|
|
886
|
+
return argDefault(read.parsed, name, files.target, buildArg, model, {
|
|
887
|
+
contexts: files.contexts ?? [],
|
|
888
|
+
envs: files.envs ?? []
|
|
889
|
+
});
|
|
890
|
+
} catch (exc) {
|
|
891
|
+
if (!(exc instanceof Unreadable))
|
|
892
|
+
throw exc;
|
|
893
|
+
const where = read.file ? ` from ${JSON.stringify(read.file)}` : "";
|
|
894
|
+
throw new CeError(`ce: {arg.${name}} has no value from --build-arg, and ce cannot work out its default${where}: ${exc.message}`);
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
|
|
128
899
|
// src/json.ts
|
|
129
900
|
import { randomUUID } from "node:crypto";
|
|
130
|
-
import
|
|
131
|
-
import
|
|
901
|
+
import fs2 from "node:fs";
|
|
902
|
+
import path2 from "node:path";
|
|
132
903
|
function isObject(value) {
|
|
133
904
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
134
905
|
}
|
|
135
906
|
function readJsonFile(file, fallback) {
|
|
136
907
|
try {
|
|
137
|
-
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(
|
|
908
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(fs2.readFileSync(file)));
|
|
138
909
|
} catch (exc) {
|
|
139
910
|
if (isErrno(exc, "ENOENT"))
|
|
140
911
|
return fallback;
|
|
@@ -145,22 +916,22 @@ function dumpJson(payload) {
|
|
|
145
916
|
return JSON.stringify(payload, (_key, value) => isObject(value) ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : value);
|
|
146
917
|
}
|
|
147
918
|
function writePrivateJson(file, payload) {
|
|
148
|
-
const directory =
|
|
149
|
-
|
|
150
|
-
const temporary =
|
|
151
|
-
const fd =
|
|
919
|
+
const directory = path2.dirname(file);
|
|
920
|
+
fs2.mkdirSync(directory, { recursive: true, mode: 448 });
|
|
921
|
+
const temporary = path2.join(directory, `.ce-forgejo-${randomUUID().slice(0, 8)}`);
|
|
922
|
+
const fd = fs2.openSync(temporary, "wx", 384);
|
|
152
923
|
try {
|
|
153
924
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
925
|
+
fs2.fchmodSync(fd, 384);
|
|
926
|
+
fs2.writeSync(fd, `${dumpJson(payload)}
|
|
156
927
|
`);
|
|
157
|
-
|
|
928
|
+
fs2.fsyncSync(fd);
|
|
158
929
|
} finally {
|
|
159
|
-
|
|
930
|
+
fs2.closeSync(fd);
|
|
160
931
|
}
|
|
161
|
-
|
|
932
|
+
fs2.renameSync(temporary, file);
|
|
162
933
|
} finally {
|
|
163
|
-
|
|
934
|
+
fs2.rmSync(temporary, { force: true });
|
|
164
935
|
}
|
|
165
936
|
}
|
|
166
937
|
|
|
@@ -341,7 +1112,10 @@ class CiMetadata {
|
|
|
341
1112
|
const pull = /^refs\/pull\/(\d+)\//u.exec(ref);
|
|
342
1113
|
if (event.startsWith("pull_request") || pull) {
|
|
343
1114
|
const payload = pull ? {} : this.#payload();
|
|
344
|
-
const fromPayload = [
|
|
1115
|
+
const fromPayload = [
|
|
1116
|
+
isObject(payload.pull_request) && payload.pull_request.number,
|
|
1117
|
+
payload.number
|
|
1118
|
+
].find(Number.isInteger);
|
|
345
1119
|
const number = pull?.[1] ?? (fromPayload === undefined ? undefined : String(fromPayload));
|
|
346
1120
|
if (!number)
|
|
347
1121
|
throw new CeError(`ce: cannot find the pull request number for ${event} on ${ref}`);
|
|
@@ -395,20 +1169,20 @@ function onDefaultBranch(vars, branch) {
|
|
|
395
1169
|
if (UNKNOWN_ON_DEFAULT_BRANCH.has(name)) {
|
|
396
1170
|
throw new CeError(`ce: the default branch's cache scope cannot use {${name}}: this run does not know its commit`);
|
|
397
1171
|
}
|
|
398
|
-
return name === "image" || name.startsWith("env.")
|
|
1172
|
+
return name === "image" || name.startsWith("env.") ? vars.get(name) : undefined;
|
|
399
1173
|
}
|
|
400
1174
|
};
|
|
401
1175
|
}
|
|
402
1176
|
|
|
403
1177
|
// src/system.ts
|
|
404
1178
|
import { spawnSync } from "node:child_process";
|
|
405
|
-
import
|
|
406
|
-
import
|
|
1179
|
+
import fs3 from "node:fs";
|
|
1180
|
+
import path3 from "node:path";
|
|
407
1181
|
import { setTimeout } from "node:timers/promises";
|
|
408
1182
|
var io = {
|
|
409
|
-
stdout: (text) => void
|
|
410
|
-
stderr: (text) => void
|
|
411
|
-
stdin: () =>
|
|
1183
|
+
stdout: (text) => void fs3.writeSync(1, text),
|
|
1184
|
+
stderr: (text) => void fs3.writeSync(2, text),
|
|
1185
|
+
stdin: () => fs3.readFileSync(0, "utf8"),
|
|
412
1186
|
argv0: () => process.argv[1] ?? "ce"
|
|
413
1187
|
};
|
|
414
1188
|
function log(line) {
|
|
@@ -423,11 +1197,11 @@ function childEnv() {
|
|
|
423
1197
|
return Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
|
|
424
1198
|
}
|
|
425
1199
|
function which(command) {
|
|
426
|
-
const candidates = command.includes("/") ? [command] : (process.env.PATH ?? "").split(
|
|
1200
|
+
const candidates = command.includes("/") ? [command] : (process.env.PATH ?? "").split(path3.delimiter).filter(Boolean).map((dir) => path3.join(dir, command));
|
|
427
1201
|
for (const candidate of candidates) {
|
|
428
1202
|
try {
|
|
429
|
-
|
|
430
|
-
if (
|
|
1203
|
+
fs3.accessSync(candidate, fs3.constants.X_OK);
|
|
1204
|
+
if (fs3.statSync(candidate).isFile())
|
|
431
1205
|
return candidate;
|
|
432
1206
|
} catch {}
|
|
433
1207
|
}
|
|
@@ -459,13 +1233,13 @@ var system = {
|
|
|
459
1233
|
}
|
|
460
1234
|
};
|
|
461
1235
|
function binDir() {
|
|
462
|
-
return
|
|
1236
|
+
return fs3.realpathSync(path3.join(import.meta.dirname, "..", "bin"));
|
|
463
1237
|
}
|
|
464
1238
|
function ensureHelperOnPath() {
|
|
465
1239
|
const dir = binDir();
|
|
466
|
-
const parts = (process.env.PATH ?? "").split(
|
|
1240
|
+
const parts = (process.env.PATH ?? "").split(path3.delimiter);
|
|
467
1241
|
if (!parts.includes(dir))
|
|
468
|
-
process.env.PATH = [dir, ...parts].join(
|
|
1242
|
+
process.env.PATH = [dir, ...parts].join(path3.delimiter);
|
|
469
1243
|
}
|
|
470
1244
|
|
|
471
1245
|
// src/template.ts
|
|
@@ -758,7 +1532,7 @@ var SUMMARIZED = {
|
|
|
758
1532
|
"--label": "labels"
|
|
759
1533
|
};
|
|
760
1534
|
var ARG_VARIABLE = /^arg\.([A-Za-z_][A-Za-z0-9_]*)$/u;
|
|
761
|
-
function withBuildArgs(vars, values, argFile) {
|
|
1535
|
+
function withBuildArgs(vars, values, argFile, fromFile) {
|
|
762
1536
|
return {
|
|
763
1537
|
known: (name) => ARG_VARIABLE.test(name) || vars.known(name),
|
|
764
1538
|
get(name) {
|
|
@@ -766,17 +1540,53 @@ function withBuildArgs(vars, values, argFile) {
|
|
|
766
1540
|
if (arg === undefined)
|
|
767
1541
|
return vars.get(name);
|
|
768
1542
|
const value = values.get(arg);
|
|
769
|
-
if (value ===
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
throw new CeError(`ce: {${name}} needs --build-arg ${arg}=<value> on this command, or --build-arg ${arg} with ${arg} set; ` + "ce does not read defaults from the Containerfile");
|
|
1543
|
+
if (typeof value === "string")
|
|
1544
|
+
return value || undefined;
|
|
1545
|
+
if (argFile) {
|
|
1546
|
+
throw new CeError(`ce: {${name}} has no value from --build-arg, and may get one from --build-arg-file, which ce does not read`);
|
|
774
1547
|
}
|
|
775
|
-
return
|
|
1548
|
+
return fromFile(arg) || undefined;
|
|
776
1549
|
}
|
|
777
1550
|
};
|
|
778
1551
|
}
|
|
779
|
-
|
|
1552
|
+
var SHORT_BOOLS = {
|
|
1553
|
+
docker: "qD",
|
|
1554
|
+
podman: "qD",
|
|
1555
|
+
buildah: "qD"
|
|
1556
|
+
};
|
|
1557
|
+
var BUILDX_VALUES = "add-host allow annotation attest build-arg build-context builder cache-from cache-to call cgroup-parent cpu-period cpu-quota cpu-shares cpuset-cpus cpuset-mems file iidfile isolation label memory memory-swap metadata-file network no-cache-filter output platform policy print progress provenance resource sbom secret security-opt shm-size ssh tag target ulimit";
|
|
1558
|
+
var BUILDAH_VALUES = "add-host annotation arch authfile blob-cache blob-info-cache-dir build-arg build-arg-file build-context cache-from cache-to cache-ttl cap-add cap-drop cdi-config-dir cert-dir cgroup-parent cgroupns compression-format compression-level cpp-flag cpu-period cpu-quota cpu-shares cpuset-cpus cpuset-mems creds cw decryption-key device dns dns-option dns-search env file format from group-add hooks-dir ignorefile iidfile iidfile-raw ipc isolation jobs label layer-label logfile loglevel manifest memory memory-swap metadata-file mount net network os os-feature os-version output override-arch override-os pid platform progress raw-iidfile retry retry-delay runtime-flag rusage-logfile sbom sbom-image-output sbom-image-purl-output sbom-merge-strategy sbom-output sbom-purl-output sbom-scanner-command sbom-scanner-image secret security-opt shm-size sign-by signature-policy source-date-epoch source-policy-file ssh tag target timestamp ulimit unsetannotation unsetenv unsetlabel userns userns-gid-map userns-gid-map-group userns-uid-map userns-uid-map-user uts variant volume";
|
|
1559
|
+
var VALUE_FLAGS = {
|
|
1560
|
+
docker: new Set(BUILDX_VALUES.split(" ")),
|
|
1561
|
+
podman: new Set(BUILDAH_VALUES.split(" ")),
|
|
1562
|
+
buildah: new Set(BUILDAH_VALUES.split(" "))
|
|
1563
|
+
};
|
|
1564
|
+
var SHORT_VALUES = {
|
|
1565
|
+
docker: "cfmot",
|
|
1566
|
+
podman: "cfmotv",
|
|
1567
|
+
buildah: "cfmotv"
|
|
1568
|
+
};
|
|
1569
|
+
var takesValue = (arg, engine) => arg.startsWith("--") ? VALUE_FLAGS[engine].has(arg.slice(2)) : arg.length === 2 && SHORT_VALUES[engine].includes(arg[1]);
|
|
1570
|
+
function ungroup(argv, engine) {
|
|
1571
|
+
const end = argv.indexOf("--");
|
|
1572
|
+
return argv.flatMap((arg, at) => {
|
|
1573
|
+
if (end >= 0 && at >= end || !/^-[^-]/u.test(arg))
|
|
1574
|
+
return [arg];
|
|
1575
|
+
const split = [];
|
|
1576
|
+
let i = 1;
|
|
1577
|
+
while (i < arg.length - 1 && SHORT_BOOLS[engine].includes(arg[i]) && arg[i + 1] !== "=")
|
|
1578
|
+
split.push(`-${arg[i++]}`);
|
|
1579
|
+
return i === 1 ? [arg] : [...split, `-${arg.slice(i)}`];
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
function parseBuildArgs(given, tagPrefix, cachePrefix, {
|
|
1583
|
+
vars = new CiMetadata,
|
|
1584
|
+
labels = {},
|
|
1585
|
+
defaultBranch,
|
|
1586
|
+
env = process.env,
|
|
1587
|
+
engine = "docker"
|
|
1588
|
+
} = {}) {
|
|
1589
|
+
const argv = ungroup(given, engine);
|
|
780
1590
|
const parts = [];
|
|
781
1591
|
const rawCacheFrom = [];
|
|
782
1592
|
const rawCacheTo = [];
|
|
@@ -794,6 +1604,10 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
|
|
|
794
1604
|
let templated = false;
|
|
795
1605
|
const buildArgs = new Map;
|
|
796
1606
|
let argFile = false;
|
|
1607
|
+
const files = [];
|
|
1608
|
+
const positionals = [];
|
|
1609
|
+
const contexts = [];
|
|
1610
|
+
const envs = [];
|
|
797
1611
|
let rest = [];
|
|
798
1612
|
const addTags = (value, flag) => {
|
|
799
1613
|
templated ||= hasPlaceholders(value);
|
|
@@ -808,7 +1622,14 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
|
|
|
808
1622
|
buildArgs.set(value, env[value]);
|
|
809
1623
|
else if (engine !== "docker")
|
|
810
1624
|
buildArgs.set(value, null);
|
|
1625
|
+
if (engine !== "docker" && buildArgs.has("TARGETPLATFORM"))
|
|
1626
|
+
buildArgs.set("TARGETPLATFORM", null);
|
|
1627
|
+
const epoch = buildArgs.get("SOURCE_DATE_EPOCH");
|
|
1628
|
+
if (engine === "docker" && typeof epoch === "string" && !(/^(?:0|-?[1-9][0-9]*)$/u.test(epoch) && BigInt(epoch) >= -(2n ** 63n) && BigInt(epoch) < 2n ** 63n))
|
|
1629
|
+
buildArgs.set("SOURCE_DATE_EPOCH", null);
|
|
811
1630
|
}
|
|
1631
|
+
if (field === "dockerfile")
|
|
1632
|
+
files.push(value);
|
|
812
1633
|
if (field === "buildArgs" || field === "labels")
|
|
813
1634
|
summary[field].push(value);
|
|
814
1635
|
else
|
|
@@ -818,8 +1639,10 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
|
|
|
818
1639
|
const arg = argv[i];
|
|
819
1640
|
const [name, attached] = arg.startsWith("--") && arg.includes("=") ? splitAt(arg, "=") : [arg, undefined];
|
|
820
1641
|
const next = () => consumeValue(argv.slice(++i), arg)[0];
|
|
1642
|
+
const short = () => arg.slice(arg[2] === "=" ? 3 : 2);
|
|
821
1643
|
if (arg === "--") {
|
|
822
1644
|
rest = argv.slice(i);
|
|
1645
|
+
positionals.push(...rest.slice(1));
|
|
823
1646
|
if (rest.length > 1)
|
|
824
1647
|
summary.context = rest.at(-1);
|
|
825
1648
|
break;
|
|
@@ -828,7 +1651,7 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
|
|
|
828
1651
|
else if (arg === "--tag")
|
|
829
1652
|
addTags(next(), "--tag=");
|
|
830
1653
|
else if (arg.startsWith("-t"))
|
|
831
|
-
addTags(
|
|
1654
|
+
addTags(short(), "-t");
|
|
832
1655
|
else if (name === "--tag")
|
|
833
1656
|
addTags(attached, "--tag=");
|
|
834
1657
|
else if (name === "--cache-from")
|
|
@@ -854,16 +1677,35 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
|
|
|
854
1677
|
parts.push(arg);
|
|
855
1678
|
if (attached === undefined)
|
|
856
1679
|
parts.push(next());
|
|
1680
|
+
} else if (name === "--build-context") {
|
|
1681
|
+
const v = attached ?? next();
|
|
1682
|
+
parts.push(arg);
|
|
1683
|
+
if (attached === undefined)
|
|
1684
|
+
parts.push(v);
|
|
1685
|
+
contexts.push(v.split("=")[0]);
|
|
1686
|
+
} else if ((name === "--env" || name === "--unsetenv") && engine !== "docker") {
|
|
1687
|
+
const v = attached ?? next();
|
|
1688
|
+
parts.push(arg);
|
|
1689
|
+
if (attached === undefined)
|
|
1690
|
+
parts.push(v);
|
|
1691
|
+
envs.push(`${name} ${v}`);
|
|
857
1692
|
} else if (arg.startsWith("-f") && arg.length > 2) {
|
|
858
1693
|
parts.push(arg);
|
|
859
|
-
summary.dockerfile =
|
|
1694
|
+
summary.dockerfile = short();
|
|
1695
|
+
files.push(summary.dockerfile);
|
|
1696
|
+
} else if (takesValue(arg, engine) && i + 1 < argv.length) {
|
|
1697
|
+
parts.push(arg, argv[++i]);
|
|
860
1698
|
} else {
|
|
861
1699
|
parts.push(arg);
|
|
862
|
-
if (!arg.startsWith("-"))
|
|
1700
|
+
if (!arg.startsWith("-")) {
|
|
1701
|
+
positionals.push(arg);
|
|
863
1702
|
summary.context = arg;
|
|
1703
|
+
}
|
|
864
1704
|
}
|
|
865
1705
|
}
|
|
866
|
-
|
|
1706
|
+
const baseVars = vars;
|
|
1707
|
+
const fromFile = containerfileDefaults({ engine, files, positionals, target: summary.target, contexts, envs }, (arg) => buildArgs.get(arg) ?? undefined);
|
|
1708
|
+
vars = withBuildArgs(baseVars, buildArgs, argFile, fromFile);
|
|
867
1709
|
const args = parts.flatMap((part) => {
|
|
868
1710
|
if (typeof part === "string")
|
|
869
1711
|
return [part];
|
|
@@ -885,7 +1727,11 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
|
|
|
885
1727
|
const cacheFrom = rawCacheFrom.map((c) => toNames.has(cacheScopeName(c)) ? normalizeCacheScope(c, cachePrefix) : c);
|
|
886
1728
|
const ghaTo = rawCacheTo.filter(isGhaCache);
|
|
887
1729
|
if (defaultBranch && ghaTo.length) {
|
|
888
|
-
const
|
|
1730
|
+
const fileArg = (arg) => {
|
|
1731
|
+
throw new CeError(`ce: the default branch's cache scope cannot use {arg.${arg}} from the Containerfile, which may differ there`);
|
|
1732
|
+
};
|
|
1733
|
+
const onDefault = withBuildArgs(onDefaultBranch(baseVars, defaultBranch()), buildArgs, argFile, fileArg);
|
|
1734
|
+
const prefix = expandPrefix(prefixTemplate, onDefault, "cache prefix");
|
|
889
1735
|
cacheFrom.push(...ghaTo.map((c) => normalizeCacheScope(withoutCacheMode(c), prefix)));
|
|
890
1736
|
}
|
|
891
1737
|
for (const rewritten of new Set(cacheFrom)) {
|
|
@@ -909,7 +1755,11 @@ function printBuildSummary(engine, args, summary) {
|
|
|
909
1755
|
if (!command || command.tagsOnly)
|
|
910
1756
|
return;
|
|
911
1757
|
const subcommand = command.name;
|
|
912
|
-
const out = [
|
|
1758
|
+
const out = [
|
|
1759
|
+
"[ce] build summary:",
|
|
1760
|
+
` engine: ${engine}`,
|
|
1761
|
+
` subcommand: ${subcommand}`
|
|
1762
|
+
];
|
|
913
1763
|
const list = (title, items) => {
|
|
914
1764
|
if (items.length)
|
|
915
1765
|
out.push(` ${title}:`, ...items.map((item) => ` - ${item}`));
|
|
@@ -947,14 +1797,20 @@ async function forgejoOidcJwt(audience, requestUrl, requestToken) {
|
|
|
947
1797
|
throw new Error("credentials in URL");
|
|
948
1798
|
url.searchParams.set("audience", audience);
|
|
949
1799
|
tokenUrl = url.href;
|
|
950
|
-
headers = {
|
|
1800
|
+
headers = {
|
|
1801
|
+
Accept: "application/json",
|
|
1802
|
+
Authorization: `bearer ${requestToken}`
|
|
1803
|
+
};
|
|
951
1804
|
new Headers(headers);
|
|
952
1805
|
} catch {
|
|
953
1806
|
throw new CeError("token request URL or headers are invalid");
|
|
954
1807
|
}
|
|
955
1808
|
let body;
|
|
956
1809
|
try {
|
|
957
|
-
const response = await fetch(tokenUrl, {
|
|
1810
|
+
const response = await fetch(tokenUrl, {
|
|
1811
|
+
headers,
|
|
1812
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
1813
|
+
});
|
|
958
1814
|
if (!response.ok)
|
|
959
1815
|
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
960
1816
|
body = await response.arrayBuffer();
|
|
@@ -995,20 +1851,20 @@ function needsRefresh(expiry) {
|
|
|
995
1851
|
}
|
|
996
1852
|
|
|
997
1853
|
// src/state.ts
|
|
998
|
-
import
|
|
1854
|
+
import fs5 from "node:fs";
|
|
999
1855
|
import { homedir } from "node:os";
|
|
1000
|
-
import
|
|
1856
|
+
import path4 from "node:path";
|
|
1001
1857
|
|
|
1002
1858
|
// src/lock.ts
|
|
1003
|
-
import
|
|
1859
|
+
import fs4 from "node:fs";
|
|
1004
1860
|
import { hostname } from "node:os";
|
|
1005
1861
|
import { setTimeout as setTimeout2 } from "node:timers/promises";
|
|
1006
1862
|
var STALE_AFTER_MS = 120000;
|
|
1007
1863
|
var POLL_MS = 50;
|
|
1008
1864
|
function snapshot(lockPath) {
|
|
1009
1865
|
try {
|
|
1010
|
-
const { ino, mtimeMs } =
|
|
1011
|
-
return { ino, mtimeMs, owner:
|
|
1866
|
+
const { ino, mtimeMs } = fs4.statSync(lockPath);
|
|
1867
|
+
return { ino, mtimeMs, owner: fs4.readFileSync(lockPath, "utf8").trim() };
|
|
1012
1868
|
} catch (exc) {
|
|
1013
1869
|
if (isErrno(exc, "ENOENT"))
|
|
1014
1870
|
return null;
|
|
@@ -1034,7 +1890,7 @@ function breakIfStale(lockPath) {
|
|
|
1034
1890
|
return;
|
|
1035
1891
|
const guard = `${lockPath}.break`;
|
|
1036
1892
|
try {
|
|
1037
|
-
|
|
1893
|
+
fs4.writeFileSync(guard, "", { flag: "wx", mode: 384 });
|
|
1038
1894
|
} catch (exc) {
|
|
1039
1895
|
if (!isErrno(exc, "EEXIST"))
|
|
1040
1896
|
throw exc;
|
|
@@ -1047,10 +1903,10 @@ function breakIfStale(lockPath) {
|
|
|
1047
1903
|
try {
|
|
1048
1904
|
const current = snapshot(lockPath);
|
|
1049
1905
|
if (current && current.ino === stale.ino && current.mtimeMs === stale.mtimeMs && current.owner === stale.owner) {
|
|
1050
|
-
|
|
1906
|
+
fs4.rmSync(lockPath, { force: true });
|
|
1051
1907
|
}
|
|
1052
1908
|
} finally {
|
|
1053
|
-
|
|
1909
|
+
fs4.rmSync(guard, { force: true });
|
|
1054
1910
|
}
|
|
1055
1911
|
}
|
|
1056
1912
|
async function withFileLock(lockPath, fn) {
|
|
@@ -1059,7 +1915,7 @@ async function withFileLock(lockPath, fn) {
|
|
|
1059
1915
|
for (;; ) {
|
|
1060
1916
|
let fd;
|
|
1061
1917
|
try {
|
|
1062
|
-
fd =
|
|
1918
|
+
fd = fs4.openSync(lockPath, "wx", 384);
|
|
1063
1919
|
} catch (exc) {
|
|
1064
1920
|
if (!isErrno(exc, "EEXIST"))
|
|
1065
1921
|
throw exc;
|
|
@@ -1068,11 +1924,11 @@ async function withFileLock(lockPath, fn) {
|
|
|
1068
1924
|
continue;
|
|
1069
1925
|
}
|
|
1070
1926
|
try {
|
|
1071
|
-
|
|
1927
|
+
fs4.writeSync(fd, `${owner}
|
|
1072
1928
|
`);
|
|
1073
|
-
ino =
|
|
1929
|
+
ino = fs4.fstatSync(fd).ino;
|
|
1074
1930
|
} finally {
|
|
1075
|
-
|
|
1931
|
+
fs4.closeSync(fd);
|
|
1076
1932
|
}
|
|
1077
1933
|
break;
|
|
1078
1934
|
}
|
|
@@ -1081,7 +1937,7 @@ async function withFileLock(lockPath, fn) {
|
|
|
1081
1937
|
} finally {
|
|
1082
1938
|
const current = snapshot(lockPath);
|
|
1083
1939
|
if (current?.ino === ino && current.owner === owner)
|
|
1084
|
-
|
|
1940
|
+
fs4.rmSync(lockPath, { force: true });
|
|
1085
1941
|
}
|
|
1086
1942
|
}
|
|
1087
1943
|
|
|
@@ -1091,16 +1947,16 @@ var STATE_FILE = "ce-forgejo-credentials.json";
|
|
|
1091
1947
|
function dockerConfigDir() {
|
|
1092
1948
|
const configured = process.env.DOCKER_CONFIG;
|
|
1093
1949
|
if (!configured)
|
|
1094
|
-
return
|
|
1095
|
-
const expanded = configured === "~" || configured.startsWith("~/") ?
|
|
1096
|
-
return
|
|
1950
|
+
return path4.join(homedir(), ".docker");
|
|
1951
|
+
const expanded = configured === "~" || configured.startsWith("~/") ? path4.join(homedir(), configured.slice(1)) : configured;
|
|
1952
|
+
return path4.resolve(expanded);
|
|
1097
1953
|
}
|
|
1098
1954
|
function helperStatePath() {
|
|
1099
|
-
return
|
|
1955
|
+
return path4.join(dockerConfigDir(), STATE_FILE);
|
|
1100
1956
|
}
|
|
1101
1957
|
async function withHelperState(fn) {
|
|
1102
1958
|
const statePath = helperStatePath();
|
|
1103
|
-
|
|
1959
|
+
fs5.mkdirSync(path4.dirname(statePath), { recursive: true, mode: 448 });
|
|
1104
1960
|
return withFileLock(`${statePath}.pidlock`, async () => {
|
|
1105
1961
|
const state = readJsonFile(statePath, { version: 1, registries: {} });
|
|
1106
1962
|
if (!isObject(state) || !isObject(state.registries)) {
|
|
@@ -1129,7 +1985,7 @@ async function saveHelperState(registry, username, audience, authority, jwt) {
|
|
|
1129
1985
|
}
|
|
1130
1986
|
async function configureDockerCredentialHelper(registry, username, audience, authority, jwt) {
|
|
1131
1987
|
await saveHelperState(registry, username, audience, authority, jwt);
|
|
1132
|
-
const configPath =
|
|
1988
|
+
const configPath = path4.join(dockerConfigDir(), "config.json");
|
|
1133
1989
|
const config = readJsonFile(configPath, {});
|
|
1134
1990
|
if (!isObject(config))
|
|
1135
1991
|
throw new CeError("Docker config is not a JSON object");
|
|
@@ -1258,16 +2114,6 @@ function envFlag(name) {
|
|
|
1258
2114
|
function shellQuote(arg) {
|
|
1259
2115
|
return /^[A-Za-z0-9_@%+:,./-][A-Za-z0-9_@%+=:,./-]*$/u.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
|
|
1260
2116
|
}
|
|
1261
|
-
function engineCommand(engine, args) {
|
|
1262
|
-
if (engine === "docker" || !system.which("gosu"))
|
|
1263
|
-
return [engine, ...args];
|
|
1264
|
-
const user = process.env.USER ?? "";
|
|
1265
|
-
if (!user) {
|
|
1266
|
-
logError("ce: gosu is available but USER is not set; cannot drop privileges. Set USER or remove gosu from PATH.");
|
|
1267
|
-
return null;
|
|
1268
|
-
}
|
|
1269
|
-
return ["gosu", user, engine, ...args];
|
|
1270
|
-
}
|
|
1271
2117
|
async function runEngine(engine, argv, tagPrefix, cachePrefix, options = {}) {
|
|
1272
2118
|
const dryRun = options.dryRun ?? envFlag("CE_DRY_RUN");
|
|
1273
2119
|
ensureHelperOnPath();
|
|
@@ -1288,14 +2134,11 @@ async function runEngine(engine, argv, tagPrefix, cachePrefix, options = {}) {
|
|
|
1288
2134
|
summary = parsed.summary;
|
|
1289
2135
|
}
|
|
1290
2136
|
if (dryRun) {
|
|
1291
|
-
const run = engineCommand(engine, args);
|
|
1292
|
-
if (!run)
|
|
1293
|
-
return 1;
|
|
1294
2137
|
if (summary)
|
|
1295
2138
|
printBuildSummary(engine, args, summary);
|
|
1296
2139
|
if (!system.which(engine))
|
|
1297
2140
|
logError(`ce: warning: ${engine} is not on PATH, so running this here would fail`);
|
|
1298
|
-
io.stdout(`${
|
|
2141
|
+
io.stdout(`${[engine, ...args].map(shellQuote).join(" ")}
|
|
1299
2142
|
`);
|
|
1300
2143
|
return 0;
|
|
1301
2144
|
}
|
|
@@ -1307,8 +2150,7 @@ async function runEngine(engine, argv, tagPrefix, cachePrefix, options = {}) {
|
|
|
1307
2150
|
return 1;
|
|
1308
2151
|
if (summary)
|
|
1309
2152
|
printBuildSummary(engine, args, summary);
|
|
1310
|
-
|
|
1311
|
-
return run ? system.exec(run[0], run) : 1;
|
|
2153
|
+
return system.exec(engine, [engine, ...args]);
|
|
1312
2154
|
}
|
|
1313
2155
|
|
|
1314
2156
|
// src/login.ts
|
|
@@ -1327,13 +2169,21 @@ function oidcConfig() {
|
|
|
1327
2169
|
return null;
|
|
1328
2170
|
const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL ?? "";
|
|
1329
2171
|
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN ?? "";
|
|
1330
|
-
const missing = [
|
|
2172
|
+
const missing = [
|
|
2173
|
+
"ACTIONS_ID_TOKEN_REQUEST_URL",
|
|
2174
|
+
"ACTIONS_ID_TOKEN_REQUEST_TOKEN"
|
|
2175
|
+
].filter((name) => !process.env[name]);
|
|
1331
2176
|
if (missing.length) {
|
|
1332
2177
|
return `Forgejo OIDC login config is incomplete: AUD is set, but ${missing.map(unsetOrEmpty).join("; ")}`;
|
|
1333
2178
|
}
|
|
1334
2179
|
try {
|
|
1335
2180
|
assertAbsoluteHttpUrl(requestUrl, "must be an absolute HTTP(S) URL");
|
|
1336
|
-
return {
|
|
2181
|
+
return {
|
|
2182
|
+
audience,
|
|
2183
|
+
requestUrl,
|
|
2184
|
+
requestToken,
|
|
2185
|
+
authority: urlAuthority(requestUrl)
|
|
2186
|
+
};
|
|
1337
2187
|
} catch (exc) {
|
|
1338
2188
|
if (!(exc instanceof CeError))
|
|
1339
2189
|
throw exc;
|
|
@@ -1346,7 +2196,14 @@ function loginWithEngines(target, password) {
|
|
|
1346
2196
|
continue;
|
|
1347
2197
|
let result;
|
|
1348
2198
|
try {
|
|
1349
|
-
result = system.run([
|
|
2199
|
+
result = system.run([
|
|
2200
|
+
engine,
|
|
2201
|
+
"login",
|
|
2202
|
+
"-u",
|
|
2203
|
+
target.user,
|
|
2204
|
+
"--password-stdin",
|
|
2205
|
+
target.registry
|
|
2206
|
+
], `${password}
|
|
1350
2207
|
`);
|
|
1351
2208
|
} catch (exc) {
|
|
1352
2209
|
if (isErrno(exc, "ENOENT"))
|
|
@@ -1389,7 +2246,10 @@ async function login() {
|
|
|
1389
2246
|
try {
|
|
1390
2247
|
const authority = urlAuthority(rawRegistry);
|
|
1391
2248
|
const registry = registryServer(rawRegistry);
|
|
1392
|
-
registries.push({
|
|
2249
|
+
registries.push({
|
|
2250
|
+
registry,
|
|
2251
|
+
useOidc: oidc !== null && sameAuthority(authority, oidc.authority)
|
|
2252
|
+
});
|
|
1393
2253
|
} catch (exc) {
|
|
1394
2254
|
if (!(exc instanceof CeError))
|
|
1395
2255
|
throw exc;
|
|
@@ -1510,9 +2370,9 @@ Placeholders, in build tags and in the tag and cache prefixes:
|
|
|
1510
2370
|
From a SemVer tag such as v1.2.3 (v optional)
|
|
1511
2371
|
{stable} Set only for a SemVer tag without a pre-release
|
|
1512
2372
|
{env.NAME} The environment variable NAME
|
|
1513
|
-
{arg.NAME} The build arg NAME
|
|
1514
|
-
|
|
1515
|
-
|
|
2373
|
+
{arg.NAME} The build arg NAME as the build gets it: from
|
|
2374
|
+
--build-arg, else the Containerfile's default for the
|
|
2375
|
+
stage built (--target's or the last)
|
|
1516
2376
|
A guard that does not pass leaves its tag out or its prefix empty even if
|
|
1517
2377
|
another guard there cannot be worked out.
|
|
1518
2378
|
Values are made safe for a tag ("ci/x" becomes "ci-x"). Quote templates
|
|
@@ -1524,8 +2384,8 @@ Commands:
|
|
|
1524
2384
|
|
|
1525
2385
|
Engines:
|
|
1526
2386
|
docker [args] Passthrough to docker
|
|
1527
|
-
podman [args] Passthrough to podman
|
|
1528
|
-
buildah [args] Passthrough to buildah
|
|
2387
|
+
podman [args] Passthrough to podman
|
|
2388
|
+
buildah [args] Passthrough to buildah
|
|
1529
2389
|
|
|
1530
2390
|
Environment variables (used by login):
|
|
1531
2391
|
Each registry is a "group". The default group uses the bare variables
|