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

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 +71 -14
  2. package/dist/ce.js +487 -112
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -24,9 +24,13 @@ In a Forgejo workflow:
24
24
  REGISTRY_HOST: ${{ forgejo.server_url }}
25
25
  run: bunx @izykitten/ce login
26
26
  - name: Container Build & Push
27
- run: bunx @izykitten/ce docker buildx build --push -t '{image}:{ref},{?default}latest' .
27
+ run: >
28
+ bunx @izykitten/ce docker buildx build --push
29
+ -t '{image}:{ref},{?version}{version},{?stable}{major}.{minor},{?stable}{major},{?stable}latest' .
28
30
  ```
29
31
 
32
+ [Recommended tags](#recommended-tags) says which tags that gives, and how to pick what `latest` follows.
33
+
30
34
  Only build commands are rewritten: `build`, `buildx build` and `image build` for Docker and Podman, `build` and `bud` for Buildah, and the tags of `docker buildx imagetools create`. Everything else, such as `docker run -t`, reaches the engine unchanged.
31
35
 
32
36
  ## Tags from the Forgejo run
@@ -53,9 +57,9 @@ Prefixes can use guards too. A guard that does not pass leaves the prefix empty,
53
57
 
54
58
  Guards decide from the run's metadata, so they need it too: outside a Forgejo Actions run, `{?default}` fails like `{default}` would. Only `{env.NAME}` and `{arg.NAME}` work everywhere. A guard that does not pass decides on its own, though: a tag with one is left out, and a prefix with one is empty, even if another guard in it could not be worked out, so `{?default}{?env.X}` outside a run with `X` empty is simply left out. To tell whether a branch is the default one, ce reads `repository.default_branch` from the event file, or `DEFAULT_BRANCH` when the event has none, as on a scheduled run; set `DEFAULT_BRANCH: main` in such a workflow. Pull requests and tags are never the default branch, so they need neither.
55
59
 
56
- Put templates in single quotes: `?` is a glob character in zsh.
60
+ Quote a template in a workflow's `env:`: YAML reads `CACHE_PREFIX: {ref}` as a mapping, and `{?pr}pr-{pr}` not at all.
57
61
 
58
- A branch, PR and default-branch build:
62
+ A branch, PR and default-branch build, for an image deployed from its default branch with no releases yet, so `latest` follows that branch:
59
63
 
60
64
  ```yaml
61
65
  - name: Container Build & Push
@@ -73,13 +77,41 @@ The same can be written by hand, since ce only prefixes a `--cache-from` whose s
73
77
 
74
78
  The `--cache-from` that `--cache` makes leaves out a gha `mode=`, which only applies to export, and a `--cache-from` given twice, as the default branch's own scope is on the default branch, is passed once.
75
79
 
76
- A release on a SemVer tag, with the version aliases only for stable releases:
80
+ ### Recommended tags
81
+
82
+ For an image others pull by version:
77
83
 
78
84
  ```sh
79
- -t '{image}:{ref},{?version}{version},{?stable}{major}.{minor},{?stable}{major},sha-{sha}'
85
+ -t '{image}:{ref},{?version}{version},{?stable}{major}.{minor},{?stable}{major},{?stable}latest'
80
86
  ```
81
87
 
82
- `{stable}` does not mean newest: rebuilding an older release moves its aliases back to it.
88
+ | Build | Tags |
89
+ | ----------------- | --------------------------------------- |
90
+ | branch `main` | `main` |
91
+ | branch `ci/fix` | `ci-fix` |
92
+ | pull request 3 | `pr-3` |
93
+ | tag `v1.2.3` | `v1.2.3`, `1.2.3`, `1.2`, `1`, `latest` |
94
+ | tag `v1.3.0-rc.1` | `v1.3.0-rc.1`, `1.3.0-rc.1` |
95
+ | tag `v0.3.1` | `v0.3.1`, `0.3.1`, `0.3`, `0`, `latest` |
96
+
97
+ - `{ref}` names every build after what was pushed, so a branch or pull request can be pulled to try it, and a release keeps its git tag's name, `v` included.
98
+ - `{?version}{version}` adds the version without the `v`, as most images are tagged, for pre-releases too.
99
+ - `1.2` and `1` follow the newest stable release in their line, for those who want fixes but not the next major version. Before 1.0, SemVer lets any minor release break things, so `0` can move from 0.3 to a 0.4 that does; pin `0.3` to avoid that.
100
+ - `latest` is the newest stable release, as on most images. A repository with no release tag yet has no `latest`: deploy `main` instead, or use `{?default}latest` until the first release.
101
+
102
+ The guard on `latest` picks what it follows:
103
+
104
+ | Tag | `latest` follows |
105
+ | --------------------- | ----------------------------------------- |
106
+ | `{?stable}latest` | stable releases |
107
+ | `{?version}latest` | every SemVer tag, pre-releases included |
108
+ | `{?tag}latest` | every tag |
109
+ | `{?default}latest` | the default branch |
110
+ | `{?env.LATEST}latest` | builds where an earlier step set `LATEST` |
111
+
112
+ Guards go by the build, not by what is in the registry: `latest` and the version aliases move to the last build whose guards pass. A `v1.2.4` fix pushed after `v2.0.0`, or a rebuild of an older release, moves them back to it. For a rule such as "only the highest version" or "the default branch until the first release", have an earlier step check the registry and set the variable an `{?env.NAME}` guard reads.
113
+
114
+ `{ref}` moves with each push. To also keep every branch and pull request build under a name of its own, add `{?branch}{ref}-{sha},{?pr}{ref}-{sha}`, which gives `main-0123456` and `pr-3-0123456`.
83
115
 
84
116
  ### Build args and the Containerfile
85
117
 
@@ -109,25 +141,25 @@ RUN xcaddy build v${CADDY_VERSION}
109
141
  - **The file:** the `-f` file (with Docker, the last one), else the context's `Dockerfile` (or `dockerfile`); podman and buildah look for `Containerfile` first.
110
142
  - **The stage:** the one being built, `--target`'s or else the last. Other stages do not matter, unless this one is built `FROM` one of them, which it then starts from.
111
143
  - **The default:** the stage's own `ARG NAME=…`, the last one winning, or that of the stage it is built from; else the global `ARG NAME=…` before the first `FROM`, which a bare `ARG NAME` in the stage, or a `FROM` line, also gets. No default anywhere, or an empty one, is no value, and a guard leaves its tag out.
112
- - **`$` in a default:** worked out as the engine does, from the `ARG`s, `--build-arg`s and `ENV`s before it, with `$NAME`, `${NAME}`, `${NAME:-x}` and `${NAME:+x}` (and with Docker, `${NAME-x}` and `${NAME+x}`). With Docker, in a stage from `scratch`, a name nothing sets is empty, but for `PATH`.
113
- - **Builtins** such as `TARGETPLATFORM`, and `SOURCE_DATE_EPOCH` and proxies such as `HTTP_PROXY`, which the engine may set itself, are the builder's, unless a `--build-arg` sets them. Docker also takes a builtin's own `ARG` default, and works out a `SOURCE_DATE_EPOCH` that is not a plain number such as `1700000000` itself.
114
- - **podman and buildah** differ inside a stage: an `ENV`, the base image's too, wins over an `ARG`, so ce only works out a reference to a name an `ENV` there sets. A stage gets the `ARG`s of the stage its `FROM` names as written, so none through `FROM $BASE`. `TARGETPLATFORM` is always the platform they build.
144
+ - **`$` in a default:** worked out as the engine does, from the `ARG`s, `--build-arg`s and `ENV`s before it, with `$NAME`, `${NAME}`, `${NAME:-x}` and `${NAME:+x}` (and with Docker, `${NAME-x}` and `${NAME+x}`). In a stage from `scratch`, a name nothing sets is empty, and `PATH` is the engines' `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`, which with podman and buildah is an `ENV`, so it wins over an `ARG PATH`.
145
+ - **Builtins** such as `TARGETPLATFORM`, and `SOURCE_DATE_EPOCH` and proxies such as `HTTP_PROXY`, which the engine may set itself, are the builder's, unless a `--build-arg` sets them. podman and buildah pass their environment's proxies only to `RUN`, so with them a proxy is like any other name. With a `--platform` such as `linux/arm/v7`, the `TARGET` ones are that platform's, as the engine normalizes it: `linux/arm` is `linux/arm/v7`, `linux/aarch64` is `linux/arm64`, a bare architecture such as `arm64` is for Linux when ce runs on Linux, and with Docker `linux/arm64/v8` is `linux/arm64`. With several, in one `--platform` such as `linux/amd64,linux/arm64` or in several, each is built on its own and every one gets the same tags, so ce works the value out for each: one they share is used, and one that differs is refused, naming two platforms it differs between. podman and buildah set them as ce works them out from podman 5.2 and buildah 1.37, so ce asks the engine its version (`podman info`, which with `--remote` is the server's); older ones let a `--build-arg TARGETPLATFORM` win, and podman 3.4 and buildah 1.23 ignore `--platform`. Docker also takes a builtin's own `ARG` default, and works out a `SOURCE_DATE_EPOCH` that is not a plain number such as `1700000000` itself.
146
+ - **podman and buildah** differ inside a stage: an `ENV`, the base image's too, wins over an `ARG`, so on a base image ce only works out a reference to a name an `ENV` there sets; from `scratch`, the stage's `ARG`s count too. `--env` sets an `ENV` at the start of every stage (`--env NAME` and `--env NAME*` from the environment), and `--unsetenv` drops the base image's. `--from` replaces the first `FROM`'s image, so that stage is on a base image even when its `FROM` is `scratch`. A stage gets the `ARG`s of the stage its `FROM` names as written, so none through `FROM $BASE`. `TARGETPLATFORM` is always the platform they build, and before the first `FROM` the builtins are not the `--platform`'s, but the machine's, or empty before podman 5.7 and buildah 1.42.
115
147
 
116
148
  **What ce refuses**, with exit 2 and the reason:
117
149
 
118
150
  - **The file is not there to read:** `-f -`, a `-f` URL or a remote context; with podman or buildah, several `-f` files or a `.in` one for `cpp`.
119
151
  - **Another frontend:** a `# syntax=` directive or `BUILDKIT_SYNTAX` naming anything but `docker/dockerfile` untagged, `:1`, or `:1.21` and later, which may be pinned by a digest.
120
- - **A value ce cannot know:** a reference to a builtin, or to a name the base image's `ENV` may set; a form like `${NAME#x}`; with podman or buildah, `--env` or `--unsetenv` changing a name it refers to.
152
+ - **A value ce cannot know:** a reference to a name the base image's `ENV` may set; a builtin of the machine that builds, such as `BUILDARCH`; a `TARGET` one without a `--platform`, with one ce does not read (such as `linux` alone, or an empty one in a list, which podman and buildah build for the machine), with one that differs between the platforms, with podman before 5.2 or buildah before 1.37, or with podman's `--arch`, `--os`, `--variant` or `--all-platforms`; a form like `${NAME#x}`; with podman or buildah, a `$` in a `--env` value, which they expand, or a character they escape there, such as a line end or a tab, and quotes in a `--unsetenv`.
121
153
  - **Instructions ce does not read:** an `ARG` or `ENV` name with something to expand, such as `ENV $NAME=x`, or with flags.
122
154
  - **Characters other than ASCII's:** letters in an instruction, a stage name or a name after a `$`; space in an `ARG`, `ENV` or `FROM` line or a heredoc's line. Also a line ending in more than one carriage return.
123
155
  - **Heredocs ce cannot place:** a delimiter that is not a word, which may be quoted; on its line, a `${}` with space or quotes in its word outside double quotes, or with quotes, `$` or `<` in it anywhere.
124
- - **Stages that change under it:** a `--build-context` replacing a stage the built one comes from (with podman and buildah, what a `FROM` names); with podman or buildah, a stage built from one with an `ONBUILD ARG` or `ONBUILD ENV`, which they run only for some image formats.
156
+ - **Stages that change under it:** a `--build-context` replacing a stage the built one comes from (with podman and buildah, what a `FROM` names); with podman or buildah, a stage built from one with an `ONBUILD ARG` or `ONBUILD ENV`, which they run only with the `docker` image format, unless `--format` or `BUILDAH_FORMAT` says which (`containers.conf` may change the default).
125
157
 
126
158
  Two places always need a `--build-arg NAME`: a `{arg.NAME}` in the cache prefix with `CACHE_FROM_DEFAULT`, as the default branch may have another Containerfile; and with podman's `--build-arg-file`, which ce does not read. ce does not see a base image's own `ONBUILD` lines, and does not model Docker's legacy builder (`DOCKER_BUILDKIT=0`). Every rule here has a test in `test/containerfile.test.ts`.
127
159
 
128
160
  ### Checking a template
129
161
 
130
- `--dry-run` before the engine, or `CE_DRY_RUN=true`, prints the build summary and the engine command ce would run, and exits 0 without running it or waiting for the engine. A template that fails still exits 2, so it also works as a check before pushing a workflow. It checks the templates, not the engine: if the engine is not on `PATH`, it warns and still exits 0, so it works on a machine without one:
162
+ `--dry-run` before the engine, or `CE_DRY_RUN=true`, prints the build summary and the engine command ce would run, and exits 0 without running it or waiting for the engine. A template that fails still exits 2, so it also works as a check before pushing a workflow. It checks the templates, not the engine: if the engine is not on `PATH`, it warns and still exits 0, so it works on a machine without one. The one exception is a `TARGET` arg from `--platform` with podman or buildah, which needs the engine's version: ce asks it, waiting for podman as a build does, and without an answer refuses the value, so the dry run exits 2:
131
163
 
132
164
  ```sh
133
165
  GITHUB_REF=refs/tags/v1.2.3 GITHUB_SERVER_URL=https://code.example GITHUB_REPOSITORY=org/app \
@@ -136,7 +168,30 @@ GITHUB_REF=refs/tags/v1.2.3 GITHUB_SERVER_URL=https://code.example GITHUB_REPOSI
136
168
 
137
169
  ### OCI labels
138
170
 
139
- With `OCI_LABELS=true`, or `--oci-labels` before the engine (`ce --oci-labels docker buildx build …`, like the prefix options), builds get `org.opencontainers.image.source`, `revision`, `version`, `title` and `description` labels from the run. Labels the run cannot tell are left out. They override a Dockerfile `LABEL` with the same key, and a `--label` you pass wins over them. `created` is not set, so the labels do not change the image on every rebuild.
171
+ Builds in a run get these `org.opencontainers.image.` labels, and the same keys as annotations:
172
+
173
+ | Key | Value |
174
+ | ------------- | -------------------------------------------------------------------------------------- |
175
+ | `source` | the repository's page, `{server}/{owner}/{repo}` |
176
+ | `url` | the repository's website from the event, or else its page |
177
+ | `revision` | the commit, `GITHUB_SHA` |
178
+ | `version` | the tag's version, such as `1.2.3`, or else the ref's name (`pr-N` for a pull request) |
179
+ | `title` | the repository's name |
180
+ | `description` | the repository's description from the event |
181
+ | `created` | when ce started the build, to the second, or the time `SOURCE_DATE_EPOCH` gives |
182
+
183
+ Keys the run cannot tell are left out, and outside a run there are none, `created` included. `OCI_LABELS=false`, or `--no-oci-labels` before the engine (`ce --no-oci-labels docker buildx build …`, like the prefix options), turns them off. The labels override a Dockerfile `LABEL` with the same key, and a `--label` you pass for a key wins over ce's label for it, an `--annotation` over its annotation. Labels and annotations are applied after the `RUN` steps, so `created` does not change their cache, but each build's image differs. For one that does not, set the epoch: `created` follows it as the engine does. With Docker, that is a `--build-arg SOURCE_DATE_EPOCH`, or else the environment's `SOURCE_DATE_EPOCH`, or else the default of a global `ARG SOURCE_DATE_EPOCH` before the first `FROM`, which ce reads from the Dockerfile. With podman and buildah, it is `--timestamp`, which they refuse beside any of the others, or else `--source-date-epoch`, or else the environment's `SOURCE_DATE_EPOCH`, or else the build arg. Each is read as the engine reads it: a whole number of seconds, which may be negative, and for `--timestamp` also in Go's other bases, such as `0x10` or `010` (8). An epoch ce cannot read, such as `1.5` or a `SOURCE_DATE_EPOCH` the environment gives that is not a number, leaves `created` out, as do a podman or buildah `--build-arg-file` with none of the others, as the file may set it, and with Docker a global default with a `$` or a Dockerfile ce cannot read, such as a remote context's.
184
+
185
+ podman and buildah expand a `$NAME` in a `--label`'s value from the build args, and make one that is not a build arg empty, with no way to escape the `$`. They also write a label's value as Go's `%q` does, so a line end becomes `\n`, as do a tab and other characters Go escapes. So with them, a label whose value has a `$`, such as a description that mentions `$HOME`, or a character they escape, is left out; its annotation, which they keep as it is, stays.
186
+
187
+ With Docker, a build for two or more platforms gets the annotations on the image index too (`manifest,index:`). BuildKit refuses index annotations for one platform, so a build for one gets them on its manifest only. `DOCKER_BUILDKIT=0` gets the labels only, as the legacy builder it picks has no `--annotation`; `docker buildx build` ignores it and always uses BuildKit. podman and buildah put them on the image's manifest, not on a `--manifest` list. With the `docker` image format they drop them, as that format has no annotations.
188
+
189
+ ### SBOM
190
+
191
+ `SBOM` adds a software bill of materials to builds. `true` (or `spdx`) gives SPDX on every engine, and `cyclonedx` gives CycloneDX with podman and buildah. It is off when unset, empty or `false`; it also takes `yes`, `no`, `1` and `0`, in any case, and any other value exits 2.
192
+
193
+ - **Docker:** `--sbom=true`, BuildKit's SBOM attestation, unless you pass `--sbom` or an `--attest type=sbom`. [It is always SPDX](https://docs.docker.com/build/metadata/attestations/sbom/), so `SBOM=cyclonedx` exits 2 before the build, unless your own flag means ce adds none. With `--load`, Docker's classic image store refuses the attestation and says what to switch to: another driver, or the containerd image store.
194
+ - **podman and buildah:** `--sbom=syft-spdx`, or `--sbom=syft-cyclonedx` for `SBOM=cyclonedx`, unless you pass a `--sbom` preset. (buildah's plain `syft` preset is CycloneDX, so ce names the format.) The SBOM is written into the image as `/root/buildinfo/content_manifests/sbom.spdx.json`, or `sbom.cdx.json` for a CycloneDX preset. A `--sbom` you pass without an `--sbom-output`, `--sbom-image-output`, `--sbom-purl-output` or `--sbom-image-purl-output` is written there too, even without `SBOM`. The scanner runs from an image, which the engine pulls if it is missing.
140
195
 
141
196
  ## The credential helper
142
197
 
@@ -156,7 +211,7 @@ These behave the same under Node and Bun, and [test/contract.test.ts](test/contr
156
211
 
157
212
  The wording of messages is not part of that and may change.
158
213
 
159
- In 0.0.3, the `--cache-from` that `--cache` makes no longer has a gha `mode=`, which only applies to export, and a `--cache-from` given twice is passed once. podman and buildah are no longer run through `gosu` as `$USER` when it is on `PATH`: ce runs every engine directly, as the user ce runs as, so to drop root, run ce itself as another user, e.g. with a `USER` line in the image.
214
+ In 0.0.3, builds in a run get the OCI labels, and annotations, by default; `OCI_LABELS=false` or `--no-oci-labels` turns them off. The `--cache-from` that `--cache` makes no longer has a gha `mode=`, which only applies to export, and a `--cache-from` given twice is passed once. podman and buildah are no longer run through `gosu` as `$USER` when it is on `PATH`: ce runs every engine directly, as the user ce runs as, so to drop root, run ce itself as another user, e.g. with a `USER` line in the image.
160
215
 
161
216
  Compared with the Python version:
162
217
 
@@ -175,3 +230,5 @@ bun test # builds dist/, then unit tests plus the contract and process
175
230
  bun check
176
231
  bun run build # dist/ce.js, which the bin scripts load; npm pack runs it
177
232
  ```
233
+
234
+ `bun run check-engines` builds each case in [scripts/engine-cases.ts](scripts/engine-cases.ts) with real Docker, podman and buildah, and compares the build arg each build gets with the one ce works out. It fails if ce would tag with a wrong value. podman and buildah run in a privileged container each, from `quay.io/podman/stable` and `quay.io/buildah/stable` unless `PODMAN_IMAGE` or `BUILDAH_IMAGE` names another, such as an older version's; `bun run check-engines docker` checks Docker alone. Run it after changing how ce reads a Containerfile.
package/dist/ce.js CHANGED
@@ -104,20 +104,31 @@ var COMMANDS = {
104
104
  };
105
105
  function findBuildCommand(engine, argv) {
106
106
  const words = [];
107
+ const options = [];
108
+ let global = 0;
107
109
  for (let i = 0;i < argv.length; i++) {
108
110
  const arg = argv[i];
109
111
  if (arg === "--")
110
112
  return null;
111
113
  if (arg.startsWith("-")) {
112
- if (takesNext(engine, arg))
113
- i++;
114
+ options.push(arg);
115
+ if (takesNext(engine, arg) && i + 1 < argv.length)
116
+ options.push(argv[++i]);
114
117
  continue;
115
118
  }
119
+ if (!words.length)
120
+ global = options.length;
116
121
  words.push(arg);
117
122
  const candidates = COMMANDS[engine].filter((command) => words.every((word, j) => command.words[j] === word));
118
123
  const match = candidates.find((command) => command.words.length === words.length);
119
124
  if (match)
120
- return { start: i + 1, name: arg, tagsOnly: match.tagsOnly ?? false };
125
+ return {
126
+ start: i + 1,
127
+ name: arg,
128
+ tagsOnly: match.tagsOnly ?? false,
129
+ engineOptions: engine === "docker" ? options.slice(0, global) : options,
130
+ buildx: words[0] === "buildx"
131
+ };
121
132
  if (!candidates.length)
122
133
  return null;
123
134
  }
@@ -143,8 +154,11 @@ function errorMessage(exc) {
143
154
  }
144
155
 
145
156
  // src/containerfile.ts
157
+ var engineEscapes = (value) => /[^\p{L}\p{M}\p{N}\p{P}\p{S} ]/u.test(value);
158
+
146
159
  class Unreadable extends Error {
147
160
  located = false;
161
+ hint = "";
148
162
  }
149
163
  var fail = (message) => {
150
164
  throw new Unreadable(message);
@@ -491,6 +505,7 @@ class Unknown {
491
505
  }
492
506
  }
493
507
  var BASE_ENV = new Unknown("base");
508
+ var SCRATCH_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
494
509
  var BUILTINS = [
495
510
  "BUILDPLATFORM",
496
511
  "BUILDOS",
@@ -505,6 +520,7 @@ var BUILTINS = [
505
520
  "TARGETSTAGE"
506
521
  ];
507
522
  var INJECTED = /^(?:SOURCE_DATE_EPOCH|(?:HTTPS?|FTP|NO|ALL)_PROXY)$/iu;
523
+ var INJECTED_IB = /^SOURCE_DATE_EPOCH$/iu;
508
524
  var PROXIES = /^(?:(?:HTTPS?|FTP|NO)_PROXY|(?:https?|ftp|no)_proxy)$/u;
509
525
  var NON_ENV = ["BUILDKIT_SBOM_SCAN_CONTEXT", "BUILDKIT_SBOM_SCAN_STAGE"];
510
526
  function expandWord(word, escape, model, lookup) {
@@ -517,8 +533,11 @@ function expandWord(word, escape, model, lookup) {
517
533
  return;
518
534
  const v = lookup(name);
519
535
  if (v instanceof Unknown) {
520
- if (v.why !== "error")
521
- fail(`ce cannot tell what $${name} is there`);
536
+ if (v.why !== "error") {
537
+ const exc = new Unreadable(`ce cannot tell what $${name} is there`);
538
+ exc.hint = v.reason;
539
+ throw exc;
540
+ }
522
541
  const exc = new Unreadable(v.reason);
523
542
  exc.located = true;
524
543
  throw exc;
@@ -616,7 +635,67 @@ function expandWord(word, escape, model, lookup) {
616
635
  };
617
636
  return until("");
618
637
  }
619
- function argDefault(parsed, name, target, buildArg, model = "buildkit", { contexts = [], envs = [] } = {}) {
638
+ var VARIANTS = new Map(Object.entries({
639
+ amd64: { "": "", v1: "", v2: "v2", v3: "v3", v4: "v4" },
640
+ arm64: { "": "", "8": "v8", v8: "v8", "v8.0": "v8" },
641
+ arm: {
642
+ "": "v7",
643
+ "5": "v5",
644
+ "6": "v6",
645
+ "7": "v7",
646
+ "8": "v8",
647
+ v5: "v5",
648
+ v6: "v6",
649
+ v7: "v7",
650
+ v8: "v8"
651
+ },
652
+ "386": { "": "" },
653
+ ppc64le: { "": "" },
654
+ riscv64: { "": "" },
655
+ s390x: { "": "" }
656
+ }).map(([arch, variants]) => [arch, new Map(Object.entries(variants))]));
657
+ var ARCH_NAMES = new Map([
658
+ ["aarch64", ["arm64"]],
659
+ ["x86_64", ["amd64"]],
660
+ ["x86-64", ["amd64"]],
661
+ ["i386", ["386", ""]],
662
+ ["armhf", ["arm", "v7"]],
663
+ ["armel", ["arm", "v6"]]
664
+ ]);
665
+ function platformArgs(platform, model) {
666
+ const parts = platform.toLowerCase().split("/");
667
+ const isArch = (name) => VARIANTS.has(name) || ARCH_NAMES.has(name);
668
+ if (parts.length === 1 && isArch(parts[0]) && process.platform === "linux")
669
+ parts.unshift("linux");
670
+ const [os, written = "", given = "", ...rest] = parts;
671
+ if (os !== "linux" || rest.length)
672
+ return;
673
+ const [arch = written, fixed] = ARCH_NAMES.get(written) ?? [];
674
+ let variant = fixed === undefined ? VARIANTS.get(arch)?.get(given) : given ? undefined : fixed;
675
+ if (variant === undefined)
676
+ return;
677
+ if (arch === "arm64" && model === "buildkit")
678
+ variant = "";
679
+ return new Map([
680
+ ["TARGETPLATFORM", [os, arch, variant].filter(Boolean).join("/")],
681
+ ["TARGETOS", os],
682
+ ["TARGETARCH", arch],
683
+ ["TARGETVARIANT", variant],
684
+ ["TARGETOSVERSION", ""]
685
+ ]);
686
+ }
687
+ function normalizePlatform(platform, engine) {
688
+ return platformArgs(platform, engine === "docker" ? "buildkit" : "imagebuilder")?.get("TARGETPLATFORM");
689
+ }
690
+ function argDefault(parsed, name, target, buildArg, model = "buildkit", {
691
+ contexts = [],
692
+ envs = [],
693
+ env: hostEnv = process.env,
694
+ platform,
695
+ platformIgnored,
696
+ format,
697
+ from: fromImage
698
+ } = {}) {
620
699
  const buildkit = model === "buildkit";
621
700
  const escape = buildkit ? parsed.escape : "\\";
622
701
  const evaluate = (word, line, lookup) => {
@@ -625,29 +704,41 @@ function argDefault(parsed, name, target, buildArg, model = "buildkit", { contex
625
704
  } catch (exc) {
626
705
  if (!(exc instanceof Unreadable))
627
706
  throw exc;
628
- return new Unknown("error", exc.located ? exc.message : `${exc.message}, on line ${line}`);
707
+ return new Unknown("error", exc.located ? exc.message : `${exc.message}, on line ${line}${exc.hint && `: ${exc.hint}`}`);
629
708
  }
630
709
  };
631
710
  const isBuiltin = (arg) => BUILTINS.includes(arg);
711
+ let targets;
712
+ const builtin = (arg, global) => {
713
+ if (arg.startsWith("BUILD"))
714
+ return new Unknown("builtin", "it is the platform of the machine that builds");
715
+ if (arg === "TARGETSTAGE")
716
+ return new Unknown("builtin");
717
+ if (global && !buildkit)
718
+ return new Unknown("builtin", "podman and buildah do not set it from --platform before the first FROM");
719
+ targets ??= platformIgnored?.() ?? (platform === undefined ? "it comes from --platform, which the build does not have" : platformArgs(platform, model) ?? `ce does not read the --platform ${JSON.stringify(platform)}`);
720
+ return typeof targets === "string" ? new Unknown("builtin", targets) : targets.get(arg);
721
+ };
722
+ const injected = (arg) => (buildkit ? INJECTED : INJECTED_IB).test(arg);
632
723
  const plain = (name) => ![...`$'"`, escape].some((ch) => name.includes(ch));
633
724
  const named = (kind, name, line) => plain(name) ? name : fail(`ce cannot tell what name the ${kind} on line ${line} sets`);
634
725
  const globals = new Map;
635
- for (const builtin of BUILTINS)
636
- globals.set(builtin, buildArg(builtin) ?? new Unknown("builtin"));
726
+ for (const arg of BUILTINS)
727
+ globals.set(arg, buildArg(arg) ?? builtin(arg, true));
637
728
  for (const { pairs, line } of parsed.globals) {
638
729
  const before = new Map(globals);
639
730
  const seen = (ref) => {
640
731
  const known = buildkit ? globals : before;
641
732
  if (known.has(ref) || buildkit || !PROXIES.test(ref))
642
733
  return known.get(ref);
643
- return buildArg(ref) ?? new Unknown("injected");
734
+ return buildArg(ref);
644
735
  };
645
736
  for (const { name, value } of pairs) {
646
737
  const arg = named("ARG", name, line);
647
738
  const given = buildArg(arg);
648
739
  if (given !== undefined)
649
740
  globals.set(arg, given);
650
- else if (INJECTED.test(arg))
741
+ else if (injected(arg))
651
742
  globals.set(arg, new Unknown("injected"));
652
743
  else if (value !== undefined && !(isBuiltin(arg) && !buildkit))
653
744
  globals.set(arg, evaluate(value, line, seen));
@@ -670,7 +761,7 @@ function argDefault(parsed, name, target, buildArg, model = "buildkit", { contex
670
761
  }
671
762
  const source = (index) => {
672
763
  const stage = parsed.stages[index];
673
- const base = evaluate(stage.base, stage.line, (ref) => globals.get(ref));
764
+ const base = index === 0 && fromImage !== undefined && !buildkit ? fromImage : evaluate(stage.base, stage.line, (ref) => globals.get(ref));
674
765
  let parent = typeof base === "string" ? findStage(base, index) : -1;
675
766
  const swapped = buildkit ? parent >= 0 ? replaced(parent) : undefined : contexts.find((c) => c === stage.base || c === base);
676
767
  if (swapped !== undefined)
@@ -695,14 +786,14 @@ function argDefault(parsed, name, target, buildArg, model = "buildkit", { contex
695
786
  let triggers = [];
696
787
  if (buildkit) {
697
788
  if (base instanceof Unknown)
698
- state.undeclared = new Unknown("error", `ce cannot tell what the FROM on line ${stage.line} is built from`);
789
+ state.undeclared = new Unknown("error", `ce cannot tell what the FROM on line ${stage.line} is built from: ${base.reason}`);
699
790
  else if (swapped !== undefined)
700
791
  state.undeclared = lost(swapped);
701
792
  else if (from) {
702
793
  state.args = new Map(from.args);
703
794
  state.undeclared = from.undeclared;
704
795
  for (const [k, v] of from.env)
705
- state.env.set(k, from.opaque || k === "PATH" ? BASE_ENV : v);
796
+ state.env.set(k, from.opaque ? BASE_ENV : k === "PATH" ? SCRATCH_PATH : v);
706
797
  for (const [k, v] of from.image)
707
798
  state.env.set(k, v);
708
799
  triggers = parsed.stages[parent].instructions.filter((i) => i.onbuild);
@@ -710,24 +801,45 @@ function argDefault(parsed, name, target, buildArg, model = "buildkit", { contex
710
801
  } else {
711
802
  if (from)
712
803
  state.env = new Map(from.env);
713
- for (const flag of envs) {
804
+ const unset = envs.filter((flag) => flag.startsWith("--unsetenv "));
805
+ for (const flag of unset) {
714
806
  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
- }
807
+ if (/["\r\n]/u.test(spec))
808
+ for (const k of state.env.keys())
809
+ state.env.set(k, new Unknown("error", `${k} may be changed by ${/[\r\n]/u.test(spec) ? `--unsetenv ${JSON.stringify(spec)}` : flag}`));
810
+ else
811
+ for (const k of spec.split(","))
812
+ state.env.delete(k);
813
+ }
814
+ for (const flag of envs.filter((f) => f.startsWith("--env "))) {
815
+ const spec = flag.slice(flag.indexOf(" ") + 1);
816
+ const copied = (k, v) => {
817
+ const what = v.includes("$") ? "expand the $" : engineEscapes(v) ? "escape the characters podman and buildah escape" : undefined;
818
+ if (what === undefined)
819
+ return v;
820
+ return new Unknown("error", spec.includes("=") ? `ce does not ${what} in --env ${engineEscapes(spec) ? JSON.stringify(spec) : spec}` : `ce does not ${what} in the ${k} that ${flag} copies`);
821
+ };
822
+ if (spec.includes("=")) {
823
+ const k = spec.slice(0, spec.indexOf("="));
824
+ state.env.set(k, copied(k, spec.slice(k.length + 1)));
825
+ } else if (spec.endsWith("*") && !spec.slice(0, -1).includes("*")) {
826
+ for (const [k, v] of Object.entries(hostEnv))
827
+ if (k.startsWith(spec.slice(0, -1)) && v !== undefined)
828
+ state.env.set(k, copied(k, v));
829
+ } else if (spec.includes("*"))
830
+ fail(`ce does not read ${flag}`);
831
+ else if (!spec.endsWith("-") && hostEnv[spec] !== undefined)
832
+ state.env.set(spec, copied(spec, hostEnv[spec]));
724
833
  }
725
834
  const written = findStage(stage.base, index);
726
835
  if (swapped !== undefined)
727
836
  state.undeclared = lost(swapped);
728
837
  else if (written >= 0)
729
838
  state.args = inheritedArgs(written);
730
- for (const { kind, pairs, line } of parent >= 0 ? parsed.stages[parent].instructions.filter((i) => i.onbuild) : []) {
839
+ const onbuilds = parent >= 0 ? parsed.stages[parent].instructions.filter((i) => i.onbuild) : [];
840
+ if (format === "docker")
841
+ triggers = onbuilds;
842
+ for (const { kind, pairs, line } of format ? [] : onbuilds) {
731
843
  for (const { name: arg } of pairs) {
732
844
  if (!plain(arg))
733
845
  fail(`ce cannot tell what name the ONBUILD on line ${line} sets`);
@@ -735,14 +847,15 @@ function argDefault(parsed, name, target, buildArg, model = "buildkit", { contex
735
847
  }
736
848
  }
737
849
  }
738
- const see = (env) => (ref) => env.has(ref) ? env.get(ref) : !buildkit || state.opaque || ref === "PATH" ? BASE_ENV : undefined;
850
+ const see = (env, args) => (ref) => env.has(ref) ? env.get(ref) : state.opaque ? BASE_ENV : ref === "PATH" ? SCRATCH_PATH : buildkit ? undefined : args.has(ref) ? args.get(ref) : PROXIES.test(ref) ? buildArg(ref) : undefined;
739
851
  const own = stage.instructions.filter((i) => !i.onbuild);
740
852
  for (const { kind, pairs, line } of [...triggers, ...own]) {
741
853
  const before = new Map(state.env);
854
+ const argsBefore = new Map(state.args);
742
855
  for (const { name, value } of pairs) {
743
856
  const arg = named(kind, name, line);
744
857
  const seen = buildkit && kind === "ARG" ? state.env : before;
745
- const evaluated = (word) => evaluate(word, line, see(seen));
858
+ const evaluated = (word) => evaluate(word, line, see(seen, argsBefore));
746
859
  if (kind === "ENV") {
747
860
  const v = value === undefined ? new Unknown("error", `ce does not read the ENV ${arg} on line ${line}`) : evaluated(value);
748
861
  state.env.set(arg, v);
@@ -753,10 +866,10 @@ function argDefault(parsed, name, target, buildArg, model = "buildkit", { contex
753
866
  let v;
754
867
  if (given !== undefined)
755
868
  v = given;
756
- else if (INJECTED.test(arg))
869
+ else if (injected(arg))
757
870
  v = new Unknown("injected");
758
871
  else if (isBuiltin(arg) && (!buildkit || value === undefined))
759
- v = globals.get(arg);
872
+ v = buildkit ? globals.get(arg) : builtin(arg, false);
760
873
  else if (value !== undefined)
761
874
  v = evaluated(value);
762
875
  else if (globals.get(arg) !== undefined)
@@ -792,7 +905,7 @@ function argDefault(parsed, name, target, buildArg, model = "buildkit", { contex
792
905
  const given = buildArg(arg);
793
906
  if (given !== undefined)
794
907
  args.set(arg, given);
795
- else if (INJECTED.test(arg))
908
+ else if (injected(arg))
796
909
  args.set(arg, new Unknown("injected"));
797
910
  else if (isBuiltin(arg))
798
911
  args.set(arg, globals.get(arg));
@@ -830,10 +943,10 @@ function argDefault(parsed, name, target, buildArg, model = "buildkit", { contex
830
943
  for (const i of ancestors)
831
944
  run(i);
832
945
  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));
946
+ const v = state.args.has(name) ? state.args.get(name) : state.undeclared ?? (!globals.has(name) && injected(name) && buildArg(name) === undefined ? new Unknown("injected") : globals.get(name));
834
947
  if (v instanceof Unknown) {
835
948
  if (v.why === "builtin")
836
- fail(`${name} is set by the builder`);
949
+ fail(`${name} is set by the builder${v.reason && `: ${v.reason}`}`);
837
950
  if (v.why === "injected")
838
951
  fail(`${name} may be set by the builder`);
839
952
  fail(v.why === "error" ? v.reason : `ce cannot tell what ${name} is`);
@@ -855,38 +968,64 @@ function readContainerfile(file, model) {
855
968
  throw exc;
856
969
  }
857
970
  }
858
- function containerfileDefaults(files, buildArg) {
971
+ function loadContainerfile(files, buildArg) {
859
972
  const docker = files.engine === "docker";
860
- const model = docker ? "buildkit" : "imagebuilder";
973
+ let file;
974
+ let parsed;
975
+ try {
976
+ file = findContainerfile(files);
977
+ parsed = readContainerfile(file, docker ? "buildkit" : "imagebuilder");
978
+ } catch (exc) {
979
+ if (exc instanceof Unreadable)
980
+ return { error: exc };
981
+ throw exc;
982
+ }
983
+ const frontend = docker ? buildArg("BUILDKIT_SYNTAX")?.trim().split(" ")[0] ?? parsed.syntax : undefined;
984
+ if (frontend !== undefined && !STANDARD_SYNTAX.test(frontend))
985
+ return {
986
+ file,
987
+ error: new Unreadable(`it is read by the frontend ${JSON.stringify(frontend)}, which may read it differently`)
988
+ };
989
+ return { file, parsed };
990
+ }
991
+ function dockerfileEpoch(files, buildArg) {
861
992
  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
- };
993
+ try {
994
+ read = loadContainerfile(files, buildArg);
995
+ } catch {
996
+ return "";
997
+ }
998
+ if (!read.parsed)
999
+ return "";
1000
+ const defaults = read.parsed.globals.flatMap(({ pairs }) => pairs).filter(({ name, value }) => name === "SOURCE_DATE_EPOCH" && value !== undefined);
1001
+ if (!defaults.length)
1002
+ return;
1003
+ const value = defaults[0].value;
1004
+ return defaults.length === 1 && !value.includes("$") ? value : "";
1005
+ }
1006
+ function containerfileDefaults(files, buildArg) {
1007
+ const model = files.engine === "docker" ? "buildkit" : "imagebuilder";
1008
+ let read;
1009
+ const load = () => loadContainerfile(files, buildArg);
1010
+ const platforms = files.platforms?.length ? [...new Set(files.platforms)] : [undefined];
881
1011
  return (name) => {
882
1012
  read ??= load();
883
1013
  try {
884
1014
  if (read.error)
885
1015
  throw read.error;
886
- return argDefault(read.parsed, name, files.target, buildArg, model, {
1016
+ const values = platforms.map((platform) => argDefault(read.parsed, name, files.target, buildArg, model, {
887
1017
  contexts: files.contexts ?? [],
888
- envs: files.envs ?? []
889
- });
1018
+ envs: files.envs ?? [],
1019
+ env: files.env ?? process.env,
1020
+ platform,
1021
+ platformIgnored: files.platformIgnored,
1022
+ format: files.format,
1023
+ from: files.from
1024
+ }));
1025
+ const other = values.findIndex((v) => (v ?? "") !== (values[0] ?? ""));
1026
+ if (other !== -1)
1027
+ fail(`it is ${JSON.stringify(values[0] ?? "")} for ${platforms[0]} but ${JSON.stringify(values[other] ?? "")} for ${platforms[other]}, and every platform gets the same tags`);
1028
+ return values[0];
890
1029
  } catch (exc) {
891
1030
  if (!(exc instanceof Unreadable))
892
1031
  throw exc;
@@ -1000,6 +1139,19 @@ var CI_VARIABLES = [
1000
1139
  var ENV_VARIABLE = /^env\.([A-Za-z_][A-Za-z0-9_]*)$/u;
1001
1140
  var SEMVER = /^v?((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)?)$/u;
1002
1141
  var nonEmpty = (value) => value ? value : undefined;
1142
+ function epochDate(epoch, base = 10) {
1143
+ const match = base === 10 ? /^([+-]?)(\d+)$/u.exec(epoch) : /^([+-]?)(0[xX][0-9a-fA-F]+|0[oO][0-7]+|0[bB][01]+|0[0-7]+|0|[1-9]\d*)$/u.exec(epoch);
1144
+ if (!match)
1145
+ return;
1146
+ const digits = match[2];
1147
+ const magnitude = base === 0 && /^0[0-7]/u.test(digits) ? BigInt(`0o${digits.slice(1)}`) : BigInt(digits);
1148
+ const seconds = match[1] === "-" ? -magnitude : magnitude;
1149
+ if (seconds < -(2n ** 63n) || seconds >= 2n ** 63n)
1150
+ return;
1151
+ const date = new Date(Number(seconds) * 1000);
1152
+ return Number.isNaN(date.getTime()) ? undefined : date;
1153
+ }
1154
+ var rfc3339 = (date) => date.toISOString().replace(/\.\d{3}Z$/u, "Z");
1003
1155
 
1004
1156
  class CiMetadata {
1005
1157
  #env;
@@ -1020,7 +1172,7 @@ class CiMetadata {
1020
1172
  this.#cache.set(name, this.#resolve(name));
1021
1173
  return this.#cache.get(name);
1022
1174
  }
1023
- labels() {
1175
+ labels(now = new Date) {
1024
1176
  const labels = {};
1025
1177
  const add = (key, value) => {
1026
1178
  try {
@@ -1034,14 +1186,29 @@ class CiMetadata {
1034
1186
  };
1035
1187
  const server = nonEmpty(this.#ciEnv("SERVER_URL"))?.replace(/\/+$/u, "");
1036
1188
  const repository = nonEmpty(this.#ciEnv("REPOSITORY"));
1037
- add("source", () => server && repository ? `${server}/${repository}` : undefined);
1189
+ const page = server && repository ? `${server}/${repository}` : undefined;
1190
+ const repo = (key) => {
1191
+ const value = this.#payload().repository;
1192
+ return isObject(value) && typeof value[key] === "string" ? value[key] : undefined;
1193
+ };
1194
+ add("source", () => page);
1195
+ add("url", () => {
1196
+ try {
1197
+ return repo("website") || page;
1198
+ } catch (exc) {
1199
+ if (exc instanceof CeError)
1200
+ return page;
1201
+ throw exc;
1202
+ }
1203
+ });
1038
1204
  add("revision", () => this.#ciEnv("SHA"));
1039
1205
  add("version", () => this.get("version") ?? this.#rawRef());
1040
1206
  add("title", () => repository?.split("/").at(-1));
1041
- add("description", () => {
1042
- const repo = this.#payload().repository;
1043
- return isObject(repo) && typeof repo.description === "string" ? repo.description : undefined;
1044
- });
1207
+ add("description", () => repo("description"));
1208
+ const epoch = this.#env.SOURCE_DATE_EPOCH;
1209
+ const time = epoch ? epochDate(epoch) : now;
1210
+ if (Object.keys(labels).length && time)
1211
+ add("created", () => rfc3339(time));
1045
1212
  return labels;
1046
1213
  }
1047
1214
  #resolve(name) {
@@ -1221,7 +1388,20 @@ var system = {
1221
1388
  throw result.error;
1222
1389
  return { status: result.status ?? 1, stderr: result.stderr ?? "" };
1223
1390
  },
1391
+ output(argv) {
1392
+ const result = spawnSync(argv[0], argv.slice(1), {
1393
+ stdio: ["ignore", "pipe", "ignore"],
1394
+ env: childEnv(),
1395
+ encoding: "utf8"
1396
+ });
1397
+ if (result.error)
1398
+ throw result.error;
1399
+ return { status: result.status ?? 1, stdout: result.stdout ?? "" };
1400
+ },
1224
1401
  sleep: (ms) => setTimeout(ms),
1402
+ sleepSync(ms) {
1403
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1404
+ },
1225
1405
  exec(file, argv) {
1226
1406
  const resolved = system.which(file);
1227
1407
  if (!resolved) {
@@ -1288,15 +1468,17 @@ function expand(template, vars, encode) {
1288
1468
  const parts = parse(template, vars);
1289
1469
  if (!guardsPass(parts, vars))
1290
1470
  return null;
1471
+ return render(parts, vars, encode, (name) => `ce: {${name}} has no value here, in ${JSON.stringify(template)}. ` + `To leave the tag out instead, guard it with {?${name}}.`);
1472
+ }
1473
+ function render(parts, vars, encode, missing) {
1291
1474
  let out = "";
1292
1475
  for (const part of parts) {
1293
1476
  if ("text" in part)
1294
1477
  out += part.text;
1295
1478
  else if (!part.guard) {
1296
1479
  const value = vars.get(part.name);
1297
- if (value === undefined) {
1298
- throw new CeError(`ce: {${part.name}} has no value here, in ${JSON.stringify(template)}. ` + `To leave the tag out instead, guard it with {?${part.name}}.`);
1299
- }
1480
+ if (value === undefined)
1481
+ throw new CeError(missing(part.name));
1300
1482
  out += encode(value);
1301
1483
  }
1302
1484
  }
@@ -1307,19 +1489,14 @@ function checkValue(template, vars, what, guards = false) {
1307
1489
  if (!guards && parts.some((part) => ("guard" in part) && part.guard)) {
1308
1490
  throw new CeError(`ce: ${what} ${JSON.stringify(template)} cannot use {?guards}; they only apply to tags and prefixes`);
1309
1491
  }
1492
+ return parts;
1310
1493
  }
1311
1494
  function expandValue(template, vars, encode, what, guards = false) {
1312
- checkValue(template, vars, what, guards);
1313
- const parts = parse(template, vars);
1495
+ const parts = checkValue(template, vars, what, guards);
1314
1496
  if (!guardsPass(parts, vars))
1315
1497
  return "";
1316
- for (const part of parts) {
1317
- if ("name" in part && !part.guard && vars.get(part.name) === undefined) {
1318
- const hint = guards ? ` To leave the ${what} empty instead, guard it with {?${part.name}}.` : "";
1319
- throw new CeError(`ce: {${part.name}} has no value here, in the ${what} ${JSON.stringify(template)}.${hint}`);
1320
- }
1321
- }
1322
- return expand(template, vars, encode);
1498
+ const hint = (name) => guards ? ` To leave the ${what} empty instead, guard it with {?${name}}.` : "";
1499
+ return render(parts, vars, encode, (name) => `ce: {${name}} has no value here, in the ${what} ${JSON.stringify(template)}.${hint(name)}`);
1323
1500
  }
1324
1501
 
1325
1502
  // src/tags.ts
@@ -1529,9 +1706,23 @@ var SUMMARIZED = {
1529
1706
  "--target": "target",
1530
1707
  "--platform": "platform",
1531
1708
  "--build-arg": "buildArgs",
1532
- "--label": "labels"
1709
+ "--label": "labels",
1710
+ "--annotation": "annotations"
1533
1711
  };
1534
1712
  var ARG_VARIABLE = /^arg\.([A-Za-z_][A-Za-z0-9_]*)$/u;
1713
+ var PLATFORM_FROM = {
1714
+ podman: [5, 2],
1715
+ buildah: [1, 37]
1716
+ };
1717
+ var PLATFORM_PARTS = [
1718
+ "--arch",
1719
+ "--os",
1720
+ "--variant",
1721
+ "--override-arch",
1722
+ "--override-os",
1723
+ "--os-version",
1724
+ "--os-feature"
1725
+ ];
1535
1726
  function withBuildArgs(vars, values, argFile, fromFile) {
1536
1727
  return {
1537
1728
  known: (name) => ARG_VARIABLE.test(name) || vars.known(name),
@@ -1556,10 +1747,12 @@ var SHORT_BOOLS = {
1556
1747
  };
1557
1748
  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
1749
  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";
1750
+ var PODMAN_ROOT_VALUES = "cdi-spec-dir cgroup-manager conmon cpu-profile db-backend default-mounts-file events-backend hooks-dir imagestore log-level max-workers memory-profile namespace network-backend network-cmd-path network-config-dir pull-option registries-conf root runroot runtime runtime-flag storage-driver storage-opt tmpdir volumepath";
1751
+ var BUILDAH_ROOT_VALUES = "cgroup-manager cpu-profile default-mounts-file imagestore log-level memory-profile registries-conf registries-conf-dir root runroot short-name-alias-conf storage-driver storage-opt userns-gid-map userns-uid-map";
1559
1752
  var VALUE_FLAGS = {
1560
1753
  docker: new Set(BUILDX_VALUES.split(" ")),
1561
- podman: new Set(BUILDAH_VALUES.split(" ")),
1562
- buildah: new Set(BUILDAH_VALUES.split(" "))
1754
+ podman: new Set(`${BUILDAH_VALUES} ${PODMAN_ROOT_VALUES}`.split(" ")),
1755
+ buildah: new Set(`${BUILDAH_VALUES} runtime ${BUILDAH_ROOT_VALUES}`.split(" "))
1563
1756
  };
1564
1757
  var SHORT_VALUES = {
1565
1758
  docker: "cfmot",
@@ -1568,23 +1761,34 @@ var SHORT_VALUES = {
1568
1761
  };
1569
1762
  var takesValue = (arg, engine) => arg.startsWith("--") ? VALUE_FLAGS[engine].has(arg.slice(2)) : arg.length === 2 && SHORT_VALUES[engine].includes(arg[1]);
1570
1763
  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];
1764
+ const out = [];
1765
+ for (let at = 0;at < argv.length; at++) {
1766
+ const arg = argv[at];
1767
+ if (arg === "--") {
1768
+ out.push(...argv.slice(at));
1769
+ break;
1770
+ }
1575
1771
  const split = [];
1576
1772
  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
- });
1773
+ if (/^-[^-]/u.test(arg))
1774
+ while (i < arg.length - 1 && SHORT_BOOLS[engine].includes(arg[i]) && arg[i + 1] !== "=")
1775
+ split.push(`-${arg[i++]}`);
1776
+ split.push(i === 1 ? arg : `-${arg.slice(i)}`);
1777
+ out.push(...split);
1778
+ if (takesValue(split.at(-1), engine) && at + 1 < argv.length)
1779
+ out.push(argv[++at]);
1780
+ }
1781
+ return out;
1581
1782
  }
1582
1783
  function parseBuildArgs(given, tagPrefix, cachePrefix, {
1583
1784
  vars = new CiMetadata,
1584
1785
  labels = {},
1585
1786
  defaultBranch,
1586
1787
  env = process.env,
1587
- engine = "docker"
1788
+ engine = "docker",
1789
+ buildx = false,
1790
+ engineVersion,
1791
+ sbom = false
1588
1792
  } = {}) {
1589
1793
  const argv = ungroup(given, engine);
1590
1794
  const parts = [];
@@ -1596,6 +1800,7 @@ function parseBuildArgs(given, tagPrefix, cachePrefix, {
1596
1800
  cacheTo: [],
1597
1801
  buildArgs: [],
1598
1802
  labels: [],
1803
+ annotations: [],
1599
1804
  dockerfile: "",
1600
1805
  target: "",
1601
1806
  platform: "",
@@ -1608,6 +1813,14 @@ function parseBuildArgs(given, tagPrefix, cachePrefix, {
1608
1813
  const positionals = [];
1609
1814
  const contexts = [];
1610
1815
  const envs = [];
1816
+ const platforms = [];
1817
+ let platformParts = false;
1818
+ let fromImage;
1819
+ let format = env["BUILDAH_FORMAT"];
1820
+ const sbomFlags = [];
1821
+ const times = new Map;
1822
+ let epochArg;
1823
+ let allPlatforms = false;
1611
1824
  let rest = [];
1612
1825
  const addTags = (value, flag) => {
1613
1826
  templated ||= hasPlaceholders(value);
@@ -1616,12 +1829,15 @@ function parseBuildArgs(given, tagPrefix, cachePrefix, {
1616
1829
  const summarize = (flag, value) => {
1617
1830
  const field = SUMMARIZED[flag];
1618
1831
  if (field === "buildArgs") {
1619
- if (value.includes("="))
1620
- buildArgs.set(...splitAt(value, "="));
1621
- else if (Object.hasOwn(env, value))
1622
- buildArgs.set(value, env[value]);
1832
+ const [key, given] = value.includes("=") ? splitAt(value, "=") : [value, Object.hasOwn(env, value) ? env[value] : undefined];
1833
+ if (given !== undefined)
1834
+ buildArgs.set(key, given);
1623
1835
  else if (engine !== "docker")
1624
1836
  buildArgs.set(value, null);
1837
+ if (key === "SOURCE_DATE_EPOCH" && given !== undefined)
1838
+ epochArg = given;
1839
+ else if (key === "SOURCE_DATE_EPOCH" && engine !== "docker")
1840
+ epochArg = undefined;
1625
1841
  if (engine !== "docker" && buildArgs.has("TARGETPLATFORM"))
1626
1842
  buildArgs.set("TARGETPLATFORM", null);
1627
1843
  const epoch = buildArgs.get("SOURCE_DATE_EPOCH");
@@ -1630,7 +1846,10 @@ function parseBuildArgs(given, tagPrefix, cachePrefix, {
1630
1846
  }
1631
1847
  if (field === "dockerfile")
1632
1848
  files.push(value);
1633
- if (field === "buildArgs" || field === "labels")
1849
+ if (field === "platform") {
1850
+ platforms.push(...value.split(","));
1851
+ summary.platform = platforms.join(",");
1852
+ } else if (field === "buildArgs" || field === "labels" || field === "annotations")
1634
1853
  summary[field].push(value);
1635
1854
  else
1636
1855
  summary[field] = value;
@@ -1640,6 +1859,18 @@ function parseBuildArgs(given, tagPrefix, cachePrefix, {
1640
1859
  const [name, attached] = arg.startsWith("--") && arg.includes("=") ? splitAt(arg, "=") : [arg, undefined];
1641
1860
  const next = () => consumeValue(argv.slice(++i), arg)[0];
1642
1861
  const short = () => arg.slice(arg[2] === "=" ? 3 : 2);
1862
+ if (engine !== "docker" && PLATFORM_PARTS.includes(name))
1863
+ platformParts = true;
1864
+ if (engine !== "docker" && name === "--format")
1865
+ format = attached ?? argv[i + 1];
1866
+ if (engine !== "docker" && name === "--all-platforms")
1867
+ allPlatforms = attached === undefined || !/^(?:0|f|false)$/iu.test(attached);
1868
+ if (engine !== "docker" && name === "--from")
1869
+ fromImage = attached ?? argv[i + 1] ?? "";
1870
+ if (name.startsWith("--sbom") || name === "--attest")
1871
+ sbomFlags.push([name, attached ?? argv[i + 1] ?? ""]);
1872
+ if (engine !== "docker" && (name === "--timestamp" || name === "--source-date-epoch"))
1873
+ times.set(name, attached ?? argv[i + 1] ?? "");
1643
1874
  if (arg === "--") {
1644
1875
  rest = argv.slice(i);
1645
1876
  positionals.push(...rest.slice(1));
@@ -1703,23 +1934,74 @@ function parseBuildArgs(given, tagPrefix, cachePrefix, {
1703
1934
  }
1704
1935
  }
1705
1936
  }
1937
+ const whyIgnored = () => {
1938
+ if (allPlatforms)
1939
+ return `--all-platforms builds for the platforms of the base images, which ce does not read`;
1940
+ if (platformParts)
1941
+ return `${engine}'s --arch, --os or --variant changes the platform, which ce does not follow`;
1942
+ const from = PLATFORM_FROM[engine];
1943
+ if (!from || !platforms.length)
1944
+ return;
1945
+ const since = `ce follows how ${engine} sets it from --platform only from ${from.join(".")}`;
1946
+ const version = engineVersion?.();
1947
+ if (version === undefined)
1948
+ return `${since}, and cannot tell which version builds`;
1949
+ return older(version, from) ? `${since}, and this is ${version}` : undefined;
1950
+ };
1951
+ let ignored;
1952
+ const platformIgnored = () => (ignored ??= { why: whyIgnored() }).why;
1706
1953
  const baseVars = vars;
1707
- const fromFile = containerfileDefaults({ engine, files, positionals, target: summary.target, contexts, envs }, (arg) => buildArgs.get(arg) ?? undefined);
1954
+ const buildFiles = {
1955
+ engine,
1956
+ files,
1957
+ positionals,
1958
+ target: summary.target,
1959
+ contexts,
1960
+ envs,
1961
+ env,
1962
+ platforms,
1963
+ platformIgnored,
1964
+ format: format === "oci" || format === "docker" ? format : undefined,
1965
+ from: fromImage
1966
+ };
1967
+ const buildArg = (arg) => buildArgs.get(arg) ?? undefined;
1968
+ const fromFile = containerfileDefaults(buildFiles, buildArg);
1708
1969
  vars = withBuildArgs(baseVars, buildArgs, argFile, fromFile);
1709
1970
  const args = parts.flatMap((part) => {
1710
1971
  if (typeof part === "string")
1711
1972
  return [part];
1712
- const refs = rewriteTagValue(part.tags, tagPrefix, vars);
1973
+ const refs = [
1974
+ ...new Set(rewriteTagValue(part.tags, tagPrefix, vars))
1975
+ ].filter((ref) => !summary.tags.includes(ref));
1713
1976
  summary.tags.push(...refs);
1714
1977
  return refs.map((ref) => `${part.flag}${ref}`);
1715
1978
  });
1716
1979
  if (templated && !summary.tags.length) {
1717
1980
  throw new CeError("ce: the guards in the -t templates left out every tag, so the image would have none");
1718
1981
  }
1719
- const ownKeys = new Set(summary.labels.map((label) => label.split("=", 1)[0]));
1720
- const added = Object.entries(labels).filter(([key]) => !ownKeys.has(key)).map(([key, value]) => `${key}=${value}`);
1721
- args.unshift(...added.map((label) => `--label=${label}`));
1722
- summary.labels.unshift(...added);
1982
+ const CREATED = "org.opencontainers.image.created";
1983
+ const timestamp = times.get("--timestamp");
1984
+ const flagEpoch = timestamp ?? times.get("--source-date-epoch");
1985
+ let epoch;
1986
+ if (engine === "docker")
1987
+ epoch = epochArg ?? (env["SOURCE_DATE_EPOCH"] || !labels[CREATED] ? undefined : dockerfileEpoch(buildFiles, buildArg));
1988
+ else if (flagEpoch !== undefined || env["SOURCE_DATE_EPOCH"])
1989
+ epoch = flagEpoch;
1990
+ else
1991
+ epoch = epochArg ?? (argFile ? "" : undefined);
1992
+ const time = epoch === undefined ? undefined : epochDate(epoch, timestamp === undefined ? 10 : 0);
1993
+ const metadata = Object.entries(labels).filter(([key]) => key !== CREATED || epoch === undefined || time).map(([key, value]) => [
1994
+ key,
1995
+ time && key === CREATED ? rfc3339(time) : value
1996
+ ]);
1997
+ const ownLabels = new Set(summary.labels.map((label) => label.split("=", 1)[0]));
1998
+ const ownAnnotations = new Set(summary.annotations.map((annotation) => annotationKey(annotation, engine)));
1999
+ const addedLabels = metadata.filter(([key, value]) => !ownLabels.has(key) && (engine === "docker" || !(value.includes("$") || engineEscapes(value)))).map(([key, value]) => `${key}=${value}`);
2000
+ const level = engine === "docker" && indexed(platforms) ? "manifest,index:" : "";
2001
+ const addedAnnotations = annotates(engine, env, buildx) ? metadata.filter(([key]) => !ownAnnotations.has(key)).map(([key, value]) => `${level}${key}=${value}`) : [];
2002
+ args.unshift(...addedLabels.map((label) => `--label=${label}`), ...addedAnnotations.map((annotation) => `--annotation=${annotation}`), ...sbomArgs(engine, sbom, sbomFlags));
2003
+ summary.labels.unshift(...addedLabels);
2004
+ summary.annotations.unshift(...addedAnnotations);
1723
2005
  const prefixTemplate = cachePrefix;
1724
2006
  if (rawCacheFrom.length || rawCacheTo.length)
1725
2007
  cachePrefix = expandPrefix(cachePrefix, vars, "cache prefix");
@@ -1744,7 +2026,48 @@ function parseBuildArgs(given, tagPrefix, cachePrefix, {
1744
2026
  summary.cacheTo.push(rewritten);
1745
2027
  }
1746
2028
  args.push(...rest);
1747
- return { args, summary };
2029
+ return { args, summary, vars };
2030
+ }
2031
+ function annotationKey(annotation, engine) {
2032
+ const key = annotation.split("=", 1)[0];
2033
+ return engine === "docker" ? key.replace(/^[a-z-]+(?:\[[^\]]*\])?(?:,[a-z-]+(?:\[[^\]]*\])?)*:/u, "") : key;
2034
+ }
2035
+ var annotates = (engine, env, buildx) => engine !== "docker" || buildx || !["0", "f", "false"].includes((env["DOCKER_BUILDKIT"] ?? "").toLowerCase());
2036
+ function indexed(platforms) {
2037
+ const normalized = platforms.map((p) => normalizePlatform(p, "docker"));
2038
+ return normalized.every((p) => p !== undefined) && new Set(normalized).size > 1;
2039
+ }
2040
+ var SBOM_PATH = "/root/buildinfo/content_manifests/sbom";
2041
+ function sbomArgs(engine, sbom, flags) {
2042
+ if (engine === "docker") {
2043
+ const asked = flags.some(([name, value]) => name === "--sbom" || name === "--attest" && csvFields(value)?.findLast((field) => fieldKey(field) === "type")?.slice("type=".length) === "sbom");
2044
+ if (!sbom || asked)
2045
+ return [];
2046
+ if (sbom === "cyclonedx")
2047
+ throw new CeError("ce: SBOM=cyclonedx is not supported by Docker's BuildKit SBOM attestation; use SBOM=spdx or SBOM=true");
2048
+ return ["--sbom=true"];
2049
+ }
2050
+ const preset = flags.findLast(([name]) => name === "--sbom")?.[1];
2051
+ const output = flags.some(([name]) => [
2052
+ "--sbom-output",
2053
+ "--sbom-image-output",
2054
+ "--sbom-purl-output",
2055
+ "--sbom-image-purl-output"
2056
+ ].includes(name));
2057
+ const used = preset ?? (sbom ? `syft-${sbom === "cyclonedx" ? "cyclonedx" : "spdx"}` : undefined);
2058
+ if (used === undefined)
2059
+ return [];
2060
+ const args = preset === undefined ? [`--sbom=${used}`] : [];
2061
+ if (!output)
2062
+ args.push(`--sbom-image-output=${SBOM_PATH}${used.endsWith("-spdx") ? ".spdx.json" : ".cdx.json"}`);
2063
+ return args;
2064
+ }
2065
+ function older(version, than) {
2066
+ const parts = /^(\d+)\.(\d+)/u.exec(version)?.slice(1).map(Number);
2067
+ if (!parts)
2068
+ return true;
2069
+ const i = parts.findIndex((n, j) => n !== than[j]);
2070
+ return i !== -1 && parts[i] < than[i];
1748
2071
  }
1749
2072
  function splitAt(value, sep) {
1750
2073
  const i = value.indexOf(sep);
@@ -1775,6 +2098,7 @@ function printBuildSummary(engine, args, summary) {
1775
2098
  out.push(` platform: ${summary.platform}`);
1776
2099
  list("build args", summary.buildArgs);
1777
2100
  list("labels", summary.labels);
2101
+ list("annotations", summary.annotations);
1778
2102
  if (summary.context)
1779
2103
  out.push(` context: ${summary.context}`);
1780
2104
  io.stderr(`${out.join(`
@@ -2091,28 +2415,60 @@ async function credentialHelper(argv) {
2091
2415
  // src/engine.ts
2092
2416
  var isEngine = (value) => ["docker", "podman", "buildah"].includes(value);
2093
2417
  var WAIT_ATTEMPTS = 30;
2094
- async function waitForEngine(engine) {
2418
+ async function waitForEngine(engine, options = []) {
2095
2419
  if (engine === "buildah")
2096
2420
  return true;
2421
+ const argv = [engine, ...options, "info"];
2422
+ let stderr = "";
2097
2423
  for (let attempt = 1;attempt <= WAIT_ATTEMPTS; attempt++) {
2098
- if (system.run([engine, "info"]).status === 0)
2424
+ const result = system.run(argv);
2425
+ if (result.status === 0)
2099
2426
  return true;
2427
+ stderr = result.stderr.trim();
2100
2428
  logError(`waiting for ${engine}... (${attempt}/${WAIT_ATTEMPTS})`);
2101
2429
  await system.sleep(1000);
2102
2430
  }
2103
2431
  logError(`no engine available: ${engine}`);
2432
+ if (stderr)
2433
+ logError(`${argv.map(shellQuote).join(" ")} last said:
2434
+ ${stderr}`);
2104
2435
  return false;
2105
2436
  }
2106
- function envFlag(name) {
2437
+ function engineVersion(engine, options = []) {
2438
+ const argv = engine === "podman" ? ["podman", ...options, "info", "--format", "{{.Version.Version}}"] : ["buildah", "--version"];
2439
+ for (let attempt = 1;; attempt++) {
2440
+ let result;
2441
+ try {
2442
+ result = system.output(argv);
2443
+ } catch {
2444
+ return;
2445
+ }
2446
+ if (result.status === 0)
2447
+ return /^(?:buildah version )?(\d+\.\d+\.\d+)\b/u.exec(result.stdout.trim())?.[1];
2448
+ if (engine === "buildah" || attempt === WAIT_ATTEMPTS)
2449
+ return;
2450
+ logError(`waiting for ${engine}... (${attempt}/${WAIT_ATTEMPTS})`);
2451
+ system.sleepSync(1000);
2452
+ }
2453
+ }
2454
+ function envFlag(name, unset = false, allowed = "true or false") {
2107
2455
  const value = (process.env[name] ?? "").toLowerCase();
2108
- if (["", "0", "false", "no"].includes(value))
2456
+ if (value === "")
2457
+ return unset;
2458
+ if (["0", "false", "no"].includes(value))
2109
2459
  return false;
2110
2460
  if (["1", "true", "yes"].includes(value))
2111
2461
  return true;
2112
- throw new CeError(`ce: ${name} must be true or false, not ${JSON.stringify(process.env[name])}`);
2462
+ throw new CeError(`ce: ${name} must be ${allowed}, not ${JSON.stringify(process.env[name])}`);
2463
+ }
2464
+ function sbomOption() {
2465
+ const value = (process.env["SBOM"] ?? "").toLowerCase();
2466
+ if (value === "spdx" || value === "cyclonedx")
2467
+ return value;
2468
+ return envFlag("SBOM", false, "true, false, spdx or cyclonedx");
2113
2469
  }
2114
2470
  function shellQuote(arg) {
2115
- return /^[A-Za-z0-9_@%+:,./-][A-Za-z0-9_@%+=:,./-]*$/u.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
2471
+ return /^[A-Za-z0-9_@%+=:,./-]+$/u.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
2116
2472
  }
2117
2473
  async function runEngine(engine, argv, tagPrefix, cachePrefix, options = {}) {
2118
2474
  const dryRun = options.dryRun ?? envFlag("CE_DRY_RUN");
@@ -2122,13 +2478,17 @@ async function runEngine(engine, argv, tagPrefix, cachePrefix, options = {}) {
2122
2478
  let summary;
2123
2479
  if (command) {
2124
2480
  const vars = new CiMetadata;
2125
- const labels = !command.tagsOnly && (options.ociLabels ?? envFlag("OCI_LABELS")) ? vars.labels() : {};
2481
+ const labels = !command.tagsOnly && (options.ociLabels ?? envFlag("OCI_LABELS", true)) ? vars.labels() : {};
2482
+ const sbom = !command.tagsOnly && sbomOption();
2126
2483
  const fromDefault = !command.tagsOnly && (options.cacheFromDefault ?? envFlag("CACHE_FROM_DEFAULT"));
2127
2484
  const parsed = parseBuildArgs(argv.slice(command.start), tagPrefix, cachePrefix, {
2128
2485
  engine,
2486
+ buildx: command.buildx,
2129
2487
  vars,
2130
2488
  labels,
2131
- defaultBranch: fromDefault ? () => vars.defaultBranch() : undefined
2489
+ sbom,
2490
+ defaultBranch: fromDefault ? () => vars.defaultBranch() : undefined,
2491
+ engineVersion: engine === "docker" ? undefined : () => engineVersion(engine, command.engineOptions)
2132
2492
  });
2133
2493
  args = [...argv.slice(0, command.start), ...parsed.args];
2134
2494
  summary = parsed.summary;
@@ -2146,7 +2506,7 @@ async function runEngine(engine, argv, tagPrefix, cachePrefix, options = {}) {
2146
2506
  logError(`Engine not found: ${engine}`);
2147
2507
  return 1;
2148
2508
  }
2149
- if (!await waitForEngine(engine))
2509
+ if (!await waitForEngine(engine, command?.engineOptions))
2150
2510
  return 1;
2151
2511
  if (summary)
2152
2512
  printBuildSummary(engine, args, summary);
@@ -2321,7 +2681,7 @@ async function login() {
2321
2681
 
2322
2682
  // src/cli.ts
2323
2683
  function helpText() {
2324
- return `Usage: ${io.argv0()} [--tag-prefix=<prefix>] [--cache-prefix=<prefix>] [--oci-labels]
2684
+ return `Usage: ${io.argv0()} [--tag-prefix=<prefix>] [--cache-prefix=<prefix>] [--no-oci-labels]
2325
2685
  [--cache-from-default] [--dry-run] [command|engine] [args]
2326
2686
 
2327
2687
  Options:
@@ -2346,10 +2706,12 @@ Options:
2346
2706
  default branch, from the cache prefix as it is there,
2347
2707
  so a new branch or PR starts from its cache.
2348
2708
  Also available via CACHE_FROM_DEFAULT=true.
2349
- --oci-labels Add org.opencontainers.image.* labels (source, revision,
2350
- version, title, description) from the Forgejo Actions
2351
- run to builds. A --label with the same key wins.
2352
- Also available via OCI_LABELS=true.
2709
+ --no-oci-labels Do not add the org.opencontainers.image.* labels and
2710
+ annotations (source, url, revision, version, title,
2711
+ description, created) that builds in a Forgejo Actions
2712
+ run otherwise get. A --label or --annotation with the
2713
+ same key wins over them.
2714
+ Also available via OCI_LABELS=false.
2353
2715
  --dry-run Print the build summary and the engine command, and
2354
2716
  exit without running it. Template errors still exit 2;
2355
2717
  a missing engine is only a warning.
@@ -2387,6 +2749,17 @@ Engines:
2387
2749
  podman [args] Passthrough to podman
2388
2750
  buildah [args] Passthrough to buildah
2389
2751
 
2752
+ Environment variables (used by builds):
2753
+ SBOM=true Add an SBOM: with Docker, BuildKit's attestation; with
2754
+ podman and buildah, syft's SPDX, written into the image
2755
+ at /root/buildinfo/content_manifests/sbom.spdx.json.
2756
+ SBOM=spdx selects the same format. SBOM=cyclonedx uses
2757
+ syft's CycloneDX with podman and buildah, written as
2758
+ sbom.cdx.json in that directory. Docker refuses it.
2759
+ An engine SBOM option you pass takes precedence.
2760
+ A podman or buildah --sbom with no output gets an image
2761
+ path matching its preset.
2762
+
2390
2763
  Environment variables (used by login):
2391
2764
  Each registry is a "group". The default group uses the bare variables
2392
2765
  below; additional registries use a __<name> suffix that ties the three
@@ -2430,6 +2803,8 @@ async function main(argv) {
2430
2803
  [cachePrefix, argv] = [a.slice("--cache-prefix=".length), argv.slice(1)];
2431
2804
  else if (a === "--oci-labels")
2432
2805
  [options.ociLabels, argv] = [true, argv.slice(1)];
2806
+ else if (a === "--no-oci-labels")
2807
+ [options.ociLabels, argv] = [false, argv.slice(1)];
2433
2808
  else if (a === "--cache-from-default")
2434
2809
  [options.cacheFromDefault, argv] = [true, argv.slice(1)];
2435
2810
  else if (a === "--dry-run")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@izykitten/ce",
3
- "version": "0.0.3-dev.5",
3
+ "version": "0.0.3-dev.7",
4
4
  "description": "Container engine wrapper: tag/cache rewriting for docker, podman and buildah, registry login, and a Forgejo OIDC Docker credential helper",
5
5
  "type": "module",
6
6
  "license": "BSD-3-Clause",
@@ -14,7 +14,8 @@
14
14
  ],
15
15
  "scripts": {
16
16
  "build": "bun build src/cli.ts --target=node --outfile=dist/ce.js",
17
- "prepack": "bun run build"
17
+ "prepack": "bun run build",
18
+ "check-engines": "bun scripts/check-engines.ts"
18
19
  },
19
20
  "engines": {
20
21
  "node": ">=22.15.0",