Skip to content

docker CLI: build the Windows arm64 docker.exe upstream does not ship (#450) - #460

Merged
zcsizmadia merged 3 commits into
mainfrom
arm64-docker-cli
Sep 21, 2026
Merged

zcsizmadia merged 3 commits into
mainfrom
arm64-docker-cli

Conversation

@zcsizmadia

Copy link
Copy Markdown
Collaborator

The build and the release plumbing for #450. The manifest still carries arm64 as a placeholder — that entry needs a published dockercli- release to point at, which needs this on main first. Same order #388 followed.

The gap

Upstream publishes nothing here. download.docker.com's static tree has exactly one directory, x86_64/; docker/cli attaches no release assets at all. So skrog cli install on Windows on ARM laid down compose, buildx and the credential helper — all of which do have real arm64 builds — and skipped the one command anybody types.

The build

third_party/docker-cli/ — docker/cli at a tag pinned to its dereferenced commit, in the same pinned Go toolchain as the engine, CGO_ENABLED=0.

Cross-compiled, and that is not a compromise the way it would be for the engine: this is go build with GOARCH set against a fully vendored pure-Go tree, not a C-linked one that needs boot-testing on native hardware.

Two details that cost time:

  • docker/cli ships vendor.mod/vendor.sum, not go.mod/go.sum — deliberately, so the repo cannot be consumed as a library. Their own scripts rename them; so does this.
  • PlatformName is left unset. Upstream's ldflags stamp it Docker Engine - Community. This is not Docker's build, and putting their build string on it would be the one genuinely misleading thing this script could do. Version and commit are set, so docker version reports something true rather than dev.

The architecture is asserted twice

In build.sh, and again in the workflow outside it:

PE machine type: 0xaa64 (ARM64)

A cross-compile that silently produced x86-64 would pass every other check here and fail only on a user's ARM machine — the exact failure #388 was about, and the one thing a build on an x86-64 runner cannot feel. The second check exists so a bug in the script's own parsing can't pass its own test.

Verified against a real local build before writing any of the CI: PE offset 128, machine bytes 64aa, 28.3 MB, from commit 4a63305d.

amd64 stays Docker's

Deliberate, and now enforced by a test. Those bytes are verifiable by anyone against the pinned digest with no reference to Skrog; a binary we build can only be checked against us; and building it too would put every user behind our build rather than only the arm64 users who have no alternative.

Not about code signing — Docker's published Windows CLI is NotSigned too, measured on the binary skrog cli install places. There's no signature being preserved on one side and lost on the other.

docs/docker-cli.md gets a table of where each of the four binaries comes from, the reasoning, and the exit condition: when upstream ships an arm64 build, both point at Docker again and third_party/docker-cli/ is deleted.

Policy

#450 asked that the "ship CLI binaries unmodified" line in PLAN §09 be decided rather than slid past. It is amended in this PR, in place, dated: building from Apache-2.0 source in our own CI is the treatment that same line already prescribes for the engine, and it is the opposite of the repackaging the trademark concern is about — nothing is redistributed from Docker's channels and the binary carries no Docker build string.

A hole a third tag namespace opens

release.yml and rootfs.yml excluded each other by name — !startsWith(tag, 'rootfs-'). That is an open list: a dockercli- tag would have attached skrog.exe to a docker CLI release, because nothing said no.

All three guards now test for their own prefix positively, which the next namespace cannot break. rootfs.yml's build job also stops rebuilding the engine — twice, ~6 min each — for every release that isn't its own.

CI's shellcheck ran a hardcoded path list; it now runs every .sh in the tree, matching what scripts/lint.ps1 already did locally.

Verified

go test ./..., go vet across all build tags, gofmt, govulncheck, shellcheck, scripts/lint.ps1, docs rebuild, check-links.ps1. The arm64 build itself runs on this PR — dockercli.yml triggers on any PR touching third_party/docker-cli/**.

Next

Cut dockercli-v29.8.1, then a one-line PR adds the URL and checksum to internal/dockercli/manifest.json and Windows on ARM has a complete toolchain.

…#450)

The build and the release plumbing. The manifest still carries arm64 as a
placeholder -- that entry needs a published dockercli- release to point at,
which needs this on main first, the same order #388 followed.

Upstream publishes nothing here: download.docker.com's static tree has
exactly one directory, x86_64/, and docker/cli attaches no release assets at
all. So `skrog cli install` on Windows on ARM laid down compose, buildx and
the credential helper -- all of which DO have real arm64 builds -- and
skipped the one command anybody types.

third_party/docker-cli/ builds it: docker/cli at a tag pinned to its
dereferenced commit, in the pinned Go toolchain the engine uses, cross
compiled with CGO off. Cross-compiling is not a compromise here the way it
would be for the engine -- this is `go build` with GOARCH set against fully
vendored pure Go, not a C-linked tree that needs boot-testing.

Two details that cost time to find:

- docker/cli ships vendor.mod and vendor.sum rather than go.mod and go.sum,
  deliberately, so it cannot be consumed as a library. Their own scripts
  rename them; so does this.
- The upstream ldflags set PlatformName to "Docker Engine - Community". It is
  left UNSET here, and that is the one thing in this change that is about
  honesty rather than mechanics: this is not Docker's build, and stamping
  their build string on it would be the single genuinely misleading thing the
  script could do. Version and commit are set, so `docker version` reports
  something true rather than "dev".

The PE machine type is asserted twice -- in build.sh and again in the
workflow, outside it. A cross-compile that silently produced x86-64 would
pass every other check and fail only on a user's ARM machine, which is the
exact failure #388 was about, and it is the one thing a build on an x86-64
runner cannot feel. Verified against a real local build: PE offset 128,
machine bytes 64aa, 28.3 MB.

amd64 stays Docker's own bytes. Anyone can re-download that zip and check it
against the pinned digest without reference to Skrog; a binary we build can
only be checked against us, and building it too would put every user behind
our build rather than only the arm64 users with no alternative. It is not
about code signing -- Docker's published Windows CLI is NotSigned, measured
on the binary `skrog cli install` places. A test fails if the amd64 URL ever
stops pointing at download.docker.com.

This amends the "ship CLI binaries unmodified" line in PLAN §09, which #450
asked be decided rather than slid past. Building from Apache-2.0 source in
our own CI is the treatment that same line already prescribes for the engine,
and it is the opposite of repackaging Docker's distribution.

Also closes a hole a third tag namespace opens. release.yml and rootfs.yml
excluded each other by name -- "not rootfs-" -- which is an open list: a
dockercli- tag would have attached skrog.exe to a docker CLI release because
nothing said no. All three guards now test for their own prefix positively,
which the next namespace cannot break. rootfs.yml's build job also stops
rebuilding the engine, twice, for every release that is not its own.

CI's shellcheck ran a hardcoded list of paths; it now runs every .sh in the
tree, matching what scripts/lint.ps1 already did locally.
`chmod +x` on a Windows working tree does not reach the index, so it landed
as 100644 and the workflow died with "Permission denied" (exit 126) before
running a line of it. guest/rootfs/build.sh is 100755, which is why nothing
else noticed.

`git update-index --chmod=+x`, which is the part that actually records it.
Not a change to the build -- a fact about it that is worth writing down,
because it is the strongest answer to "why should I trust a binary you
built".

The output has no timestamp and no build host in it (BuildTime is
deliberately unset), so the same commit produces the same bytes anywhere.
Confirmed rather than assumed: my local build and this PR's CI build of
docker/cli 4a63305d both came out at

  910087c0d9a80ac0e7802f8c48b98cdc7fd890c78e0dab5f0472bee856ac50a0
  28,319,232 bytes

Different machines, different kernels, same binary. The SLSA attestation says
we built it; reproducibility means nobody has to take our word for what from.
@zcsizmadia
zcsizmadia merged commit cdf1609 into main Sep 21, 2026
16 checks passed
@zcsizmadia
zcsizmadia deleted the arm64-docker-cli branch September 21, 2026 18:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant