@izykitten/ce 0.0.3-dev.2 → 0.0.3-dev.4
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 +36 -18
- package/dist/ce.js +980 -103
- 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,738 @@ 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) || rest.replace(PLAIN_BRACES, "").includes("${"))
|
|
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"'\\`<${}]*)?\}/gu;
|
|
297
|
+
function jsonPrefix(text) {
|
|
298
|
+
let depth = 0;
|
|
299
|
+
for (let i = 0;i < text.length; i++) {
|
|
300
|
+
const ch = text[i];
|
|
301
|
+
if (ch === '"') {
|
|
302
|
+
for (i++;i < text.length && text[i] !== '"'; i++)
|
|
303
|
+
if (text[i] === "\\")
|
|
304
|
+
i++;
|
|
305
|
+
} else if (ch === "[" || ch === "{")
|
|
306
|
+
depth++;
|
|
307
|
+
else if ((ch === "]" || ch === "}") && --depth === 0)
|
|
308
|
+
return text.slice(0, i + 1);
|
|
309
|
+
}
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
var DIRECTIVE_SPACE = /[\t\n\f\r ]/u;
|
|
313
|
+
function directive(text) {
|
|
314
|
+
const match = /^([a-zA-Z][a-zA-Z0-9]*)[\t\n\f\r ]*=[\t\n\f\r ]*([^\n]+)$/u.exec(trimEnd(text, DIRECTIVE_SPACE));
|
|
315
|
+
return match ? { key: match[1].toLowerCase(), value: match[2] } : undefined;
|
|
316
|
+
}
|
|
317
|
+
var DIRECTIVES = ["syntax", "escape", "check"];
|
|
318
|
+
function detectSyntax(lines) {
|
|
319
|
+
if (lines[0]?.startsWith("#!"))
|
|
320
|
+
lines = lines.slice(1);
|
|
321
|
+
for (const prefix of ["#", "//"]) {
|
|
322
|
+
const seen = new Set;
|
|
323
|
+
for (const line of lines) {
|
|
324
|
+
if (!line.startsWith(prefix))
|
|
325
|
+
break;
|
|
326
|
+
const match = directive(line.slice(prefix.length).replace(TRIM_START, ""));
|
|
327
|
+
if (!match || !DIRECTIVES.includes(match.key) || seen.has(match.key))
|
|
328
|
+
break;
|
|
329
|
+
seen.add(match.key);
|
|
330
|
+
if (match.key === "syntax")
|
|
331
|
+
return match.value.split(" ")[0];
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
function pair(word) {
|
|
337
|
+
const eq = word.indexOf("=");
|
|
338
|
+
return eq < 0 ? { name: word, value: undefined } : { name: word.slice(0, eq), value: word.slice(eq + 1) };
|
|
339
|
+
}
|
|
340
|
+
function parseContainerfile(text, model = "buildkit") {
|
|
341
|
+
const buildkit = model === "buildkit";
|
|
342
|
+
const lines = text.replace(/^\ufeff/u, "").split(`
|
|
343
|
+
`).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, ""));
|
|
344
|
+
let escape = "\\";
|
|
345
|
+
let i = 0;
|
|
346
|
+
for (let seen = false;!buildkit && i < lines.length; i++) {
|
|
347
|
+
const directive = /^#[ \t]*escape[ \t]*=[ \t]*([^\n])/u.exec(lines[i].replace(TRIM_START, "").toLowerCase());
|
|
348
|
+
if (!directive)
|
|
349
|
+
break;
|
|
350
|
+
if (seen)
|
|
351
|
+
fail("there is more than one escape directive");
|
|
352
|
+
seen = true;
|
|
353
|
+
escape = directive[1];
|
|
354
|
+
if (escape !== "\\" && escape !== "`")
|
|
355
|
+
fail(`the escape directive ${JSON.stringify(escape)} is not \\ or \``);
|
|
356
|
+
}
|
|
357
|
+
for (const seen = new Set;buildkit && i < lines.length; i++) {
|
|
358
|
+
const line = lines[i].replace(TRIM_START, "");
|
|
359
|
+
const found = line.startsWith("#") ? directive(line.slice(1).replace(TRIM_START, "")) : undefined;
|
|
360
|
+
if (!found || !DIRECTIVES.includes(found.key) || seen.has(found.key))
|
|
361
|
+
break;
|
|
362
|
+
seen.add(found.key);
|
|
363
|
+
if (found.key === "escape") {
|
|
364
|
+
const { value } = found;
|
|
365
|
+
if (value !== "\\" && value !== "`")
|
|
366
|
+
fail(`the escape directive ${JSON.stringify(value)} is not \\ or \``);
|
|
367
|
+
escape = value;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const e = escape === "\\" ? "\\\\" : escape;
|
|
371
|
+
const continued = buildkit ? new RegExp(`([^${e}])${e}[ \\t]*$|^${e}[ \\t]*$`, "u") : new RegExp(`()${e}[ \\t]*$`, "u");
|
|
372
|
+
const parsed = {
|
|
373
|
+
escape,
|
|
374
|
+
syntax: detectSyntax(lines),
|
|
375
|
+
globals: [],
|
|
376
|
+
stages: []
|
|
377
|
+
};
|
|
378
|
+
while (i < lines.length) {
|
|
379
|
+
let start = 0;
|
|
380
|
+
let line = "";
|
|
381
|
+
for (;i < lines.length; i++) {
|
|
382
|
+
const physical = lines[i];
|
|
383
|
+
if (/^(?:#|$)/u.test(trim(physical)))
|
|
384
|
+
continue;
|
|
385
|
+
start ||= i + 1;
|
|
386
|
+
if (continued.test(physical)) {
|
|
387
|
+
line += physical.replace(continued, "$1");
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
line += physical;
|
|
391
|
+
i++;
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
line = trim(line);
|
|
395
|
+
if (!line)
|
|
396
|
+
continue;
|
|
397
|
+
const [instruction = "", rest = ""] = COMMAND.exec(line).slice(1);
|
|
398
|
+
const keyword = instructionOf(instruction, start);
|
|
399
|
+
for (const { name, tabs } of heredocs(keyword, rest, start, buildkit)) {
|
|
400
|
+
for (;; i++) {
|
|
401
|
+
if (i >= lines.length)
|
|
402
|
+
fail(`the heredoc ${name} from line ${start} never ends`);
|
|
403
|
+
const end = lines[i];
|
|
404
|
+
if ((tabs ? end.replace(/^\t+/u, "") : end) === name) {
|
|
405
|
+
i++;
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const [, onbuild = "", onbuildRest = ""] = keyword === "ONBUILD" ? COMMAND.exec(trim(rest)) ?? [] : [];
|
|
411
|
+
const kind = onbuild ? instructionOf(onbuild, start) : keyword;
|
|
412
|
+
const args = onbuild ? onbuildRest : rest;
|
|
413
|
+
if (keyword !== "FROM" && kind !== "ARG" && kind !== "ENV")
|
|
414
|
+
continue;
|
|
415
|
+
if (UNUSUAL_SPACE.test(line))
|
|
416
|
+
fail(`ce reads only ASCII space in the ${kind} on line ${start}`);
|
|
417
|
+
const split = onbuild && buildkit ? "\\" : escape;
|
|
418
|
+
const stage = parsed.stages.at(-1);
|
|
419
|
+
if (keyword === "FROM") {
|
|
420
|
+
const all = words(rest, buildkit ? escape : "\\");
|
|
421
|
+
let flags = 0;
|
|
422
|
+
while (all[flags]?.startsWith("--"))
|
|
423
|
+
if (all[flags++] === "--")
|
|
424
|
+
break;
|
|
425
|
+
const parts = all.slice(flags);
|
|
426
|
+
if (!(parts.length === 1 || parts.length === 3 && /^[Aa][Ss]$/u.test(parts[1])) || parts.some((part) => IS_SPACE.test(part)))
|
|
427
|
+
fail(`ce cannot read the FROM on line ${start}`);
|
|
428
|
+
parsed.stages.push({
|
|
429
|
+
name: parts[2] ?? "",
|
|
430
|
+
line: start,
|
|
431
|
+
base: parts[0],
|
|
432
|
+
instructions: []
|
|
433
|
+
});
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
const set = words(args, split);
|
|
437
|
+
if (set[0]?.startsWith("--"))
|
|
438
|
+
fail(`ce does not read the flags of the ${kind} on line ${start}`);
|
|
439
|
+
const from = {
|
|
440
|
+
line: start,
|
|
441
|
+
...onbuild !== "" && { onbuild: true }
|
|
442
|
+
};
|
|
443
|
+
if (kind === "ARG" && (stage || !onbuild)) {
|
|
444
|
+
if (!set.length)
|
|
445
|
+
fail(`the ARG on line ${start} names nothing`);
|
|
446
|
+
(stage?.instructions ?? parsed.globals).push({
|
|
447
|
+
kind: "ARG",
|
|
448
|
+
pairs: set.map(pair),
|
|
449
|
+
...from
|
|
450
|
+
});
|
|
451
|
+
} else if (kind === "ENV" && stage) {
|
|
452
|
+
const legacy = set.length > 0 && !set[0].includes("=");
|
|
453
|
+
stage.instructions.push({
|
|
454
|
+
kind: "ENV",
|
|
455
|
+
pairs: legacy ? [{ name: set[0], value: undefined }] : set.map(pair),
|
|
456
|
+
...from
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return parsed;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
class Unknown {
|
|
464
|
+
why;
|
|
465
|
+
reason;
|
|
466
|
+
constructor(why, reason = "") {
|
|
467
|
+
this.why = why;
|
|
468
|
+
this.reason = reason;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
var BASE_ENV = new Unknown("base");
|
|
472
|
+
var BUILTINS = [
|
|
473
|
+
"BUILDPLATFORM",
|
|
474
|
+
"BUILDOS",
|
|
475
|
+
"BUILDOSVERSION",
|
|
476
|
+
"BUILDARCH",
|
|
477
|
+
"BUILDVARIANT",
|
|
478
|
+
"TARGETPLATFORM",
|
|
479
|
+
"TARGETOS",
|
|
480
|
+
"TARGETOSVERSION",
|
|
481
|
+
"TARGETARCH",
|
|
482
|
+
"TARGETVARIANT",
|
|
483
|
+
"TARGETSTAGE"
|
|
484
|
+
];
|
|
485
|
+
var INJECTED = /^(?:SOURCE_DATE_EPOCH|(?:HTTPS?|FTP|NO|ALL)_PROXY)$/iu;
|
|
486
|
+
var PROXIES = /^(?:(?:HTTPS?|FTP|NO)_PROXY|(?:https?|ftp|no)_proxy)$/u;
|
|
487
|
+
var NON_ENV = ["BUILDKIT_SBOM_SCAN_CONTEXT", "BUILDKIT_SBOM_SCAN_STAGE"];
|
|
488
|
+
function expandWord(word, escape, model, lookup) {
|
|
489
|
+
const buildkit = model === "buildkit";
|
|
490
|
+
let i = 0;
|
|
491
|
+
let depth = 0;
|
|
492
|
+
const value = (name) => {
|
|
493
|
+
const v = lookup(name);
|
|
494
|
+
if (v instanceof Unknown) {
|
|
495
|
+
if (v.why !== "error")
|
|
496
|
+
fail(`ce cannot tell what $${name} is there`);
|
|
497
|
+
const exc = new Unreadable(v.reason);
|
|
498
|
+
exc.located = true;
|
|
499
|
+
throw exc;
|
|
500
|
+
}
|
|
501
|
+
return v;
|
|
502
|
+
};
|
|
503
|
+
const readName = () => {
|
|
504
|
+
const rest = word.slice(i);
|
|
505
|
+
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]) ?? "";
|
|
506
|
+
const after = rest.slice(name.length);
|
|
507
|
+
if (NON_ASCII.test(after[0] ?? "") && /^[\p{L}\p{Nd}]/u.test(after))
|
|
508
|
+
fail(`ce reads only ASCII names after a $, not in ${JSON.stringify(word)}`);
|
|
509
|
+
i += name.length;
|
|
510
|
+
return name;
|
|
511
|
+
};
|
|
512
|
+
const unsupported = () => fail(`ce does not evaluate the \${} in ${JSON.stringify(word)}`);
|
|
513
|
+
const dollar = () => {
|
|
514
|
+
i++;
|
|
515
|
+
if (word[i] !== "{") {
|
|
516
|
+
const name = readName();
|
|
517
|
+
return name ? value(name) ?? "" : "$";
|
|
518
|
+
}
|
|
519
|
+
i++;
|
|
520
|
+
const name = readName();
|
|
521
|
+
if (!name)
|
|
522
|
+
unsupported();
|
|
523
|
+
if (word[i] === "}") {
|
|
524
|
+
i++;
|
|
525
|
+
return value(name) ?? "";
|
|
526
|
+
}
|
|
527
|
+
const op = (buildkit ? /^:?[-+]/u : /^:[-+]/u).exec(word.slice(i))?.[0];
|
|
528
|
+
if (!op)
|
|
529
|
+
unsupported();
|
|
530
|
+
i += op.length;
|
|
531
|
+
if (++depth > 100)
|
|
532
|
+
fail("ce does not evaluate ${} nested this deeply");
|
|
533
|
+
const alternative = until("}");
|
|
534
|
+
depth--;
|
|
535
|
+
const v = value(name);
|
|
536
|
+
const set = op.startsWith(":") ? v !== undefined && v !== "" : v !== undefined;
|
|
537
|
+
return op.endsWith("-") ? set ? v : alternative : set ? alternative : "";
|
|
538
|
+
};
|
|
539
|
+
const until = (end) => {
|
|
540
|
+
let out = "";
|
|
541
|
+
while (i < word.length) {
|
|
542
|
+
const ch = word[i];
|
|
543
|
+
if (end && ch === end) {
|
|
544
|
+
i++;
|
|
545
|
+
return out;
|
|
546
|
+
}
|
|
547
|
+
if (ch === escape) {
|
|
548
|
+
out += word[i + 1] ?? "";
|
|
549
|
+
i += 2;
|
|
550
|
+
} else if (ch === "'") {
|
|
551
|
+
const close = word.indexOf("'", i + 1);
|
|
552
|
+
if (close < 0)
|
|
553
|
+
fail(`unterminated ' in ${JSON.stringify(word)}`);
|
|
554
|
+
out += word.slice(i + 1, close);
|
|
555
|
+
i = close + 1;
|
|
556
|
+
} else if (ch === '"') {
|
|
557
|
+
i++;
|
|
558
|
+
for (;; ) {
|
|
559
|
+
if (i >= word.length)
|
|
560
|
+
fail(`unterminated " in ${JSON.stringify(word)}`);
|
|
561
|
+
const c = word[i];
|
|
562
|
+
if (c === '"') {
|
|
563
|
+
i++;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
if (c === escape && ['"', "$", escape].includes(word[i + 1] ?? "")) {
|
|
567
|
+
out += word[i + 1];
|
|
568
|
+
i += 2;
|
|
569
|
+
} else if (c === "$")
|
|
570
|
+
out += dollar();
|
|
571
|
+
else {
|
|
572
|
+
out += c;
|
|
573
|
+
i++;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
} else if (ch === "$")
|
|
577
|
+
out += dollar();
|
|
578
|
+
else {
|
|
579
|
+
out += ch;
|
|
580
|
+
i++;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (end)
|
|
584
|
+
fail(`missing ${end} in ${JSON.stringify(word)}`);
|
|
585
|
+
return out;
|
|
586
|
+
};
|
|
587
|
+
return until("");
|
|
588
|
+
}
|
|
589
|
+
function argDefault(parsed, name, target, buildArg, model = "buildkit", { contexts = [], envs = [] } = {}) {
|
|
590
|
+
const buildkit = model === "buildkit";
|
|
591
|
+
const escape = buildkit ? parsed.escape : "\\";
|
|
592
|
+
const evaluate = (word, line, lookup) => {
|
|
593
|
+
try {
|
|
594
|
+
return expandWord(word, escape, model, lookup);
|
|
595
|
+
} catch (exc) {
|
|
596
|
+
if (!(exc instanceof Unreadable))
|
|
597
|
+
throw exc;
|
|
598
|
+
return new Unknown("error", exc.located ? exc.message : `${exc.message}, on line ${line}`);
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
const isBuiltin = (arg) => BUILTINS.includes(arg);
|
|
602
|
+
const plain = (name) => ![...`$'"`, escape].some((ch) => name.includes(ch));
|
|
603
|
+
const named = (kind, name, line) => plain(name) ? name : fail(`ce cannot tell what name the ${kind} on line ${line} sets`);
|
|
604
|
+
const globals = new Map;
|
|
605
|
+
for (const builtin of BUILTINS)
|
|
606
|
+
globals.set(builtin, buildArg(builtin) ?? new Unknown("builtin"));
|
|
607
|
+
for (const { pairs, line } of parsed.globals) {
|
|
608
|
+
const before = new Map(globals);
|
|
609
|
+
const seen = (ref) => {
|
|
610
|
+
const known = buildkit ? globals : before;
|
|
611
|
+
if (known.has(ref) || buildkit || !PROXIES.test(ref))
|
|
612
|
+
return known.get(ref);
|
|
613
|
+
return buildArg(ref) ?? new Unknown("injected");
|
|
614
|
+
};
|
|
615
|
+
for (const { name, value } of pairs) {
|
|
616
|
+
const arg = named("ARG", name, line);
|
|
617
|
+
const given = buildArg(arg);
|
|
618
|
+
if (given !== undefined)
|
|
619
|
+
globals.set(arg, given);
|
|
620
|
+
else if (INJECTED.test(arg))
|
|
621
|
+
globals.set(arg, new Unknown("injected"));
|
|
622
|
+
else if (value !== undefined && !(isBuiltin(arg) && !buildkit))
|
|
623
|
+
globals.set(arg, evaluate(value, line, seen));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const findStage = (wanted, before, target = false) => {
|
|
627
|
+
const at = parsed.stages.findLastIndex((s, i) => i < before && (buildkit ? stageKey(s.name) === stageKey(wanted) : (s.name || String(i)) === wanted || !target && String(i) === wanted));
|
|
628
|
+
if (at >= 0 || buildkit || !target || !/^\d+$/u.test(wanted))
|
|
629
|
+
return at;
|
|
630
|
+
const position = Number(wanted);
|
|
631
|
+
return position < before ? position : -1;
|
|
632
|
+
};
|
|
633
|
+
const replaced = (index) => {
|
|
634
|
+
const stage = parsed.stages[index].name;
|
|
635
|
+
return buildkit && stage ? contexts.find((c) => stageKey(familiar(c)) === stageKey(stage)) : undefined;
|
|
636
|
+
};
|
|
637
|
+
for (const stage of parsed.stages) {
|
|
638
|
+
if (!buildkit && !plain(stage.name))
|
|
639
|
+
fail(`ce cannot tell what the stage ${JSON.stringify(stage.name)} is named`);
|
|
640
|
+
}
|
|
641
|
+
const source = (index) => {
|
|
642
|
+
const stage = parsed.stages[index];
|
|
643
|
+
const base = evaluate(stage.base, stage.line, (ref) => globals.get(ref));
|
|
644
|
+
let parent = typeof base === "string" ? findStage(base, index) : -1;
|
|
645
|
+
const swapped = buildkit ? parent >= 0 ? replaced(parent) : undefined : contexts.find((c) => c === stage.base || c === base);
|
|
646
|
+
if (swapped !== undefined)
|
|
647
|
+
parent = -1;
|
|
648
|
+
return { base, parent, swapped };
|
|
649
|
+
};
|
|
650
|
+
const states = new Map;
|
|
651
|
+
const run = (index) => {
|
|
652
|
+
const known = states.get(index);
|
|
653
|
+
if (known)
|
|
654
|
+
return known;
|
|
655
|
+
const stage = parsed.stages[index];
|
|
656
|
+
const { base, parent, swapped } = source(index);
|
|
657
|
+
const from = parent >= 0 ? run(parent) : undefined;
|
|
658
|
+
const state = {
|
|
659
|
+
env: new Map,
|
|
660
|
+
args: new Map,
|
|
661
|
+
image: new Map(from?.image),
|
|
662
|
+
opaque: from?.opaque ?? (swapped !== undefined || base !== "scratch")
|
|
663
|
+
};
|
|
664
|
+
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`);
|
|
665
|
+
let triggers = [];
|
|
666
|
+
if (buildkit) {
|
|
667
|
+
if (base instanceof Unknown)
|
|
668
|
+
state.undeclared = new Unknown("error", `ce cannot tell what the FROM on line ${stage.line} is built from`);
|
|
669
|
+
else if (swapped !== undefined)
|
|
670
|
+
state.undeclared = lost(swapped);
|
|
671
|
+
else if (from) {
|
|
672
|
+
state.args = new Map(from.args);
|
|
673
|
+
state.undeclared = from.undeclared;
|
|
674
|
+
for (const [k, v] of from.env)
|
|
675
|
+
state.env.set(k, from.opaque || k === "PATH" ? BASE_ENV : v);
|
|
676
|
+
for (const [k, v] of from.image)
|
|
677
|
+
state.env.set(k, v);
|
|
678
|
+
triggers = parsed.stages[parent].instructions.filter((i) => i.onbuild);
|
|
679
|
+
}
|
|
680
|
+
} else {
|
|
681
|
+
if (from)
|
|
682
|
+
state.env = new Map(from.env);
|
|
683
|
+
for (const flag of envs) {
|
|
684
|
+
const spec = flag.slice(flag.indexOf(" ") + 1);
|
|
685
|
+
const names = flag.startsWith("--unsetenv ") ? spec.includes('"') ? ["*"] : spec.split(",") : [spec.split("=")[0].replace(/-$/u, "")];
|
|
686
|
+
const set = (k) => state.env.set(k, new Unknown("error", `${k} may be changed by ${flag}`));
|
|
687
|
+
for (const env of names)
|
|
688
|
+
if (env.includes("*"))
|
|
689
|
+
[...state.env.keys()].forEach(set);
|
|
690
|
+
else
|
|
691
|
+
set(env);
|
|
692
|
+
}
|
|
693
|
+
const written = findStage(stage.base, index);
|
|
694
|
+
if (swapped !== undefined)
|
|
695
|
+
state.undeclared = lost(swapped);
|
|
696
|
+
else if (written >= 0)
|
|
697
|
+
state.args = inheritedArgs(written);
|
|
698
|
+
for (const { kind, pairs, line } of parent >= 0 ? parsed.stages[parent].instructions.filter((i) => i.onbuild) : []) {
|
|
699
|
+
for (const { name: arg } of pairs) {
|
|
700
|
+
if (!plain(arg))
|
|
701
|
+
fail(`ce cannot tell what name the ONBUILD on line ${line} sets`);
|
|
702
|
+
(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`));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const see = (env) => (ref) => env.has(ref) ? env.get(ref) : BASE_ENV;
|
|
707
|
+
const own = stage.instructions.filter((i) => !i.onbuild);
|
|
708
|
+
for (const { kind, pairs, line } of [...triggers, ...own]) {
|
|
709
|
+
const before = new Map(state.env);
|
|
710
|
+
for (const { name, value } of pairs) {
|
|
711
|
+
const arg = named(kind, name, line);
|
|
712
|
+
const seen = buildkit && kind === "ARG" ? state.env : before;
|
|
713
|
+
const evaluated = (word) => evaluate(word, line, see(seen));
|
|
714
|
+
if (kind === "ENV") {
|
|
715
|
+
const v = value === undefined ? new Unknown("error", `ce does not read the ENV ${arg} on line ${line}`) : evaluated(value);
|
|
716
|
+
state.env.set(arg, v);
|
|
717
|
+
state.image.set(arg, v);
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
const given = buildArg(arg);
|
|
721
|
+
let v;
|
|
722
|
+
if (given !== undefined)
|
|
723
|
+
v = given;
|
|
724
|
+
else if (INJECTED.test(arg))
|
|
725
|
+
v = new Unknown("injected");
|
|
726
|
+
else if (isBuiltin(arg) && (!buildkit || value === undefined))
|
|
727
|
+
v = globals.get(arg);
|
|
728
|
+
else if (value !== undefined)
|
|
729
|
+
v = evaluated(value);
|
|
730
|
+
else if (globals.get(arg) !== undefined)
|
|
731
|
+
v = globals.get(arg);
|
|
732
|
+
else {
|
|
733
|
+
if (!state.args.has(arg))
|
|
734
|
+
state.args.set(arg, state.undeclared);
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
state.args.set(arg, v);
|
|
738
|
+
if (buildkit && !NON_ENV.includes(arg))
|
|
739
|
+
state.env.set(arg, v);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
states.set(index, state);
|
|
743
|
+
return state;
|
|
744
|
+
};
|
|
745
|
+
const inherited = new Map;
|
|
746
|
+
const inheritedArgs = (index) => {
|
|
747
|
+
const todo = [];
|
|
748
|
+
let i = index;
|
|
749
|
+
while (i >= 0 && !inherited.has(i)) {
|
|
750
|
+
todo.unshift(i);
|
|
751
|
+
i = findStage(parsed.stages[i].base, i);
|
|
752
|
+
}
|
|
753
|
+
let args = new Map(inherited.get(i));
|
|
754
|
+
for (const at of todo) {
|
|
755
|
+
for (const { kind, pairs, line, onbuild } of parsed.stages[at].instructions) {
|
|
756
|
+
if (kind !== "ARG" || onbuild)
|
|
757
|
+
continue;
|
|
758
|
+
for (const { name: arg, value } of pairs) {
|
|
759
|
+
named("ARG", arg, line);
|
|
760
|
+
const given = buildArg(arg);
|
|
761
|
+
if (given !== undefined)
|
|
762
|
+
args.set(arg, given);
|
|
763
|
+
else if (INJECTED.test(arg))
|
|
764
|
+
args.set(arg, new Unknown("injected"));
|
|
765
|
+
else if (isBuiltin(arg))
|
|
766
|
+
args.set(arg, globals.get(arg));
|
|
767
|
+
else if (value?.includes("$"))
|
|
768
|
+
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`));
|
|
769
|
+
else if (value !== undefined)
|
|
770
|
+
args.set(arg, evaluate(value, line, () => {
|
|
771
|
+
return;
|
|
772
|
+
}));
|
|
773
|
+
else if (globals.get(arg) !== undefined)
|
|
774
|
+
args.set(arg, globals.get(arg));
|
|
775
|
+
else if (!args.has(arg))
|
|
776
|
+
args.set(arg, undefined);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
inherited.set(at, args);
|
|
780
|
+
args = new Map(args);
|
|
781
|
+
}
|
|
782
|
+
return args;
|
|
783
|
+
};
|
|
784
|
+
let index = parsed.stages.length - 1;
|
|
785
|
+
if (index < 0)
|
|
786
|
+
fail("it has no FROM");
|
|
787
|
+
if (target) {
|
|
788
|
+
index = findStage(target, parsed.stages.length, true);
|
|
789
|
+
if (index < 0)
|
|
790
|
+
fail(`it has no stage named ${JSON.stringify(target)}`);
|
|
791
|
+
}
|
|
792
|
+
const swapped = replaced(index);
|
|
793
|
+
if (swapped !== undefined)
|
|
794
|
+
fail(`--build-context ${swapped} replaces the stage built`);
|
|
795
|
+
const ancestors = [];
|
|
796
|
+
for (let i = index;i >= 0; i = source(i).parent)
|
|
797
|
+
ancestors.unshift(i);
|
|
798
|
+
for (const i of ancestors)
|
|
799
|
+
run(i);
|
|
800
|
+
const state = run(index);
|
|
801
|
+
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));
|
|
802
|
+
if (v instanceof Unknown) {
|
|
803
|
+
if (v.why === "builtin")
|
|
804
|
+
fail(`${name} is set by the builder`);
|
|
805
|
+
if (v.why === "injected")
|
|
806
|
+
fail(`${name} may be set by the builder`);
|
|
807
|
+
fail(v.why === "error" ? v.reason : `ce cannot tell what ${name} is`);
|
|
808
|
+
}
|
|
809
|
+
return v;
|
|
810
|
+
}
|
|
811
|
+
function readContainerfile(file, model) {
|
|
812
|
+
let text;
|
|
813
|
+
try {
|
|
814
|
+
text = fs.readFileSync(file, "utf8");
|
|
815
|
+
} catch (exc) {
|
|
816
|
+
return fail(`ce cannot read ${JSON.stringify(file)}: ${errorMessage(exc)}`);
|
|
817
|
+
}
|
|
818
|
+
try {
|
|
819
|
+
return parseContainerfile(text, model);
|
|
820
|
+
} catch (exc) {
|
|
821
|
+
if (exc instanceof Unreadable)
|
|
822
|
+
fail(`${JSON.stringify(file)}: ${exc.message}`);
|
|
823
|
+
throw exc;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
function containerfileDefaults(files, buildArg) {
|
|
827
|
+
const docker = files.engine === "docker";
|
|
828
|
+
const model = docker ? "buildkit" : "imagebuilder";
|
|
829
|
+
let read;
|
|
830
|
+
const load = () => {
|
|
831
|
+
let file;
|
|
832
|
+
let parsed;
|
|
833
|
+
try {
|
|
834
|
+
file = findContainerfile(files);
|
|
835
|
+
parsed = readContainerfile(file, model);
|
|
836
|
+
} catch (exc) {
|
|
837
|
+
if (exc instanceof Unreadable)
|
|
838
|
+
return { error: exc };
|
|
839
|
+
throw exc;
|
|
840
|
+
}
|
|
841
|
+
const frontend = docker ? buildArg("BUILDKIT_SYNTAX")?.trim().split(" ")[0] ?? parsed.syntax : undefined;
|
|
842
|
+
if (frontend !== undefined && !STANDARD_SYNTAX.test(frontend))
|
|
843
|
+
return {
|
|
844
|
+
file,
|
|
845
|
+
error: new Unreadable(`it is read by the frontend ${JSON.stringify(frontend)}, which may read it differently`)
|
|
846
|
+
};
|
|
847
|
+
return { file, parsed };
|
|
848
|
+
};
|
|
849
|
+
return (name) => {
|
|
850
|
+
read ??= load();
|
|
851
|
+
try {
|
|
852
|
+
if (read.error)
|
|
853
|
+
throw read.error;
|
|
854
|
+
return argDefault(read.parsed, name, files.target, buildArg, model, {
|
|
855
|
+
contexts: files.contexts ?? [],
|
|
856
|
+
envs: files.envs ?? []
|
|
857
|
+
});
|
|
858
|
+
} catch (exc) {
|
|
859
|
+
if (!(exc instanceof Unreadable))
|
|
860
|
+
throw exc;
|
|
861
|
+
const where = read.file ? ` from ${JSON.stringify(read.file)}` : "";
|
|
862
|
+
throw new CeError(`ce: {arg.${name}} has no value from --build-arg, and ce cannot work out its default${where}: ${exc.message}`);
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
128
867
|
// src/json.ts
|
|
129
868
|
import { randomUUID } from "node:crypto";
|
|
130
|
-
import
|
|
131
|
-
import
|
|
869
|
+
import fs2 from "node:fs";
|
|
870
|
+
import path2 from "node:path";
|
|
132
871
|
function isObject(value) {
|
|
133
872
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
134
873
|
}
|
|
135
874
|
function readJsonFile(file, fallback) {
|
|
136
875
|
try {
|
|
137
|
-
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(
|
|
876
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(fs2.readFileSync(file)));
|
|
138
877
|
} catch (exc) {
|
|
139
878
|
if (isErrno(exc, "ENOENT"))
|
|
140
879
|
return fallback;
|
|
@@ -145,22 +884,22 @@ function dumpJson(payload) {
|
|
|
145
884
|
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
885
|
}
|
|
147
886
|
function writePrivateJson(file, payload) {
|
|
148
|
-
const directory =
|
|
149
|
-
|
|
150
|
-
const temporary =
|
|
151
|
-
const fd =
|
|
887
|
+
const directory = path2.dirname(file);
|
|
888
|
+
fs2.mkdirSync(directory, { recursive: true, mode: 448 });
|
|
889
|
+
const temporary = path2.join(directory, `.ce-forgejo-${randomUUID().slice(0, 8)}`);
|
|
890
|
+
const fd = fs2.openSync(temporary, "wx", 384);
|
|
152
891
|
try {
|
|
153
892
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
893
|
+
fs2.fchmodSync(fd, 384);
|
|
894
|
+
fs2.writeSync(fd, `${dumpJson(payload)}
|
|
156
895
|
`);
|
|
157
|
-
|
|
896
|
+
fs2.fsyncSync(fd);
|
|
158
897
|
} finally {
|
|
159
|
-
|
|
898
|
+
fs2.closeSync(fd);
|
|
160
899
|
}
|
|
161
|
-
|
|
900
|
+
fs2.renameSync(temporary, file);
|
|
162
901
|
} finally {
|
|
163
|
-
|
|
902
|
+
fs2.rmSync(temporary, { force: true });
|
|
164
903
|
}
|
|
165
904
|
}
|
|
166
905
|
|
|
@@ -244,7 +983,7 @@ class CiMetadata {
|
|
|
244
983
|
get(name) {
|
|
245
984
|
const envName = ENV_VARIABLE.exec(name)?.[1];
|
|
246
985
|
if (envName)
|
|
247
|
-
return nonEmpty(this.#env[envName]);
|
|
986
|
+
return Object.hasOwn(this.#env, envName) ? nonEmpty(this.#env[envName]) : undefined;
|
|
248
987
|
if (!this.#cache.has(name))
|
|
249
988
|
this.#cache.set(name, this.#resolve(name));
|
|
250
989
|
return this.#cache.get(name);
|
|
@@ -341,7 +1080,10 @@ class CiMetadata {
|
|
|
341
1080
|
const pull = /^refs\/pull\/(\d+)\//u.exec(ref);
|
|
342
1081
|
if (event.startsWith("pull_request") || pull) {
|
|
343
1082
|
const payload = pull ? {} : this.#payload();
|
|
344
|
-
const fromPayload = [
|
|
1083
|
+
const fromPayload = [
|
|
1084
|
+
isObject(payload.pull_request) && payload.pull_request.number,
|
|
1085
|
+
payload.number
|
|
1086
|
+
].find(Number.isInteger);
|
|
345
1087
|
const number = pull?.[1] ?? (fromPayload === undefined ? undefined : String(fromPayload));
|
|
346
1088
|
if (!number)
|
|
347
1089
|
throw new CeError(`ce: cannot find the pull request number for ${event} on ${ref}`);
|
|
@@ -402,13 +1144,13 @@ function onDefaultBranch(vars, branch) {
|
|
|
402
1144
|
|
|
403
1145
|
// src/system.ts
|
|
404
1146
|
import { spawnSync } from "node:child_process";
|
|
405
|
-
import
|
|
406
|
-
import
|
|
1147
|
+
import fs3 from "node:fs";
|
|
1148
|
+
import path3 from "node:path";
|
|
407
1149
|
import { setTimeout } from "node:timers/promises";
|
|
408
1150
|
var io = {
|
|
409
|
-
stdout: (text) => void
|
|
410
|
-
stderr: (text) => void
|
|
411
|
-
stdin: () =>
|
|
1151
|
+
stdout: (text) => void fs3.writeSync(1, text),
|
|
1152
|
+
stderr: (text) => void fs3.writeSync(2, text),
|
|
1153
|
+
stdin: () => fs3.readFileSync(0, "utf8"),
|
|
412
1154
|
argv0: () => process.argv[1] ?? "ce"
|
|
413
1155
|
};
|
|
414
1156
|
function log(line) {
|
|
@@ -423,11 +1165,11 @@ function childEnv() {
|
|
|
423
1165
|
return Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
|
|
424
1166
|
}
|
|
425
1167
|
function which(command) {
|
|
426
|
-
const candidates = command.includes("/") ? [command] : (process.env.PATH ?? "").split(
|
|
1168
|
+
const candidates = command.includes("/") ? [command] : (process.env.PATH ?? "").split(path3.delimiter).filter(Boolean).map((dir) => path3.join(dir, command));
|
|
427
1169
|
for (const candidate of candidates) {
|
|
428
1170
|
try {
|
|
429
|
-
|
|
430
|
-
if (
|
|
1171
|
+
fs3.accessSync(candidate, fs3.constants.X_OK);
|
|
1172
|
+
if (fs3.statSync(candidate).isFile())
|
|
431
1173
|
return candidate;
|
|
432
1174
|
} catch {}
|
|
433
1175
|
}
|
|
@@ -459,13 +1201,13 @@ var system = {
|
|
|
459
1201
|
}
|
|
460
1202
|
};
|
|
461
1203
|
function binDir() {
|
|
462
|
-
return
|
|
1204
|
+
return fs3.realpathSync(path3.join(import.meta.dirname, "..", "bin"));
|
|
463
1205
|
}
|
|
464
1206
|
function ensureHelperOnPath() {
|
|
465
1207
|
const dir = binDir();
|
|
466
|
-
const parts = (process.env.PATH ?? "").split(
|
|
1208
|
+
const parts = (process.env.PATH ?? "").split(path3.delimiter);
|
|
467
1209
|
if (!parts.includes(dir))
|
|
468
|
-
process.env.PATH = [dir, ...parts].join(
|
|
1210
|
+
process.env.PATH = [dir, ...parts].join(path3.delimiter);
|
|
469
1211
|
}
|
|
470
1212
|
|
|
471
1213
|
// src/template.ts
|
|
@@ -494,12 +1236,26 @@ function parse(template, vars) {
|
|
|
494
1236
|
text(template.slice(last));
|
|
495
1237
|
return parts;
|
|
496
1238
|
}
|
|
497
|
-
function
|
|
498
|
-
|
|
1239
|
+
function guardsPass(parts, vars) {
|
|
1240
|
+
let unknown;
|
|
499
1241
|
for (const part of parts) {
|
|
500
|
-
if ("name" in part
|
|
501
|
-
|
|
1242
|
+
if (!("name" in part) || !part.guard)
|
|
1243
|
+
continue;
|
|
1244
|
+
try {
|
|
1245
|
+
if (vars.get(part.name) === undefined)
|
|
1246
|
+
return false;
|
|
1247
|
+
} catch (exc) {
|
|
1248
|
+
unknown ??= exc;
|
|
1249
|
+
}
|
|
502
1250
|
}
|
|
1251
|
+
if (unknown !== undefined)
|
|
1252
|
+
throw unknown;
|
|
1253
|
+
return true;
|
|
1254
|
+
}
|
|
1255
|
+
function expand(template, vars, encode) {
|
|
1256
|
+
const parts = parse(template, vars);
|
|
1257
|
+
if (!guardsPass(parts, vars))
|
|
1258
|
+
return null;
|
|
503
1259
|
let out = "";
|
|
504
1260
|
for (const part of parts) {
|
|
505
1261
|
if ("text" in part)
|
|
@@ -514,16 +1270,21 @@ function expand(template, vars, encode) {
|
|
|
514
1270
|
}
|
|
515
1271
|
return out;
|
|
516
1272
|
}
|
|
517
|
-
function checkValue(template, vars, what) {
|
|
518
|
-
|
|
519
|
-
|
|
1273
|
+
function checkValue(template, vars, what, guards = false) {
|
|
1274
|
+
const parts = parse(template, vars);
|
|
1275
|
+
if (!guards && parts.some((part) => ("guard" in part) && part.guard)) {
|
|
1276
|
+
throw new CeError(`ce: ${what} ${JSON.stringify(template)} cannot use {?guards}; they only apply to tags and prefixes`);
|
|
520
1277
|
}
|
|
521
1278
|
}
|
|
522
|
-
function expandValue(template, vars, encode, what) {
|
|
523
|
-
checkValue(template, vars, what);
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
1279
|
+
function expandValue(template, vars, encode, what, guards = false) {
|
|
1280
|
+
checkValue(template, vars, what, guards);
|
|
1281
|
+
const parts = parse(template, vars);
|
|
1282
|
+
if (!guardsPass(parts, vars))
|
|
1283
|
+
return "";
|
|
1284
|
+
for (const part of parts) {
|
|
1285
|
+
if ("name" in part && !part.guard && vars.get(part.name) === undefined) {
|
|
1286
|
+
const hint = guards ? ` To leave the ${what} empty instead, guard it with {?${part.name}}.` : "";
|
|
1287
|
+
throw new CeError(`ce: {${part.name}} has no value here, in the ${what} ${JSON.stringify(template)}.${hint}`);
|
|
527
1288
|
}
|
|
528
1289
|
}
|
|
529
1290
|
return expand(template, vars, encode);
|
|
@@ -585,7 +1346,7 @@ function expandImage(template, vars) {
|
|
|
585
1346
|
return image;
|
|
586
1347
|
}
|
|
587
1348
|
function expandPrefix(prefix, vars, what) {
|
|
588
|
-
return hasPlaceholders(prefix) ? expandValue(prefix, vars, encodeTagValue, what) : prefix;
|
|
1349
|
+
return hasPlaceholders(prefix) ? expandValue(prefix, vars, encodeTagValue, what, true) : prefix;
|
|
589
1350
|
}
|
|
590
1351
|
function literalRef(repo, tag) {
|
|
591
1352
|
if (tag.includes("/")) {
|
|
@@ -608,7 +1369,7 @@ function rewriteTagValue(value, tagPrefix, vars = new CiMetadata) {
|
|
|
608
1369
|
if (hasPlaceholders(repoTemplate))
|
|
609
1370
|
checkValue(repoTemplate, vars, "image name");
|
|
610
1371
|
if (hasPlaceholders(tagPrefix))
|
|
611
|
-
checkValue(tagPrefix, vars, "tag prefix");
|
|
1372
|
+
checkValue(tagPrefix, vars, "tag prefix", true);
|
|
612
1373
|
let repo;
|
|
613
1374
|
const expandRepo = () => repo ??= hasPlaceholders(repoTemplate) ? expandImage(repoTemplate, vars) : repoTemplate;
|
|
614
1375
|
if (tagList === undefined)
|
|
@@ -738,8 +1499,50 @@ var SUMMARIZED = {
|
|
|
738
1499
|
"--build-arg": "buildArgs",
|
|
739
1500
|
"--label": "labels"
|
|
740
1501
|
};
|
|
741
|
-
|
|
742
|
-
|
|
1502
|
+
var ARG_VARIABLE = /^arg\.([A-Za-z_][A-Za-z0-9_]*)$/u;
|
|
1503
|
+
function withBuildArgs(vars, values, argFile, fromFile) {
|
|
1504
|
+
return {
|
|
1505
|
+
known: (name) => ARG_VARIABLE.test(name) || vars.known(name),
|
|
1506
|
+
get(name) {
|
|
1507
|
+
const arg = ARG_VARIABLE.exec(name)?.[1];
|
|
1508
|
+
if (arg === undefined)
|
|
1509
|
+
return vars.get(name);
|
|
1510
|
+
const value = values.get(arg);
|
|
1511
|
+
if (typeof value === "string")
|
|
1512
|
+
return value || undefined;
|
|
1513
|
+
if (argFile) {
|
|
1514
|
+
throw new CeError(`ce: {${name}} has no value from --build-arg, and may get one from --build-arg-file, which ce does not read`);
|
|
1515
|
+
}
|
|
1516
|
+
return fromFile(arg) || undefined;
|
|
1517
|
+
}
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
var SHORT_BOOLS = {
|
|
1521
|
+
docker: "qD",
|
|
1522
|
+
podman: "qD",
|
|
1523
|
+
buildah: "qD"
|
|
1524
|
+
};
|
|
1525
|
+
function ungroup(argv, engine) {
|
|
1526
|
+
const end = argv.indexOf("--");
|
|
1527
|
+
return argv.flatMap((arg, at) => {
|
|
1528
|
+
if (end >= 0 && at >= end || !/^-[^-]/u.test(arg))
|
|
1529
|
+
return [arg];
|
|
1530
|
+
const split = [];
|
|
1531
|
+
let i = 1;
|
|
1532
|
+
while (i < arg.length - 1 && SHORT_BOOLS[engine].includes(arg[i]) && arg[i + 1] !== "=")
|
|
1533
|
+
split.push(`-${arg[i++]}`);
|
|
1534
|
+
return i === 1 ? [arg] : [...split, `-${arg.slice(i)}`];
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
function parseBuildArgs(given, tagPrefix, cachePrefix, {
|
|
1538
|
+
vars = new CiMetadata,
|
|
1539
|
+
labels = {},
|
|
1540
|
+
defaultBranch,
|
|
1541
|
+
env = process.env,
|
|
1542
|
+
engine = "docker"
|
|
1543
|
+
} = {}) {
|
|
1544
|
+
const argv = ungroup(given, engine);
|
|
1545
|
+
const parts = [];
|
|
743
1546
|
const rawCacheFrom = [];
|
|
744
1547
|
const rawCacheTo = [];
|
|
745
1548
|
const summary = {
|
|
@@ -754,16 +1557,31 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
|
|
|
754
1557
|
context: ""
|
|
755
1558
|
};
|
|
756
1559
|
let templated = false;
|
|
1560
|
+
const buildArgs = new Map;
|
|
1561
|
+
let argFile = false;
|
|
1562
|
+
const files = [];
|
|
1563
|
+
const positionals = [];
|
|
1564
|
+
const contexts = [];
|
|
1565
|
+
const envs = [];
|
|
757
1566
|
let rest = [];
|
|
758
1567
|
const addTags = (value, flag) => {
|
|
759
1568
|
templated ||= hasPlaceholders(value);
|
|
760
|
-
|
|
761
|
-
for (const ref of refs)
|
|
762
|
-
args.push(`${flag}${ref}`);
|
|
763
|
-
summary.tags.push(...refs);
|
|
1569
|
+
parts.push({ tags: value, flag });
|
|
764
1570
|
};
|
|
765
1571
|
const summarize = (flag, value) => {
|
|
766
1572
|
const field = SUMMARIZED[flag];
|
|
1573
|
+
if (field === "buildArgs") {
|
|
1574
|
+
if (value.includes("="))
|
|
1575
|
+
buildArgs.set(...splitAt(value, "="));
|
|
1576
|
+
else if (Object.hasOwn(env, value))
|
|
1577
|
+
buildArgs.set(value, env[value]);
|
|
1578
|
+
else if (engine !== "docker")
|
|
1579
|
+
buildArgs.set(value, null);
|
|
1580
|
+
if (engine !== "docker" && buildArgs.has("TARGETPLATFORM"))
|
|
1581
|
+
buildArgs.set("TARGETPLATFORM", null);
|
|
1582
|
+
}
|
|
1583
|
+
if (field === "dockerfile")
|
|
1584
|
+
files.push(value);
|
|
767
1585
|
if (field === "buildArgs" || field === "labels")
|
|
768
1586
|
summary[field].push(value);
|
|
769
1587
|
else
|
|
@@ -773,8 +1591,10 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
|
|
|
773
1591
|
const arg = argv[i];
|
|
774
1592
|
const [name, attached] = arg.startsWith("--") && arg.includes("=") ? splitAt(arg, "=") : [arg, undefined];
|
|
775
1593
|
const next = () => consumeValue(argv.slice(++i), arg)[0];
|
|
1594
|
+
const short = () => arg.slice(arg[2] === "=" ? 3 : 2);
|
|
776
1595
|
if (arg === "--") {
|
|
777
1596
|
rest = argv.slice(i);
|
|
1597
|
+
positionals.push(...rest.slice(1));
|
|
778
1598
|
if (rest.length > 1)
|
|
779
1599
|
summary.context = rest.at(-1);
|
|
780
1600
|
break;
|
|
@@ -783,7 +1603,7 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
|
|
|
783
1603
|
else if (arg === "--tag")
|
|
784
1604
|
addTags(next(), "--tag=");
|
|
785
1605
|
else if (arg.startsWith("-t"))
|
|
786
|
-
addTags(
|
|
1606
|
+
addTags(short(), "-t");
|
|
787
1607
|
else if (name === "--tag")
|
|
788
1608
|
addTags(attached, "--tag=");
|
|
789
1609
|
else if (name === "--cache-from")
|
|
@@ -798,21 +1618,51 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
|
|
|
798
1618
|
const flag = name;
|
|
799
1619
|
if (attached === undefined) {
|
|
800
1620
|
const v = next();
|
|
801
|
-
|
|
1621
|
+
parts.push(arg, v);
|
|
802
1622
|
summarize(flag, v);
|
|
803
1623
|
} else {
|
|
804
|
-
|
|
1624
|
+
parts.push(arg);
|
|
805
1625
|
summarize(flag, attached);
|
|
806
1626
|
}
|
|
1627
|
+
} else if (name === "--build-arg-file") {
|
|
1628
|
+
argFile = true;
|
|
1629
|
+
parts.push(arg);
|
|
1630
|
+
if (attached === undefined)
|
|
1631
|
+
parts.push(next());
|
|
1632
|
+
} else if (name === "--build-context") {
|
|
1633
|
+
const v = attached ?? next();
|
|
1634
|
+
parts.push(arg);
|
|
1635
|
+
if (attached === undefined)
|
|
1636
|
+
parts.push(v);
|
|
1637
|
+
contexts.push(v.split("=")[0]);
|
|
1638
|
+
} else if ((name === "--env" || name === "--unsetenv") && engine !== "docker") {
|
|
1639
|
+
const v = attached ?? next();
|
|
1640
|
+
parts.push(arg);
|
|
1641
|
+
if (attached === undefined)
|
|
1642
|
+
parts.push(v);
|
|
1643
|
+
envs.push(`${name} ${v}`);
|
|
807
1644
|
} else if (arg.startsWith("-f") && arg.length > 2) {
|
|
808
|
-
|
|
809
|
-
summary.dockerfile =
|
|
1645
|
+
parts.push(arg);
|
|
1646
|
+
summary.dockerfile = short();
|
|
1647
|
+
files.push(summary.dockerfile);
|
|
810
1648
|
} else {
|
|
811
|
-
|
|
812
|
-
if (!arg.startsWith("-"))
|
|
1649
|
+
parts.push(arg);
|
|
1650
|
+
if (!arg.startsWith("-")) {
|
|
1651
|
+
positionals.push(arg);
|
|
813
1652
|
summary.context = arg;
|
|
1653
|
+
}
|
|
814
1654
|
}
|
|
815
1655
|
}
|
|
1656
|
+
const baseVars = vars;
|
|
1657
|
+
const fromFile = containerfileDefaults({ engine, files, positionals, target: summary.target, contexts, envs }, (arg) => buildArgs.get(arg) ?? undefined);
|
|
1658
|
+
vars = withBuildArgs(baseVars, buildArgs, argFile, fromFile);
|
|
1659
|
+
const args = parts.flatMap((part) => {
|
|
1660
|
+
if (typeof part === "string")
|
|
1661
|
+
return [part];
|
|
1662
|
+
const refs = rewriteTagValue(part.tags, tagPrefix, vars);
|
|
1663
|
+
summary.tags.push(...refs);
|
|
1664
|
+
return refs.map((ref) => `${part.flag}${ref}`);
|
|
1665
|
+
});
|
|
816
1666
|
if (templated && !summary.tags.length) {
|
|
817
1667
|
throw new CeError("ce: the guards in the -t templates left out every tag, so the image would have none");
|
|
818
1668
|
}
|
|
@@ -827,7 +1677,11 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
|
|
|
827
1677
|
const cacheFrom = rawCacheFrom.map((c) => toNames.has(cacheScopeName(c)) ? normalizeCacheScope(c, cachePrefix) : c);
|
|
828
1678
|
const ghaTo = rawCacheTo.filter(isGhaCache);
|
|
829
1679
|
if (defaultBranch && ghaTo.length) {
|
|
830
|
-
const
|
|
1680
|
+
const fileArg = (arg) => {
|
|
1681
|
+
throw new CeError(`ce: the default branch's cache scope cannot use {arg.${arg}} from the Containerfile, which may differ there`);
|
|
1682
|
+
};
|
|
1683
|
+
const onDefault = withBuildArgs(onDefaultBranch(baseVars, defaultBranch()), buildArgs, argFile, fileArg);
|
|
1684
|
+
const prefix = expandPrefix(prefixTemplate, onDefault, "cache prefix");
|
|
831
1685
|
cacheFrom.push(...ghaTo.map((c) => normalizeCacheScope(withoutCacheMode(c), prefix)));
|
|
832
1686
|
}
|
|
833
1687
|
for (const rewritten of new Set(cacheFrom)) {
|
|
@@ -851,7 +1705,11 @@ function printBuildSummary(engine, args, summary) {
|
|
|
851
1705
|
if (!command || command.tagsOnly)
|
|
852
1706
|
return;
|
|
853
1707
|
const subcommand = command.name;
|
|
854
|
-
const out = [
|
|
1708
|
+
const out = [
|
|
1709
|
+
"[ce] build summary:",
|
|
1710
|
+
` engine: ${engine}`,
|
|
1711
|
+
` subcommand: ${subcommand}`
|
|
1712
|
+
];
|
|
855
1713
|
const list = (title, items) => {
|
|
856
1714
|
if (items.length)
|
|
857
1715
|
out.push(` ${title}:`, ...items.map((item) => ` - ${item}`));
|
|
@@ -889,14 +1747,20 @@ async function forgejoOidcJwt(audience, requestUrl, requestToken) {
|
|
|
889
1747
|
throw new Error("credentials in URL");
|
|
890
1748
|
url.searchParams.set("audience", audience);
|
|
891
1749
|
tokenUrl = url.href;
|
|
892
|
-
headers = {
|
|
1750
|
+
headers = {
|
|
1751
|
+
Accept: "application/json",
|
|
1752
|
+
Authorization: `bearer ${requestToken}`
|
|
1753
|
+
};
|
|
893
1754
|
new Headers(headers);
|
|
894
1755
|
} catch {
|
|
895
1756
|
throw new CeError("token request URL or headers are invalid");
|
|
896
1757
|
}
|
|
897
1758
|
let body;
|
|
898
1759
|
try {
|
|
899
|
-
const response = await fetch(tokenUrl, {
|
|
1760
|
+
const response = await fetch(tokenUrl, {
|
|
1761
|
+
headers,
|
|
1762
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
1763
|
+
});
|
|
900
1764
|
if (!response.ok)
|
|
901
1765
|
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
902
1766
|
body = await response.arrayBuffer();
|
|
@@ -937,20 +1801,20 @@ function needsRefresh(expiry) {
|
|
|
937
1801
|
}
|
|
938
1802
|
|
|
939
1803
|
// src/state.ts
|
|
940
|
-
import
|
|
1804
|
+
import fs5 from "node:fs";
|
|
941
1805
|
import { homedir } from "node:os";
|
|
942
|
-
import
|
|
1806
|
+
import path4 from "node:path";
|
|
943
1807
|
|
|
944
1808
|
// src/lock.ts
|
|
945
|
-
import
|
|
1809
|
+
import fs4 from "node:fs";
|
|
946
1810
|
import { hostname } from "node:os";
|
|
947
1811
|
import { setTimeout as setTimeout2 } from "node:timers/promises";
|
|
948
1812
|
var STALE_AFTER_MS = 120000;
|
|
949
1813
|
var POLL_MS = 50;
|
|
950
1814
|
function snapshot(lockPath) {
|
|
951
1815
|
try {
|
|
952
|
-
const { ino, mtimeMs } =
|
|
953
|
-
return { ino, mtimeMs, owner:
|
|
1816
|
+
const { ino, mtimeMs } = fs4.statSync(lockPath);
|
|
1817
|
+
return { ino, mtimeMs, owner: fs4.readFileSync(lockPath, "utf8").trim() };
|
|
954
1818
|
} catch (exc) {
|
|
955
1819
|
if (isErrno(exc, "ENOENT"))
|
|
956
1820
|
return null;
|
|
@@ -976,7 +1840,7 @@ function breakIfStale(lockPath) {
|
|
|
976
1840
|
return;
|
|
977
1841
|
const guard = `${lockPath}.break`;
|
|
978
1842
|
try {
|
|
979
|
-
|
|
1843
|
+
fs4.writeFileSync(guard, "", { flag: "wx", mode: 384 });
|
|
980
1844
|
} catch (exc) {
|
|
981
1845
|
if (!isErrno(exc, "EEXIST"))
|
|
982
1846
|
throw exc;
|
|
@@ -989,10 +1853,10 @@ function breakIfStale(lockPath) {
|
|
|
989
1853
|
try {
|
|
990
1854
|
const current = snapshot(lockPath);
|
|
991
1855
|
if (current && current.ino === stale.ino && current.mtimeMs === stale.mtimeMs && current.owner === stale.owner) {
|
|
992
|
-
|
|
1856
|
+
fs4.rmSync(lockPath, { force: true });
|
|
993
1857
|
}
|
|
994
1858
|
} finally {
|
|
995
|
-
|
|
1859
|
+
fs4.rmSync(guard, { force: true });
|
|
996
1860
|
}
|
|
997
1861
|
}
|
|
998
1862
|
async function withFileLock(lockPath, fn) {
|
|
@@ -1001,7 +1865,7 @@ async function withFileLock(lockPath, fn) {
|
|
|
1001
1865
|
for (;; ) {
|
|
1002
1866
|
let fd;
|
|
1003
1867
|
try {
|
|
1004
|
-
fd =
|
|
1868
|
+
fd = fs4.openSync(lockPath, "wx", 384);
|
|
1005
1869
|
} catch (exc) {
|
|
1006
1870
|
if (!isErrno(exc, "EEXIST"))
|
|
1007
1871
|
throw exc;
|
|
@@ -1010,11 +1874,11 @@ async function withFileLock(lockPath, fn) {
|
|
|
1010
1874
|
continue;
|
|
1011
1875
|
}
|
|
1012
1876
|
try {
|
|
1013
|
-
|
|
1877
|
+
fs4.writeSync(fd, `${owner}
|
|
1014
1878
|
`);
|
|
1015
|
-
ino =
|
|
1879
|
+
ino = fs4.fstatSync(fd).ino;
|
|
1016
1880
|
} finally {
|
|
1017
|
-
|
|
1881
|
+
fs4.closeSync(fd);
|
|
1018
1882
|
}
|
|
1019
1883
|
break;
|
|
1020
1884
|
}
|
|
@@ -1023,7 +1887,7 @@ async function withFileLock(lockPath, fn) {
|
|
|
1023
1887
|
} finally {
|
|
1024
1888
|
const current = snapshot(lockPath);
|
|
1025
1889
|
if (current?.ino === ino && current.owner === owner)
|
|
1026
|
-
|
|
1890
|
+
fs4.rmSync(lockPath, { force: true });
|
|
1027
1891
|
}
|
|
1028
1892
|
}
|
|
1029
1893
|
|
|
@@ -1033,16 +1897,16 @@ var STATE_FILE = "ce-forgejo-credentials.json";
|
|
|
1033
1897
|
function dockerConfigDir() {
|
|
1034
1898
|
const configured = process.env.DOCKER_CONFIG;
|
|
1035
1899
|
if (!configured)
|
|
1036
|
-
return
|
|
1037
|
-
const expanded = configured === "~" || configured.startsWith("~/") ?
|
|
1038
|
-
return
|
|
1900
|
+
return path4.join(homedir(), ".docker");
|
|
1901
|
+
const expanded = configured === "~" || configured.startsWith("~/") ? path4.join(homedir(), configured.slice(1)) : configured;
|
|
1902
|
+
return path4.resolve(expanded);
|
|
1039
1903
|
}
|
|
1040
1904
|
function helperStatePath() {
|
|
1041
|
-
return
|
|
1905
|
+
return path4.join(dockerConfigDir(), STATE_FILE);
|
|
1042
1906
|
}
|
|
1043
1907
|
async function withHelperState(fn) {
|
|
1044
1908
|
const statePath = helperStatePath();
|
|
1045
|
-
|
|
1909
|
+
fs5.mkdirSync(path4.dirname(statePath), { recursive: true, mode: 448 });
|
|
1046
1910
|
return withFileLock(`${statePath}.pidlock`, async () => {
|
|
1047
1911
|
const state = readJsonFile(statePath, { version: 1, registries: {} });
|
|
1048
1912
|
if (!isObject(state) || !isObject(state.registries)) {
|
|
@@ -1071,7 +1935,7 @@ async function saveHelperState(registry, username, audience, authority, jwt) {
|
|
|
1071
1935
|
}
|
|
1072
1936
|
async function configureDockerCredentialHelper(registry, username, audience, authority, jwt) {
|
|
1073
1937
|
await saveHelperState(registry, username, audience, authority, jwt);
|
|
1074
|
-
const configPath =
|
|
1938
|
+
const configPath = path4.join(dockerConfigDir(), "config.json");
|
|
1075
1939
|
const config = readJsonFile(configPath, {});
|
|
1076
1940
|
if (!isObject(config))
|
|
1077
1941
|
throw new CeError("Docker config is not a JSON object");
|
|
@@ -1200,18 +2064,9 @@ function envFlag(name) {
|
|
|
1200
2064
|
function shellQuote(arg) {
|
|
1201
2065
|
return /^[A-Za-z0-9_@%+:,./-][A-Za-z0-9_@%+=:,./-]*$/u.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
|
|
1202
2066
|
}
|
|
1203
|
-
function engineCommand(engine, args) {
|
|
1204
|
-
if (engine === "docker" || !system.which("gosu"))
|
|
1205
|
-
return [engine, ...args];
|
|
1206
|
-
const user = process.env.USER ?? "";
|
|
1207
|
-
if (!user) {
|
|
1208
|
-
logError("ce: gosu is available but USER is not set; cannot drop privileges. Set USER or remove gosu from PATH.");
|
|
1209
|
-
return null;
|
|
1210
|
-
}
|
|
1211
|
-
return ["gosu", user, engine, ...args];
|
|
1212
|
-
}
|
|
1213
2067
|
async function runEngine(engine, argv, tagPrefix, cachePrefix, options = {}) {
|
|
1214
2068
|
const dryRun = options.dryRun ?? envFlag("CE_DRY_RUN");
|
|
2069
|
+
ensureHelperOnPath();
|
|
1215
2070
|
const command = findBuildCommand(engine, argv);
|
|
1216
2071
|
let args = argv;
|
|
1217
2072
|
let summary;
|
|
@@ -1220,6 +2075,7 @@ async function runEngine(engine, argv, tagPrefix, cachePrefix, options = {}) {
|
|
|
1220
2075
|
const labels = !command.tagsOnly && (options.ociLabels ?? envFlag("OCI_LABELS")) ? vars.labels() : {};
|
|
1221
2076
|
const fromDefault = !command.tagsOnly && (options.cacheFromDefault ?? envFlag("CACHE_FROM_DEFAULT"));
|
|
1222
2077
|
const parsed = parseBuildArgs(argv.slice(command.start), tagPrefix, cachePrefix, {
|
|
2078
|
+
engine,
|
|
1223
2079
|
vars,
|
|
1224
2080
|
labels,
|
|
1225
2081
|
defaultBranch: fromDefault ? () => vars.defaultBranch() : undefined
|
|
@@ -1228,16 +2084,14 @@ async function runEngine(engine, argv, tagPrefix, cachePrefix, options = {}) {
|
|
|
1228
2084
|
summary = parsed.summary;
|
|
1229
2085
|
}
|
|
1230
2086
|
if (dryRun) {
|
|
1231
|
-
const run = engineCommand(engine, args);
|
|
1232
|
-
if (!run)
|
|
1233
|
-
return 1;
|
|
1234
2087
|
if (summary)
|
|
1235
2088
|
printBuildSummary(engine, args, summary);
|
|
1236
|
-
|
|
2089
|
+
if (!system.which(engine))
|
|
2090
|
+
logError(`ce: warning: ${engine} is not on PATH, so running this here would fail`);
|
|
2091
|
+
io.stdout(`${[engine, ...args].map(shellQuote).join(" ")}
|
|
1237
2092
|
`);
|
|
1238
2093
|
return 0;
|
|
1239
2094
|
}
|
|
1240
|
-
ensureHelperOnPath();
|
|
1241
2095
|
if (!system.which(engine)) {
|
|
1242
2096
|
logError(`Engine not found: ${engine}`);
|
|
1243
2097
|
return 1;
|
|
@@ -1246,8 +2100,7 @@ async function runEngine(engine, argv, tagPrefix, cachePrefix, options = {}) {
|
|
|
1246
2100
|
return 1;
|
|
1247
2101
|
if (summary)
|
|
1248
2102
|
printBuildSummary(engine, args, summary);
|
|
1249
|
-
|
|
1250
|
-
return run ? system.exec(run[0], run) : 1;
|
|
2103
|
+
return system.exec(engine, [engine, ...args]);
|
|
1251
2104
|
}
|
|
1252
2105
|
|
|
1253
2106
|
// src/login.ts
|
|
@@ -1266,13 +2119,21 @@ function oidcConfig() {
|
|
|
1266
2119
|
return null;
|
|
1267
2120
|
const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL ?? "";
|
|
1268
2121
|
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN ?? "";
|
|
1269
|
-
const missing = [
|
|
2122
|
+
const missing = [
|
|
2123
|
+
"ACTIONS_ID_TOKEN_REQUEST_URL",
|
|
2124
|
+
"ACTIONS_ID_TOKEN_REQUEST_TOKEN"
|
|
2125
|
+
].filter((name) => !process.env[name]);
|
|
1270
2126
|
if (missing.length) {
|
|
1271
2127
|
return `Forgejo OIDC login config is incomplete: AUD is set, but ${missing.map(unsetOrEmpty).join("; ")}`;
|
|
1272
2128
|
}
|
|
1273
2129
|
try {
|
|
1274
2130
|
assertAbsoluteHttpUrl(requestUrl, "must be an absolute HTTP(S) URL");
|
|
1275
|
-
return {
|
|
2131
|
+
return {
|
|
2132
|
+
audience,
|
|
2133
|
+
requestUrl,
|
|
2134
|
+
requestToken,
|
|
2135
|
+
authority: urlAuthority(requestUrl)
|
|
2136
|
+
};
|
|
1276
2137
|
} catch (exc) {
|
|
1277
2138
|
if (!(exc instanceof CeError))
|
|
1278
2139
|
throw exc;
|
|
@@ -1285,7 +2146,14 @@ function loginWithEngines(target, password) {
|
|
|
1285
2146
|
continue;
|
|
1286
2147
|
let result;
|
|
1287
2148
|
try {
|
|
1288
|
-
result = system.run([
|
|
2149
|
+
result = system.run([
|
|
2150
|
+
engine,
|
|
2151
|
+
"login",
|
|
2152
|
+
"-u",
|
|
2153
|
+
target.user,
|
|
2154
|
+
"--password-stdin",
|
|
2155
|
+
target.registry
|
|
2156
|
+
], `${password}
|
|
1289
2157
|
`);
|
|
1290
2158
|
} catch (exc) {
|
|
1291
2159
|
if (isErrno(exc, "ENOENT"))
|
|
@@ -1328,7 +2196,10 @@ async function login() {
|
|
|
1328
2196
|
try {
|
|
1329
2197
|
const authority = urlAuthority(rawRegistry);
|
|
1330
2198
|
const registry = registryServer(rawRegistry);
|
|
1331
|
-
registries.push({
|
|
2199
|
+
registries.push({
|
|
2200
|
+
registry,
|
|
2201
|
+
useOidc: oidc !== null && sameAuthority(authority, oidc.authority)
|
|
2202
|
+
});
|
|
1332
2203
|
} catch (exc) {
|
|
1333
2204
|
if (!(exc instanceof CeError))
|
|
1334
2205
|
throw exc;
|
|
@@ -1430,13 +2301,14 @@ Options:
|
|
|
1430
2301
|
run to builds. A --label with the same key wins.
|
|
1431
2302
|
Also available via OCI_LABELS=true.
|
|
1432
2303
|
--dry-run Print the build summary and the engine command, and
|
|
1433
|
-
exit without running it. Template errors still exit 2
|
|
2304
|
+
exit without running it. Template errors still exit 2;
|
|
2305
|
+
a missing engine is only a warning.
|
|
1434
2306
|
Also available via CE_DRY_RUN=true.
|
|
1435
2307
|
|
|
1436
2308
|
Placeholders, in build tags and in the tag and cache prefixes:
|
|
1437
2309
|
{name} The value of name; an error if it has none in this run.
|
|
1438
2310
|
{?name} Expands to nothing; leaves the tag out unless name has
|
|
1439
|
-
a value.
|
|
2311
|
+
a value. In a prefix, leaves the prefix empty instead.
|
|
1440
2312
|
{image} Registry host and repository, lowercased
|
|
1441
2313
|
{ref} Branch name, pr-<number>, or tag name
|
|
1442
2314
|
{branch} {pr} {tag}
|
|
@@ -1448,6 +2320,11 @@ Placeholders, in build tags and in the tag and cache prefixes:
|
|
|
1448
2320
|
From a SemVer tag such as v1.2.3 (v optional)
|
|
1449
2321
|
{stable} Set only for a SemVer tag without a pre-release
|
|
1450
2322
|
{env.NAME} The environment variable NAME
|
|
2323
|
+
{arg.NAME} The build arg NAME as the build gets it: from
|
|
2324
|
+
--build-arg, else the Containerfile's default for the
|
|
2325
|
+
stage built (--target's or the last)
|
|
2326
|
+
A guard that does not pass leaves its tag out or its prefix empty even if
|
|
2327
|
+
another guard there cannot be worked out.
|
|
1451
2328
|
Values are made safe for a tag ("ci/x" becomes "ci-x"). Quote templates
|
|
1452
2329
|
in single quotes. Example:
|
|
1453
2330
|
-t '{image}:{ref},{?default}latest'
|
|
@@ -1457,8 +2334,8 @@ Commands:
|
|
|
1457
2334
|
|
|
1458
2335
|
Engines:
|
|
1459
2336
|
docker [args] Passthrough to docker
|
|
1460
|
-
podman [args] Passthrough to podman
|
|
1461
|
-
buildah [args] Passthrough to buildah
|
|
2337
|
+
podman [args] Passthrough to podman
|
|
2338
|
+
buildah [args] Passthrough to buildah
|
|
1462
2339
|
|
|
1463
2340
|
Environment variables (used by login):
|
|
1464
2341
|
Each registry is a "group". The default group uses the bare variables
|