Files
meshcore-analyzer/tests/unit/test-dockerfile-buildplatform.js
JadeandClaude d1bf4ca042 fix(docker): build natively on arm64 hosts (#2359)
Building the image natively on an arm64 machine (a Raspberry Pi 4 or 5
on 64-bit OS, Apple silicon) fails at the first `RUN` of the build
stage:

```
=> ERROR [builder  2/40] RUN apk add --no-cache curl xz && ...
0.841 exec /bin/sh: exec format error
```

**Cause:** the Dockerfile declares `ARG BUILDPLATFORM=linux/amd64`. A
default on that ARG overrides the value BuildKit sets for the machine
doing the build, so an arm64 host pulls the amd64 `golang` image and
can't run it. CI didn't catch it because its arm64 image is cross-built
on an amd64 runner, where `linux/amd64` was correct anyway.

**Fix:** declare `ARG BUILDPLATFORM` without a default. BuildKit, the
default builder since Docker 23 and what buildx uses, fills it in on
every host. amd64 builds and the CI cross-build are unchanged.

**Workaround on current master:** `docker build --build-arg
BUILDPLATFORM=linux/arm64 .`

**Test:** `tests/unit/test-dockerfile-buildplatform.js` fails on the old
Dockerfile and passes on this one. It checks three things:
- `BUILDPLATFORM` has no default.
- The Go builder stays pinned to `$BUILDPLATFORM`.
- No stage hard-codes a platform.

It's wired into `test-all.sh`.

Found while building the image on a Raspberry Pi running Raspberry Pi OS
Trixie (arm64) with Docker 29.


---

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-31 12:13:29 +01:00

45 lines
1.8 KiB
JavaScript

/* test-dockerfile-buildplatform.js — the build stage runs on the building
* machine's own architecture.
*
* BuildKit sets BUILDPLATFORM to the host doing the build. Giving the global
* ARG a default (it used to say linux/amd64) overrides that, so a native build
* on an arm64 host — a Raspberry Pi, Apple silicon — pulled the amd64 Go image
* and failed at the first RUN with "exec format error". CI never saw it: its
* arm64 image is cross-built on an amd64 runner, where the default happened to
* be right.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const assert = require('assert');
const dockerfile = fs.readFileSync(path.join(__dirname, '..', '..', 'Dockerfile'), 'utf8');
const lines = dockerfile.split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#'));
let passed = 0, failed = 0;
function test(name, fn) {
try { fn(); passed++; console.log(` ✅ ${name}`); }
catch (e) { failed++; console.log(` ❌ ${name}: ${e.message}`); }
}
console.log('\n=== Dockerfile: build platform ===');
test('BUILDPLATFORM is never given a default', () => {
const withDefault = lines.filter((l) => /^ARG\s+BUILDPLATFORM\s*=/.test(l));
assert.deepStrictEqual(withDefault, [], 'found: ' + withDefault.join(' | '));
});
test('the Go builder stage is pinned to BUILDPLATFORM (native, no QEMU)', () => {
const from = lines.find((l) => /^FROM\b.*golang:.*\bAS\s+builder\b/i.test(l));
assert.ok(from, 'builder FROM line not found');
assert.ok(/--platform=\$BUILDPLATFORM\b/.test(from), from);
});
test('no stage hard-codes a platform', () => {
const pinned = lines.filter((l) => /^FROM\b/.test(l) && /--platform=(?!\$)/.test(l));
assert.deepStrictEqual(pinned, [], 'found: ' + pinned.join(' | '));
});
console.log(`\n${passed} passed, ${failed} failed`);
if (failed) process.exit(1);