Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/release-auto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,17 @@ jobs:
linux_cross: true
macos_cross: false

# FastLED/fbuild#XXX (TBD): aarch64-apple-darwin now
# cross-compiles from the linux runner via soldr + cargo-zigbuild
# + soldr's managed Apple SDK. This drops one macos-latest
# runner-minute from every release and removes the only
# mac-host dependency from the matrix.
- target: aarch64-apple-darwin
runner: macos-latest
runner: ubuntu-latest
binary_ext: ""
linux_cross: false
macos_cross: false
mac_cross_linux: true

- target: x86_64-pc-windows-msvc
runner: windows-latest
Expand All @@ -182,6 +188,7 @@ jobs:
binary_ext: ${{ matrix.binary_ext }}
linux_cross: ${{ matrix.linux_cross }}
macos_cross: ${{ matrix.macos_cross }}
mac_cross_linux: ${{ matrix.mac_cross_linux == true }}
ref: ${{ needs.prepare.outputs.release_ref }}

publish:
Expand Down
47 changes: 45 additions & 2 deletions .github/workflows/template_native_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ on:
type: boolean
default: false
description: "macOS x86_64 cross-compilation on ARM runner"
mac_cross_linux:
required: false
type: boolean
default: false
description: "Cross-compile macOS target from Linux via soldr + cargo-zigbuild + managed Apple SDK"
ref:
required: false
type: string
Expand Down Expand Up @@ -136,10 +141,25 @@ jobs:
echo "PYO3_CROSS_PYTHON_VERSION=3.13" >> "$GITHUB_ENV"
echo "PYO3_CROSS_PYTHON_IMPLEMENTATION=CPython" >> "$GITHUB_ENV"

# Linux → mac cross — soldr fetches the Apple SDK ahead of the
# cargo invocation so the SDKROOT export is debuggable independent
# of the build itself.
- name: Prepare Apple SDK (Linux → mac cross)
if: inputs.mac_cross_linux
shell: bash
run: soldr prepare --target ${{ inputs.target }}

- name: Build release binaries
shell: bash
run: |
if [ "${{ runner.os }}" = "Linux" ] && [[ "${{ inputs.target }}" == *-pc-windows-msvc ]]; then
if [ "${{ inputs.mac_cross_linux }}" = "true" ]; then
# Linux → aarch64-apple-darwin: zigbuild + soldr-managed SDK.
# Identical recipe to ci/docker-mac-arm64-cross/build.sh, which
# validates this same path against a minimal ubuntu:24.04 image.
soldr cargo zigbuild --release --target ${{ inputs.target }} \
-p fbuild-cli \
-p fbuild-daemon
elif [ "${{ runner.os }}" = "Linux" ] && [[ "${{ inputs.target }}" == *-pc-windows-msvc ]]; then
cargo xwin build --release --target ${{ inputs.target }} \
-p fbuild-cli \
-p fbuild-daemon
Expand Down Expand Up @@ -173,7 +193,19 @@ jobs:
shell: bash
run: |
PYTHON_TARGET_DIR="target/python-extension"
if [ "${{ runner.os }}" = "Linux" ] && [[ "${{ inputs.target }}" == *-pc-windows-msvc ]]; then
if [ "${{ inputs.mac_cross_linux }}" = "true" ]; then
# Linux → aarch64-apple-darwin PyO3 extension. zigbuild +
# soldr-managed SDK + PYO3_NO_PYTHON=1 (PyO3's abi3-py310
# feature lets us skip host interpreter discovery on cross
# builds). Lands at
# target/python-extension/aarch64-apple-darwin/release/lib_native.dylib
# — same path the macOS native lane produced before, so the
# stage step is unchanged.
PYO3_NO_PYTHON=1 soldr cargo zigbuild --release \
--target-dir "${PYTHON_TARGET_DIR}" \
--target ${{ inputs.target }} -p fbuild-python \
--features extension-module
elif [ "${{ runner.os }}" = "Linux" ] && [[ "${{ inputs.target }}" == *-pc-windows-msvc ]]; then
PYO3_NO_PYTHON=1 cargo xwin build --release \
--target-dir "${PYTHON_TARGET_DIR}" \
--target ${{ inputs.target }} -p fbuild-python \
Expand Down Expand Up @@ -220,6 +252,11 @@ jobs:
"${PYTHON_TARGET_DIR}/release/_native.dll"; do
[ -f "$ext_src" ] && cp "$ext_src" staging/_native.pyd && break
done
elif [ "${{ inputs.mac_cross_linux }}" = "true" ]; then
# Linux → mac cross: dylib lands under the apple-darwin
# target dir (no manylinux floor, unlike the Linux PyO3 lane).
ext_src="${PYTHON_TARGET_DIR}/${{ inputs.target }}/release/lib_native.dylib"
[ -f "$ext_src" ] && cp "$ext_src" staging/_native.abi3.so
elif [ "${{ runner.os }}" = "Linux" ]; then
TARGET="${{ inputs.target }}"
ARCH="${TARGET%%-*}"
Expand Down Expand Up @@ -248,6 +285,12 @@ jobs:
llvm-strip staging/fbuild || true
llvm-strip staging/fbuild-daemon || true
llvm-strip staging/_native.abi3.so 2>/dev/null || true
elif [ "${{ inputs.mac_cross_linux }}" = "true" ]; then
# Linux host `strip` is binutils — it would silently corrupt
# a Mach-O. Skip stripping; ~30 MB is acceptable for release
# artifacts. A future iteration can wire `llvm-strip` from
# ziglang's bundled tools.
true
elif [[ "${{ inputs.target }}" == *-pc-windows-msvc ]]; then
true
elif command -v strip &> /dev/null; then
Expand Down
86 changes: 86 additions & 0 deletions ci/docker-mac-arm64-cross/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Simulate the FastLED/fbuild GitHub Actions release runner for the
# `aarch64-apple-darwin` lane on **Linux x86_64**, with no Apple-side
# tooling and no pre-installed Rust toolchain. Soldr bootstraps
# everything else.
#
# This is the "NO CHEATING" proof: we ship a vanilla Debian base + the
# absolute minimum apt deps, and soldr brings rustup, the pinned
# toolchain, zig, the Apple SDK, and `cargo-zigbuild` — exactly the
# story soldr#997 promised for cross-compile completeness.
#
# Build:
# docker build -f ci/docker-mac-arm64-cross/Dockerfile -t fbuild-mac-arm64-cross .
#
# Run (from repo root):
# docker run --rm -v "$PWD:/src" -w /src fbuild-mac-arm64-cross \
# ./ci/docker-mac-arm64-cross/build.sh

# ubuntu:24.04 is exactly what `ubuntu-latest` is on GitHub Actions
# right now (the runners moved off 22.04 in 2025-Q4). Picking it
# matches the constraint "simulate the GHA runner" verbatim and
# avoids glibc-floor friction: the published `soldr` linux-gnu
# binary requires glibc 2.39, which matches 24.04. Older bases
# (debian bookworm, ubuntu 22.04) would silently downgrade us to
# either the musl binary or a soldr that 404's its own libc.
FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive

# Minimum apt set — no rust, no zig, no clang. Soldr fetches all of those.
# curl + ca-certificates → soldr's HTTP fetches
# git → cargo fetch from git deps
# xz-utils + bzip2 → tarball extraction for various release archives
# python3 + python3-pip → soldr is shipped as a pip wheel
# python3-venv → uv venvs
# build-essential → cc/ld for any build script that needs them
# pkg-config → many *-sys crates poke pkg-config at build time
# file → final verification of the Mach-O output
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates curl git xz-utils bzip2 zstd \
python3 python3-pip python3-venv \
build-essential pkg-config file \
&& rm -rf /var/lib/apt/lists/*

# uv for python ops (CLAUDE.md mandate).
RUN curl -LsSf https://astral.sh/uv/install.sh | sh \
&& cp /root/.local/bin/uv /usr/local/bin/ \
&& uv venv --seed /opt/soldr-venv

# Soldr binary tarball from GitHub Releases — the published soldr wheel
# is tagged `manylinux_2_39` (glibc 2.39 floor, ubuntu-24.04+) so a
# `pip install soldr` on Debian bookworm (glibc 2.36) silently falls back
# to an unrelated squatted `soldr-0.1.0` placeholder package. The tarball
# is built against glibc 2.17 (manylinux_2_17 floor) so it runs on every
# remotely-recent distro. Issue tracked at zackees/soldr#1005 (forthcoming).
ARG SOLDR_VERSION=0.7.59
RUN mkdir -p /opt/soldr-bin \
&& curl -fsSL \
"https://github.com/zackees/soldr/releases/download/v${SOLDR_VERSION}/soldr-v${SOLDR_VERSION}-x86_64-unknown-linux-gnu.tar.zst" \
| zstd -d --stdout \
| tar -xf - -C /opt/soldr-bin \
&& ls -la /opt/soldr-bin

RUN for bin in soldr cargo-chef crgx zccache zccache-daemon zccache-fp; do \
[ -f "/opt/soldr-bin/$bin" ] && cp "/opt/soldr-bin/$bin" "/usr/local/bin/$bin" && chmod +x "/usr/local/bin/$bin"; \
done

# Pre-seed soldr's home so its first-run setup is non-interactive +
# the rustup auto-bootstrap has a writable HOME to drop ~/.cargo/ into.
ENV HOME=/root
RUN soldr --version

# cargo-zigbuild from PyPI — same approach fbuild's existing
# template_native_build.yml takes for its linux musl + linux PyO3 cross
# steps (see #331). Ships zig in the venv.
RUN /opt/soldr-venv/bin/pip install --no-cache-dir cargo-zigbuild ziglang \
&& ln -s /opt/soldr-venv/bin/cargo-zigbuild /usr/local/bin/cargo-zigbuild

# Soldr-controlled defaults:
# - SOLDR_APPLE_SDK_VERSION pins the Apple SDK soldr fetches at
# `cargo zigbuild --target *-apple-darwin` time.
# - SOLDR_TRUST_MODE=permissive accepts unpinned managed downloads
# (the Phase A catalogue blobs are sha-pinned by soldr itself).
ENV SOLDR_APPLE_SDK_VERSION=11.3 \
SOLDR_TRUST_MODE=permissive \
CARGO_TERM_COLOR=always
38 changes: 38 additions & 0 deletions ci/docker-mac-arm64-cross/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# `ci/docker-mac-arm64-cross/` — Linux → `aarch64-apple-darwin` simulator

Reproduces fbuild's `aarch64-apple-darwin` release lane on **Linux x86_64**
with **no Apple-side tooling and no pre-installed Rust toolchain**. Soldr
bootstraps rustup, the pinned 1.94.1 channel, zig, the Apple SDK, and
`cargo-zigbuild` from a vanilla `ubuntu:24.04` base.

This is the proof-of-concept that lets fbuild's release pipeline drop
its `macos-latest` runner for the mac-arm64 lane and replace it with the
same `ubuntu-latest` lane every other target already uses.

## Build + run

```bash
# From the fbuild repo root:
docker build -f ci/docker-mac-arm64-cross/Dockerfile -t fbuild-mac-arm64-cross .
docker run --rm -v "$PWD:/src" -w /src fbuild-mac-arm64-cross \
bash ci/docker-mac-arm64-cross/build.sh
```

`build.sh` produces three artifacts under `$PWD/staging/` and asserts via
`file(1)` that each is a `Mach-O 64-bit arm64` binary — the
`NO CHEATING` gate. If anything regressed and we accidentally produced
the host Linux binary, `file` reports `ELF 64-bit LSB pie executable,
x86-64` and the script fails loudly.

## Why `ubuntu:24.04` (not `debian:bookworm-slim`)

Soldr's published Linux-gnu binary requires `glibc 2.39`. Debian
bookworm-slim ships `glibc 2.36`, so `soldr --version` 404's its own
libc and a `pip install soldr` falls back to a squatted `soldr-0.1.0`
placeholder on PyPI. `ubuntu:24.04` matches what GitHub Actions'
`ubuntu-latest` resolves to today, so this is also the most-faithful
GHA-runner simulation.

The friction is tracked at zackees/soldr — Phase B-2 should lower the
soldr Linux-gnu binary to a `manylinux_2_17` floor so any distro from
the last ~5 years can run it directly.
74 changes: 74 additions & 0 deletions ci/docker-mac-arm64-cross/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# Run inside the `fbuild-mac-arm64-cross` docker image (see Dockerfile).
# Cross-compiles fbuild + fbuild-daemon + the PyO3 extension to
# aarch64-apple-darwin using soldr + cargo-zigbuild + soldr's Apple SDK.
#
# Output layout (in $PWD/staging):
# fbuild ← Mach-O 64-bit executable arm64
# fbuild-daemon ← Mach-O 64-bit executable arm64
# _native.abi3.so ← Mach-O 64-bit dylib arm64 (PyO3 extension)

set -euo pipefail

TARGET="aarch64-apple-darwin"
STAGING="${STAGING:-$PWD/staging}"
mkdir -p "$STAGING"

echo "::group::soldr version + rust toolchain"
soldr --version
# `soldr toolchain ensure` installs rustup (if missing) + the pinned
# channel from rust-toolchain.toml + the requested target stdlib + the
# components soldr's bootstrap matrix needs.
soldr toolchain ensure
soldr rustup target add "$TARGET"
echo "::endgroup::"

echo "::group::cargo-zigbuild + soldr-managed apple SDK"
which cargo-zigbuild
cargo-zigbuild --version
# Force a pre-fetch of the Apple SDK before the real build so a
# slow / failing SDK download is debuggable separately from the cargo
# build itself.
soldr prepare --target "$TARGET"
echo "::endgroup::"

echo "::group::Build fbuild-cli + fbuild-daemon"
soldr cargo zigbuild --release --target "$TARGET" \
-p fbuild-cli -p fbuild-daemon
echo "::endgroup::"

echo "::group::Build fbuild-python PyO3 extension"
PYO3_NO_PYTHON=1 soldr cargo zigbuild --release \
--target-dir target/python-extension \
--target "$TARGET" -p fbuild-python \
--features extension-module
echo "::endgroup::"

echo "::group::Stage + verify artifacts"
cp "target/$TARGET/release/fbuild" "$STAGING/fbuild"
cp "target/$TARGET/release/fbuild-daemon" "$STAGING/fbuild-daemon"

EXT_SRC="target/python-extension/$TARGET/release/lib_native.dylib"
if [ ! -f "$EXT_SRC" ]; then
echo "ERROR: PyO3 extension not found at $EXT_SRC" >&2
find target/python-extension -name "lib_native.*" -o -name "_native.*" >&2 || true
exit 1
fi
cp "$EXT_SRC" "$STAGING/_native.abi3.so"

# Verify the output is actually a Mach-O ARM64 binary — this is the
# `NO CHEATING` gate. If file(1) reports anything other than
# `Mach-O 64-bit ... arm64` for all three artifacts, the cross-compile
# silently produced the host binary instead and we must fail loudly.
for f in fbuild fbuild-daemon _native.abi3.so; do
desc="$(file "$STAGING/$f")"
echo " $desc"
if ! echo "$desc" | grep -qE "Mach-O.*(arm64|aarch64)"; then
echo "ERROR: $f is not Mach-O arm64 — got: $desc" >&2
exit 1
fi
done
echo "::endgroup::"

echo "All three artifacts are valid Mach-O arm64. Staging dir: $STAGING"
ls -lh "$STAGING"
Loading