@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.
Files changed (3) hide show
  1. package/README.md +36 -18
  2. package/dist/ce.js +980 -103
  3. 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(["config", "context", "host", "log-level", "tlscacert", "tlscert", "tlskey", "builder"]),
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: [{ words: ["build"] }, { words: ["image", "build"] }, { words: ["buildx", "build"] }],
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 fs from "node:fs";
131
- import path from "node:path";
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(fs.readFileSync(file)));
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 = path.dirname(file);
149
- fs.mkdirSync(directory, { recursive: true, mode: 448 });
150
- const temporary = path.join(directory, `.ce-forgejo-${randomUUID().slice(0, 8)}`);
151
- const fd = fs.openSync(temporary, "wx", 384);
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
- fs.fchmodSync(fd, 384);
155
- fs.writeSync(fd, `${dumpJson(payload)}
893
+ fs2.fchmodSync(fd, 384);
894
+ fs2.writeSync(fd, `${dumpJson(payload)}
156
895
  `);
157
- fs.fsyncSync(fd);
896
+ fs2.fsyncSync(fd);
158
897
  } finally {
159
- fs.closeSync(fd);
898
+ fs2.closeSync(fd);
160
899
  }
161
- fs.renameSync(temporary, file);
900
+ fs2.renameSync(temporary, file);
162
901
  } finally {
163
- fs.rmSync(temporary, { force: true });
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 = [isObject(payload.pull_request) && payload.pull_request.number, payload.number].find(Number.isInteger);
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 fs2 from "node:fs";
406
- import path2 from "node:path";
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 fs2.writeSync(1, text),
410
- stderr: (text) => void fs2.writeSync(2, text),
411
- stdin: () => fs2.readFileSync(0, "utf8"),
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(path2.delimiter).filter(Boolean).map((dir) => path2.join(dir, command));
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
- fs2.accessSync(candidate, fs2.constants.X_OK);
430
- if (fs2.statSync(candidate).isFile())
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 fs2.realpathSync(path2.join(import.meta.dirname, "..", "bin"));
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(path2.delimiter);
1208
+ const parts = (process.env.PATH ?? "").split(path3.delimiter);
467
1209
  if (!parts.includes(dir))
468
- process.env.PATH = [dir, ...parts].join(path2.delimiter);
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 expand(template, vars, encode) {
498
- const parts = parse(template, vars);
1239
+ function guardsPass(parts, vars) {
1240
+ let unknown;
499
1241
  for (const part of parts) {
500
- if ("name" in part && part.guard && vars.get(part.name) === undefined)
501
- return null;
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
- if (parse(template, vars).some((part) => ("guard" in part) && part.guard)) {
519
- throw new CeError(`ce: ${what} ${JSON.stringify(template)} cannot use {?guards}; they only apply to tags`);
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
- for (const part of parse(template, vars)) {
525
- if ("name" in part && vars.get(part.name) === undefined) {
526
- throw new CeError(`ce: {${part.name}} has no value here, in the ${what} ${JSON.stringify(template)}`);
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
- function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, labels = {}, defaultBranch } = {}) {
742
- const args = [];
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
- const refs = rewriteTagValue(value, tagPrefix, vars);
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(arg.slice(2), "-t");
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
- args.push(arg, v);
1621
+ parts.push(arg, v);
802
1622
  summarize(flag, v);
803
1623
  } else {
804
- args.push(arg);
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
- args.push(arg);
809
- summary.dockerfile = arg.slice(2);
1645
+ parts.push(arg);
1646
+ summary.dockerfile = short();
1647
+ files.push(summary.dockerfile);
810
1648
  } else {
811
- args.push(arg);
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 prefix = expandPrefix(prefixTemplate, onDefaultBranch(vars, defaultBranch()), "cache prefix");
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 = ["[ce] build summary:", ` engine: ${engine}`, ` subcommand: ${subcommand}`];
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 = { Accept: "application/json", Authorization: `bearer ${requestToken}` };
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, { headers, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
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 fs4 from "node:fs";
1804
+ import fs5 from "node:fs";
941
1805
  import { homedir } from "node:os";
942
- import path3 from "node:path";
1806
+ import path4 from "node:path";
943
1807
 
944
1808
  // src/lock.ts
945
- import fs3 from "node:fs";
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 } = fs3.statSync(lockPath);
953
- return { ino, mtimeMs, owner: fs3.readFileSync(lockPath, "utf8").trim() };
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
- fs3.writeFileSync(guard, "", { flag: "wx", mode: 384 });
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
- fs3.rmSync(lockPath, { force: true });
1856
+ fs4.rmSync(lockPath, { force: true });
993
1857
  }
994
1858
  } finally {
995
- fs3.rmSync(guard, { force: true });
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 = fs3.openSync(lockPath, "wx", 384);
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
- fs3.writeSync(fd, `${owner}
1877
+ fs4.writeSync(fd, `${owner}
1014
1878
  `);
1015
- ino = fs3.fstatSync(fd).ino;
1879
+ ino = fs4.fstatSync(fd).ino;
1016
1880
  } finally {
1017
- fs3.closeSync(fd);
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
- fs3.rmSync(lockPath, { force: true });
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 path3.join(homedir(), ".docker");
1037
- const expanded = configured === "~" || configured.startsWith("~/") ? path3.join(homedir(), configured.slice(1)) : configured;
1038
- return path3.resolve(expanded);
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 path3.join(dockerConfigDir(), STATE_FILE);
1905
+ return path4.join(dockerConfigDir(), STATE_FILE);
1042
1906
  }
1043
1907
  async function withHelperState(fn) {
1044
1908
  const statePath = helperStatePath();
1045
- fs4.mkdirSync(path3.dirname(statePath), { recursive: true, mode: 448 });
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 = path3.join(dockerConfigDir(), "config.json");
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
- io.stdout(`${run.map(shellQuote).join(" ")}
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
- const run = engineCommand(engine, args);
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 = ["ACTIONS_ID_TOKEN_REQUEST_URL", "ACTIONS_ID_TOKEN_REQUEST_TOKEN"].filter((name) => !process.env[name]);
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 { audience, requestUrl, requestToken, authority: urlAuthority(requestUrl) };
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([engine, "login", "-u", target.user, "--password-stdin", target.registry], `${password}
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({ registry, useOidc: oidc !== null && sameAuthority(authority, oidc.authority) });
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. Not allowed in prefixes.
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 (via gosu if available)
1461
- buildah [args] Passthrough to buildah (via gosu if available)
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