@izykitten/ce 0.0.3-dev.3 → 0.0.3-dev.5

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