@izykitten/ce 0.0.3-dev.6 → 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.
- package/README.md +39 -10
- package/dist/ce.js +486 -111
- package/package.json +4 -3
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:
|
|
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
|
|
@@ -55,7 +59,7 @@ Guards decide from the run's metadata, so they need it too: outside a Forgejo Ac
|
|
|
55
59
|
|
|
56
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
|
|
@@ -137,25 +141,25 @@ RUN xcaddy build v${CADDY_VERSION}
|
|
|
137
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.
|
|
138
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.
|
|
139
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.
|
|
140
|
-
- **`$` 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}`).
|
|
141
|
-
- **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.
|
|
142
|
-
- **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.
|
|
143
147
|
|
|
144
148
|
**What ce refuses**, with exit 2 and the reason:
|
|
145
149
|
|
|
146
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`.
|
|
147
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.
|
|
148
|
-
- **A value ce cannot know:** a reference to a
|
|
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`.
|
|
149
153
|
- **Instructions ce does not read:** an `ARG` or `ENV` name with something to expand, such as `ENV $NAME=x`, or with flags.
|
|
150
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.
|
|
151
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.
|
|
152
|
-
- **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
|
|
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).
|
|
153
157
|
|
|
154
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`.
|
|
155
159
|
|
|
156
160
|
### Checking a template
|
|
157
161
|
|
|
158
|
-
`--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:
|
|
159
163
|
|
|
160
164
|
```sh
|
|
161
165
|
GITHUB_REF=refs/tags/v1.2.3 GITHUB_SERVER_URL=https://code.example GITHUB_REPOSITORY=org/app \
|
|
@@ -164,7 +168,30 @@ GITHUB_REF=refs/tags/v1.2.3 GITHUB_SERVER_URL=https://code.example GITHUB_REPOSI
|
|
|
164
168
|
|
|
165
169
|
### OCI labels
|
|
166
170
|
|
|
167
|
-
|
|
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.
|
|
168
195
|
|
|
169
196
|
## The credential helper
|
|
170
197
|
|
|
@@ -184,7 +211,7 @@ These behave the same under Node and Bun, and [test/contract.test.ts](test/contr
|
|
|
184
211
|
|
|
185
212
|
The wording of messages is not part of that and may change.
|
|
186
213
|
|
|
187
|
-
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.
|
|
188
215
|
|
|
189
216
|
Compared with the Python version:
|
|
190
217
|
|
|
@@ -203,3 +230,5 @@ bun test # builds dist/, then unit tests plus the contract and process
|
|
|
203
230
|
bun check
|
|
204
231
|
bun run build # dist/ce.js, which the bin scripts load; npm pack runs it
|
|
205
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
|
-
|
|
113
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
636
|
-
globals.set(
|
|
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)
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
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) :
|
|
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 (
|
|
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 (
|
|
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) &&
|
|
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
|
|
971
|
+
function loadContainerfile(files, buildArg) {
|
|
859
972
|
const docker = files.engine === "docker";
|
|
860
|
-
|
|
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
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1043
|
-
|
|
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(
|
|
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
|
-
|
|
1317
|
-
|
|
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
|
|
1562
|
-
buildah: new Set(BUILDAH_VALUES
|
|
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
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
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
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
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
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
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 === "
|
|
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
|
|
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 =
|
|
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
|
|
1720
|
-
const
|
|
1721
|
-
|
|
1722
|
-
|
|
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,25 +2415,57 @@ 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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
2471
|
return /^[A-Za-z0-9_@%+=:,./-]+$/u.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
|
|
@@ -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
|
-
|
|
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
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
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.
|
|
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",
|
|
@@ -30,4 +31,4 @@
|
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@types/bun": "1.4.3"
|
|
32
33
|
}
|
|
33
|
-
}
|
|
34
|
+
}
|