@izykitten/ce 0.0.3-dev.3 → 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 +24 -17
  2. package/dist/ce.js +898 -88
  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
 
@@ -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}`);
@@ -395,20 +1137,20 @@ function onDefaultBranch(vars, branch) {
395
1137
  if (UNKNOWN_ON_DEFAULT_BRANCH.has(name)) {
396
1138
  throw new CeError(`ce: the default branch's cache scope cannot use {${name}}: this run does not know its commit`);
397
1139
  }
398
- return name === "image" || name.startsWith("env.") || name.startsWith("arg.") ? vars.get(name) : undefined;
1140
+ return name === "image" || name.startsWith("env.") ? vars.get(name) : undefined;
399
1141
  }
400
1142
  };
401
1143
  }
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
@@ -758,7 +1500,7 @@ var SUMMARIZED = {
758
1500
  "--label": "labels"
759
1501
  };
760
1502
  var ARG_VARIABLE = /^arg\.([A-Za-z_][A-Za-z0-9_]*)$/u;
761
- function withBuildArgs(vars, values, argFile) {
1503
+ function withBuildArgs(vars, values, argFile, fromFile) {
762
1504
  return {
763
1505
  known: (name) => ARG_VARIABLE.test(name) || vars.known(name),
764
1506
  get(name) {
@@ -766,17 +1508,40 @@ function withBuildArgs(vars, values, argFile) {
766
1508
  if (arg === undefined)
767
1509
  return vars.get(name);
768
1510
  const value = values.get(arg);
769
- if (value === undefined && argFile) {
770
- throw new CeError(`ce: {${name}} needs --build-arg ${arg} on this command, as ce does not read --build-arg-file`);
771
- }
772
- if (value == null) {
773
- throw new CeError(`ce: {${name}} needs --build-arg ${arg}=<value> on this command, or --build-arg ${arg} with ${arg} set; ` + "ce does not read defaults from the Containerfile");
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`);
774
1515
  }
775
- return value || undefined;
1516
+ return fromFile(arg) || undefined;
776
1517
  }
777
1518
  };
778
1519
  }
779
- function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, labels = {}, defaultBranch, env = process.env, engine = "docker" } = {}) {
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);
780
1545
  const parts = [];
781
1546
  const rawCacheFrom = [];
782
1547
  const rawCacheTo = [];
@@ -794,6 +1559,10 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
794
1559
  let templated = false;
795
1560
  const buildArgs = new Map;
796
1561
  let argFile = false;
1562
+ const files = [];
1563
+ const positionals = [];
1564
+ const contexts = [];
1565
+ const envs = [];
797
1566
  let rest = [];
798
1567
  const addTags = (value, flag) => {
799
1568
  templated ||= hasPlaceholders(value);
@@ -808,7 +1577,11 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
808
1577
  buildArgs.set(value, env[value]);
809
1578
  else if (engine !== "docker")
810
1579
  buildArgs.set(value, null);
1580
+ if (engine !== "docker" && buildArgs.has("TARGETPLATFORM"))
1581
+ buildArgs.set("TARGETPLATFORM", null);
811
1582
  }
1583
+ if (field === "dockerfile")
1584
+ files.push(value);
812
1585
  if (field === "buildArgs" || field === "labels")
813
1586
  summary[field].push(value);
814
1587
  else
@@ -818,8 +1591,10 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
818
1591
  const arg = argv[i];
819
1592
  const [name, attached] = arg.startsWith("--") && arg.includes("=") ? splitAt(arg, "=") : [arg, undefined];
820
1593
  const next = () => consumeValue(argv.slice(++i), arg)[0];
1594
+ const short = () => arg.slice(arg[2] === "=" ? 3 : 2);
821
1595
  if (arg === "--") {
822
1596
  rest = argv.slice(i);
1597
+ positionals.push(...rest.slice(1));
823
1598
  if (rest.length > 1)
824
1599
  summary.context = rest.at(-1);
825
1600
  break;
@@ -828,7 +1603,7 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
828
1603
  else if (arg === "--tag")
829
1604
  addTags(next(), "--tag=");
830
1605
  else if (arg.startsWith("-t"))
831
- addTags(arg.slice(2), "-t");
1606
+ addTags(short(), "-t");
832
1607
  else if (name === "--tag")
833
1608
  addTags(attached, "--tag=");
834
1609
  else if (name === "--cache-from")
@@ -854,16 +1629,33 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
854
1629
  parts.push(arg);
855
1630
  if (attached === undefined)
856
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}`);
857
1644
  } else if (arg.startsWith("-f") && arg.length > 2) {
858
1645
  parts.push(arg);
859
- summary.dockerfile = arg.slice(2);
1646
+ summary.dockerfile = short();
1647
+ files.push(summary.dockerfile);
860
1648
  } else {
861
1649
  parts.push(arg);
862
- if (!arg.startsWith("-"))
1650
+ if (!arg.startsWith("-")) {
1651
+ positionals.push(arg);
863
1652
  summary.context = arg;
1653
+ }
864
1654
  }
865
1655
  }
866
- vars = withBuildArgs(vars, buildArgs, argFile);
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);
867
1659
  const args = parts.flatMap((part) => {
868
1660
  if (typeof part === "string")
869
1661
  return [part];
@@ -885,7 +1677,11 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
885
1677
  const cacheFrom = rawCacheFrom.map((c) => toNames.has(cacheScopeName(c)) ? normalizeCacheScope(c, cachePrefix) : c);
886
1678
  const ghaTo = rawCacheTo.filter(isGhaCache);
887
1679
  if (defaultBranch && ghaTo.length) {
888
- 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");
889
1685
  cacheFrom.push(...ghaTo.map((c) => normalizeCacheScope(withoutCacheMode(c), prefix)));
890
1686
  }
891
1687
  for (const rewritten of new Set(cacheFrom)) {
@@ -909,7 +1705,11 @@ function printBuildSummary(engine, args, summary) {
909
1705
  if (!command || command.tagsOnly)
910
1706
  return;
911
1707
  const subcommand = command.name;
912
- const out = ["[ce] build summary:", ` engine: ${engine}`, ` subcommand: ${subcommand}`];
1708
+ const out = [
1709
+ "[ce] build summary:",
1710
+ ` engine: ${engine}`,
1711
+ ` subcommand: ${subcommand}`
1712
+ ];
913
1713
  const list = (title, items) => {
914
1714
  if (items.length)
915
1715
  out.push(` ${title}:`, ...items.map((item) => ` - ${item}`));
@@ -947,14 +1747,20 @@ async function forgejoOidcJwt(audience, requestUrl, requestToken) {
947
1747
  throw new Error("credentials in URL");
948
1748
  url.searchParams.set("audience", audience);
949
1749
  tokenUrl = url.href;
950
- headers = { Accept: "application/json", Authorization: `bearer ${requestToken}` };
1750
+ headers = {
1751
+ Accept: "application/json",
1752
+ Authorization: `bearer ${requestToken}`
1753
+ };
951
1754
  new Headers(headers);
952
1755
  } catch {
953
1756
  throw new CeError("token request URL or headers are invalid");
954
1757
  }
955
1758
  let body;
956
1759
  try {
957
- 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
+ });
958
1764
  if (!response.ok)
959
1765
  throw new Error(`HTTP ${response.status} ${response.statusText}`);
960
1766
  body = await response.arrayBuffer();
@@ -995,20 +1801,20 @@ function needsRefresh(expiry) {
995
1801
  }
996
1802
 
997
1803
  // src/state.ts
998
- import fs4 from "node:fs";
1804
+ import fs5 from "node:fs";
999
1805
  import { homedir } from "node:os";
1000
- import path3 from "node:path";
1806
+ import path4 from "node:path";
1001
1807
 
1002
1808
  // src/lock.ts
1003
- import fs3 from "node:fs";
1809
+ import fs4 from "node:fs";
1004
1810
  import { hostname } from "node:os";
1005
1811
  import { setTimeout as setTimeout2 } from "node:timers/promises";
1006
1812
  var STALE_AFTER_MS = 120000;
1007
1813
  var POLL_MS = 50;
1008
1814
  function snapshot(lockPath) {
1009
1815
  try {
1010
- const { ino, mtimeMs } = fs3.statSync(lockPath);
1011
- 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() };
1012
1818
  } catch (exc) {
1013
1819
  if (isErrno(exc, "ENOENT"))
1014
1820
  return null;
@@ -1034,7 +1840,7 @@ function breakIfStale(lockPath) {
1034
1840
  return;
1035
1841
  const guard = `${lockPath}.break`;
1036
1842
  try {
1037
- fs3.writeFileSync(guard, "", { flag: "wx", mode: 384 });
1843
+ fs4.writeFileSync(guard, "", { flag: "wx", mode: 384 });
1038
1844
  } catch (exc) {
1039
1845
  if (!isErrno(exc, "EEXIST"))
1040
1846
  throw exc;
@@ -1047,10 +1853,10 @@ function breakIfStale(lockPath) {
1047
1853
  try {
1048
1854
  const current = snapshot(lockPath);
1049
1855
  if (current && current.ino === stale.ino && current.mtimeMs === stale.mtimeMs && current.owner === stale.owner) {
1050
- fs3.rmSync(lockPath, { force: true });
1856
+ fs4.rmSync(lockPath, { force: true });
1051
1857
  }
1052
1858
  } finally {
1053
- fs3.rmSync(guard, { force: true });
1859
+ fs4.rmSync(guard, { force: true });
1054
1860
  }
1055
1861
  }
1056
1862
  async function withFileLock(lockPath, fn) {
@@ -1059,7 +1865,7 @@ async function withFileLock(lockPath, fn) {
1059
1865
  for (;; ) {
1060
1866
  let fd;
1061
1867
  try {
1062
- fd = fs3.openSync(lockPath, "wx", 384);
1868
+ fd = fs4.openSync(lockPath, "wx", 384);
1063
1869
  } catch (exc) {
1064
1870
  if (!isErrno(exc, "EEXIST"))
1065
1871
  throw exc;
@@ -1068,11 +1874,11 @@ async function withFileLock(lockPath, fn) {
1068
1874
  continue;
1069
1875
  }
1070
1876
  try {
1071
- fs3.writeSync(fd, `${owner}
1877
+ fs4.writeSync(fd, `${owner}
1072
1878
  `);
1073
- ino = fs3.fstatSync(fd).ino;
1879
+ ino = fs4.fstatSync(fd).ino;
1074
1880
  } finally {
1075
- fs3.closeSync(fd);
1881
+ fs4.closeSync(fd);
1076
1882
  }
1077
1883
  break;
1078
1884
  }
@@ -1081,7 +1887,7 @@ async function withFileLock(lockPath, fn) {
1081
1887
  } finally {
1082
1888
  const current = snapshot(lockPath);
1083
1889
  if (current?.ino === ino && current.owner === owner)
1084
- fs3.rmSync(lockPath, { force: true });
1890
+ fs4.rmSync(lockPath, { force: true });
1085
1891
  }
1086
1892
  }
1087
1893
 
@@ -1091,16 +1897,16 @@ var STATE_FILE = "ce-forgejo-credentials.json";
1091
1897
  function dockerConfigDir() {
1092
1898
  const configured = process.env.DOCKER_CONFIG;
1093
1899
  if (!configured)
1094
- return path3.join(homedir(), ".docker");
1095
- const expanded = configured === "~" || configured.startsWith("~/") ? path3.join(homedir(), configured.slice(1)) : configured;
1096
- 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);
1097
1903
  }
1098
1904
  function helperStatePath() {
1099
- return path3.join(dockerConfigDir(), STATE_FILE);
1905
+ return path4.join(dockerConfigDir(), STATE_FILE);
1100
1906
  }
1101
1907
  async function withHelperState(fn) {
1102
1908
  const statePath = helperStatePath();
1103
- fs4.mkdirSync(path3.dirname(statePath), { recursive: true, mode: 448 });
1909
+ fs5.mkdirSync(path4.dirname(statePath), { recursive: true, mode: 448 });
1104
1910
  return withFileLock(`${statePath}.pidlock`, async () => {
1105
1911
  const state = readJsonFile(statePath, { version: 1, registries: {} });
1106
1912
  if (!isObject(state) || !isObject(state.registries)) {
@@ -1129,7 +1935,7 @@ async function saveHelperState(registry, username, audience, authority, jwt) {
1129
1935
  }
1130
1936
  async function configureDockerCredentialHelper(registry, username, audience, authority, jwt) {
1131
1937
  await saveHelperState(registry, username, audience, authority, jwt);
1132
- const configPath = path3.join(dockerConfigDir(), "config.json");
1938
+ const configPath = path4.join(dockerConfigDir(), "config.json");
1133
1939
  const config = readJsonFile(configPath, {});
1134
1940
  if (!isObject(config))
1135
1941
  throw new CeError("Docker config is not a JSON object");
@@ -1258,16 +2064,6 @@ function envFlag(name) {
1258
2064
  function shellQuote(arg) {
1259
2065
  return /^[A-Za-z0-9_@%+:,./-][A-Za-z0-9_@%+=:,./-]*$/u.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
1260
2066
  }
1261
- function engineCommand(engine, args) {
1262
- if (engine === "docker" || !system.which("gosu"))
1263
- return [engine, ...args];
1264
- const user = process.env.USER ?? "";
1265
- if (!user) {
1266
- logError("ce: gosu is available but USER is not set; cannot drop privileges. Set USER or remove gosu from PATH.");
1267
- return null;
1268
- }
1269
- return ["gosu", user, engine, ...args];
1270
- }
1271
2067
  async function runEngine(engine, argv, tagPrefix, cachePrefix, options = {}) {
1272
2068
  const dryRun = options.dryRun ?? envFlag("CE_DRY_RUN");
1273
2069
  ensureHelperOnPath();
@@ -1288,14 +2084,11 @@ async function runEngine(engine, argv, tagPrefix, cachePrefix, options = {}) {
1288
2084
  summary = parsed.summary;
1289
2085
  }
1290
2086
  if (dryRun) {
1291
- const run = engineCommand(engine, args);
1292
- if (!run)
1293
- return 1;
1294
2087
  if (summary)
1295
2088
  printBuildSummary(engine, args, summary);
1296
2089
  if (!system.which(engine))
1297
2090
  logError(`ce: warning: ${engine} is not on PATH, so running this here would fail`);
1298
- io.stdout(`${run.map(shellQuote).join(" ")}
2091
+ io.stdout(`${[engine, ...args].map(shellQuote).join(" ")}
1299
2092
  `);
1300
2093
  return 0;
1301
2094
  }
@@ -1307,8 +2100,7 @@ async function runEngine(engine, argv, tagPrefix, cachePrefix, options = {}) {
1307
2100
  return 1;
1308
2101
  if (summary)
1309
2102
  printBuildSummary(engine, args, summary);
1310
- const run = engineCommand(engine, args);
1311
- return run ? system.exec(run[0], run) : 1;
2103
+ return system.exec(engine, [engine, ...args]);
1312
2104
  }
1313
2105
 
1314
2106
  // src/login.ts
@@ -1327,13 +2119,21 @@ function oidcConfig() {
1327
2119
  return null;
1328
2120
  const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL ?? "";
1329
2121
  const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN ?? "";
1330
- 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]);
1331
2126
  if (missing.length) {
1332
2127
  return `Forgejo OIDC login config is incomplete: AUD is set, but ${missing.map(unsetOrEmpty).join("; ")}`;
1333
2128
  }
1334
2129
  try {
1335
2130
  assertAbsoluteHttpUrl(requestUrl, "must be an absolute HTTP(S) URL");
1336
- return { audience, requestUrl, requestToken, authority: urlAuthority(requestUrl) };
2131
+ return {
2132
+ audience,
2133
+ requestUrl,
2134
+ requestToken,
2135
+ authority: urlAuthority(requestUrl)
2136
+ };
1337
2137
  } catch (exc) {
1338
2138
  if (!(exc instanceof CeError))
1339
2139
  throw exc;
@@ -1346,7 +2146,14 @@ function loginWithEngines(target, password) {
1346
2146
  continue;
1347
2147
  let result;
1348
2148
  try {
1349
- 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}
1350
2157
  `);
1351
2158
  } catch (exc) {
1352
2159
  if (isErrno(exc, "ENOENT"))
@@ -1389,7 +2196,10 @@ async function login() {
1389
2196
  try {
1390
2197
  const authority = urlAuthority(rawRegistry);
1391
2198
  const registry = registryServer(rawRegistry);
1392
- registries.push({ registry, useOidc: oidc !== null && sameAuthority(authority, oidc.authority) });
2199
+ registries.push({
2200
+ registry,
2201
+ useOidc: oidc !== null && sameAuthority(authority, oidc.authority)
2202
+ });
1393
2203
  } catch (exc) {
1394
2204
  if (!(exc instanceof CeError))
1395
2205
  throw exc;
@@ -1510,9 +2320,9 @@ Placeholders, in build tags and in the tag and cache prefixes:
1510
2320
  From a SemVer tag such as v1.2.3 (v optional)
1511
2321
  {stable} Set only for a SemVer tag without a pre-release
1512
2322
  {env.NAME} The environment variable NAME
1513
- {arg.NAME} The build arg NAME from this command's --build-arg;
1514
- an error without one, even guarded, as ce does not
1515
- read Containerfiles
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)
1516
2326
  A guard that does not pass leaves its tag out or its prefix empty even if
1517
2327
  another guard there cannot be worked out.
1518
2328
  Values are made safe for a tag ("ci/x" becomes "ci-x"). Quote templates
@@ -1524,8 +2334,8 @@ Commands:
1524
2334
 
1525
2335
  Engines:
1526
2336
  docker [args] Passthrough to docker
1527
- podman [args] Passthrough to podman (via gosu if available)
1528
- buildah [args] Passthrough to buildah (via gosu if available)
2337
+ podman [args] Passthrough to podman
2338
+ buildah [args] Passthrough to buildah
1529
2339
 
1530
2340
  Environment variables (used by login):
1531
2341
  Each registry is a "group". The default group uses the bare variables