Skip to content

unix: support building Linux x86-64 from macOS aarch64#1056

Open
indygreg wants to merge 1 commit into
mainfrom
gps-macos-linux-x86-64-cross-compile
Open

unix: support building Linux x86-64 from macOS aarch64#1056
indygreg wants to merge 1 commit into
mainfrom
gps-macos-linux-x86-64-cross-compile

Conversation

@indygreg

Copy link
Copy Markdown
Collaborator

Docker on aarch64 macOS will automagically virtualize x86-64 containers if containers are spawned with platform=linux/amd64. Performance of spawned containers is a bit slower than native, but not horrible. This functionality means it is viable to develop Linux x86-64 from modern Apple hardware.

This commit teaches the build system to support cross-compiling Linux x86-64 from macOS aarch64.

Implementing this wasn't too difficult: we need to pass platform into Docker's APIs for building and creating containers. We need to teach code to resolve the effective host platform when this scenario is detected. And we need to advertise support for cross-compiling in the targets.yml file.

In case you are wondering, yes, a similar solution could be employed for Linux too by using emulation. But this requires Docker be configured to support emulation, which isn't as common. Rosetta on macOS "just works" and is therefore the lowest hanging fruit to implement.

@indygreg
indygreg requested a review from geofft March 26, 2026 10:20
Comment thread cpython-unix/targets.yml
host_platforms:
- linux_x86_64
# Rosetta doesn't support AVX-512. So we cannot run x86-64-v4 binaries
# under Rosetta. But they can build correctly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be moved to x86_64_v4-unknown-linux-musl

@jjhelmus

Copy link
Copy Markdown
Contributor

I tried this out locally and after building for aarch64-unknown-linux-gnu and removing the build directory a follow up build for x86_64-unknown-linux-gnu fails when building the image-gcc target.

For some reason armhf deb packages are being installed in the container and uname -a reports Linux 9ceedc289281 6.10.14-linuxkit #1 SMP Tue Oct 14 07:32:13 UTC 2025 armv7l GNU/Linux.

I think this is caused by docker-py not fully respecting the platform argument, see docker/docker-py#2990. There may be a workaround that involves removing cached images.

@indygreg

Copy link
Copy Markdown
Collaborator Author

That's a fun quirk. I'll take a look and try to concoct a workaround. I know a bit about Docker/Buildkit internals and may be able to devise something non-obvious.

@indygreg

Copy link
Copy Markdown
Collaborator Author

Yeah, I bet the code in docker buildx build handles this platform issue. Whereas the raw Docker image build API (which abstracts away the BuildKit bits) doesn't. We may want to switch to the CLI command for image building. I may do that as its own PR.

@indygreg

Copy link
Copy Markdown
Collaborator Author

Oh, I think it is sillier than that. We're adding platform when cross compiling on Linux. That forces Docker to run an image from a non-native arch, invalidating assumptions that Linux images are always for the native arch.

Docker on aarch64 macOS will automagically virtualize x86-64 containers
if containers are spawned with `platform=linux/amd64`. Performance of
spawned containers is a bit slower than native, but not horrible. This
functionality means it is viable to develop Linux x86-64 from modern
Apple hardware.

This commit teaches the build system to support cross-compiling
Linux x86-64 from macOS aarch64.

Implementing this wasn't too difficult: we need to pass `platform` into
Docker's APIs for building and creating containers. We need to teach
code to resolve the effective host platform when this scenario is
detected. And we need to advertise support for cross-compiling in the
`targets.yml` file.

In case you are wondering, yes, a similar solution could be employed for
Linux too by using emulation. But this requires Docker be configured to
support emulation, which isn't common. Rosetta on macOS "just works" and
is therefore the lowest hanging fruit to implement.
@indygreg
indygreg force-pushed the gps-macos-linux-x86-64-cross-compile branch from d1fddea to affe720 Compare March 27, 2026 07:56
@jjhelmus

Copy link
Copy Markdown
Contributor

I'm still getting armhf Debian packages installed into the image-build and image-gcc containers when building with --target-triple x86_64-unknown-linux-gnu on a macOS host. I may have some armhf debian containers in my cache from debugging issues with that platform but the cache should not be effecting the platforms selection.

@indygreg

Copy link
Copy Markdown
Collaborator Author

I'm still getting armhf Debian packages installed into the image-build and image-gcc containers when building with --target-triple x86_64-unknown-linux-gnu on a macOS host. I may have some armhf debian containers in my cache from debugging issues with that platform but the cache should not be effecting the platforms selection.

Right - our build system is supposed to load the local image tags into Docker and then execute those images directly by specifying their SHA-256 digest. The only time we would be loading incorrect images is if the docker build equivalent that we're running is producing an invalid image and we're loading that.

Try removing build/image-*.tar files and see if you can reproduce?

@jjhelmus

Copy link
Copy Markdown
Contributor

I tracked down why the wrong platform was being used on my Mac. client.api.build() uses Docker’s legacy builder rather than BuildKit. As described in docker/docker-py#2990, the legacy builder can incorrectly reuse a cached base-image variant despite an explicit platform argument. This can cause an image for the wrong architecture to be produced.

A small reproducer is:

#!/usr/bin/env -S uv run --no-dev
import io
import docker

IMAGE = "debian@sha256:32ad5050caffb2c7e969dac873bce2c370015c2256ff984b70c1c08b3a2816a0"
client = docker.from_env(timeout=600)

print("Seeding the local cache with linux/arm/v7...")
for _ in client.api.pull(IMAGE, platform="linux/arm/v7", stream=True, decode=True): pass

print("Requesting linux/amd64 through docker-py...")
fh = io.BytesIO(f"FROM {IMAGE}\nRUN uname -m && dpkg --print-architecture\n".encode())
for event in client.api.build(fileobj=fh, platform="linux/amd64", decode=True):
    if "stream" in event:
        print(event["stream"], end="")

On my machine, this prints armv7l and armhf despite requesting linux/amd64.

A workaround is to invoke docker buildx build in a subprocess so the image is built with BuildKit, which correctly resolves the requested platform. I’ve applied that workaround on top of this branch and submitted it as #1179.

jjhelmus added a commit that referenced this pull request Jul 23, 2026
Update to #1056 to use `docker buildx` for reliable handeling of the
`platform` argument.

---------

Co-authored-by: Gregory Szorc <gregory.szorc@gmail.com>
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.

2 participants