Skip to content

Commit 659e456

Browse files
hyperbotsxclaude
andcommitted
macos ci: optional universal (x86_64 + arm64) build
Adds MACOS_UNIVERSAL / --universal: qmake builds both slices via QMAKE_APPLE_DEVICE_ARCHS, the Go CLI tools are built per-arch and merged with lipo (Go cannot emit a universal binary), and the DMG/zip are named -macos-universal. Requires a universal Qt - the official Qt 6.2+ archives are universal, Homebrew bottles are not - and the build fails early with a clear message otherwise. A single universal DMG also removes the auto-updater trap where Apple Silicon users are offered an Intel-only build as an update. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent df5894c commit 659e456

5 files changed

Lines changed: 139 additions & 20 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: macOS universal build
2+
3+
on:
4+
push:
5+
branches: ['feature/apple-silicon-universal']
6+
workflow_dispatch:
7+
8+
env:
9+
MACOS_UNIVERSAL: 1
10+
11+
jobs:
12+
build:
13+
runs-on: macos-14
14+
steps:
15+
- uses: actions/checkout@v4
16+
with:
17+
submodules: recursive
18+
fetch-depth: 0
19+
20+
- name: Install Go (pinned)
21+
uses: actions/setup-go@v5
22+
with:
23+
go-version: '1.24'
24+
25+
- name: Install Qt 6.2.4 (pinned, official universal binaries)
26+
run: |
27+
pipx install aqtinstall
28+
aqt install-qt mac desktop 6.2.4 clang_64 -m qtwebsockets -O "$HOME/Qt"
29+
echo "QTDIR=$HOME/Qt/6.2.4/macos" >> "$GITHUB_ENV"
30+
31+
- name: Build and package (universal)
32+
run: ./scripts/macos/build-local.sh --package --universal
33+
34+
- name: Verify universal architecture
35+
run: |
36+
for bin in \
37+
build/Moolticute.app/Contents/MacOS/moolticute \
38+
build/Moolticute.app/Contents/MacOS/moolticuted \
39+
build/Moolticute.app/Contents/MacOS/cli/mc-agent \
40+
build/Moolticute.app/Contents/MacOS/cli/mc-cli
41+
do
42+
file "$bin"
43+
archs="$(lipo -archs "$bin" | tr ' ' '\n' | sort | tr '\n' ' ')"
44+
if [ "$archs" != "arm64 x86_64 " ]; then
45+
echo "ERROR: $bin is '$archs', expected both arm64 and x86_64"
46+
exit 1
47+
fi
48+
done
49+
# Qt frameworks come from the universal Qt archives; spot-check one
50+
lipo -archs build/Moolticute.app/Contents/Frameworks/QtCore.framework/Versions/A/QtCore
51+
52+
- name: Run tests
53+
working-directory: build
54+
run: ./tests/tests
55+
56+
- name: Package DMG
57+
run: ./scripts/macos/package-release.sh
58+
59+
- name: Upload universal artifacts
60+
uses: actions/upload-artifact@v4
61+
with:
62+
name: Moolticute-macos-universal
63+
path: |
64+
build/Moolticute-*-macos-universal.dmg
65+
build/Moolticute-*-macos-universal.zip
66+
if-no-files-found: error

documentation/macos_apple_silicon.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,23 @@ file build/Moolticute.app/Contents/MacOS/moolticute
3131
# Expected on Apple Silicon: Mach-O 64-bit executable arm64
3232
```
3333

34+
## Universal builds (one app for Intel and Apple Silicon)
35+
36+
`--universal` produces a single bundle carrying both `x86_64` and `arm64`
37+
slices, so one DMG serves every Mac (and the auto-updater can no longer hand
38+
Apple Silicon users an Intel-only build):
39+
40+
```bash
41+
./scripts/macos/build-local.sh --package --universal
42+
./scripts/macos/package-release.sh # -> Moolticute-<version>-macos-universal.dmg
43+
```
44+
45+
This requires a **universal Qt**: the official Qt 6.2+ macOS archives (e.g.
46+
installed with `aqtinstall`) are universal, while Homebrew's bottles are
47+
single-arch. The build stops with a clear message if Qt is not universal.
48+
`mc-agent`/`mc-cli` are built for both architectures and merged with `lipo`,
49+
since Go cannot emit a universal binary directly. CI: `macos-universal.yml`.
50+
3451
## Upgrading from the Intel build
3552

3653
Quit Moolticute (and make sure the daemon is gone) **before** replacing the

scripts/ci/osx/env.sh

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,33 @@ OSX_SCRIPTDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1010
export MC_AGENT_REF="${MC_AGENT_REF:-30e78a776e161dda12d803b58de14ea96c739b94}"
1111
export MC_CLI_REF="${MC_CLI_REF:-8d2654a36cb895efbe9b753e0a006d0b44f11d9e}"
1212

13-
export MACOS_ARCH="$(uname -m)"
14-
15-
case "$MACOS_ARCH" in
16-
arm64)
17-
export GOARCH=arm64
18-
export MC_CLI_TOOLS_ARCH=arm64
19-
;;
20-
x86_64)
21-
export GOARCH=amd64
22-
export MC_CLI_TOOLS_ARCH=amd64
23-
;;
24-
*)
25-
echo "Unsupported macOS architecture: $MACOS_ARCH"
26-
exit 1
27-
;;
28-
esac
13+
# Set MACOS_UNIVERSAL=1 to build a universal (x86_64 + arm64) bundle.
14+
# This needs a universal Qt: the official Qt 6.2+ macOS archives (as
15+
# installed by aqtinstall) are universal, Homebrew's single-arch bottles
16+
# are not.
17+
export MACOS_UNIVERSAL="${MACOS_UNIVERSAL:-0}"
18+
19+
if [ "$MACOS_UNIVERSAL" = "1" ]; then
20+
export MACOS_ARCH="universal"
21+
export MC_CLI_TOOLS_ARCH=universal
22+
else
23+
export MACOS_ARCH="$(uname -m)"
24+
25+
case "$MACOS_ARCH" in
26+
arm64)
27+
export GOARCH=arm64
28+
export MC_CLI_TOOLS_ARCH=arm64
29+
;;
30+
x86_64)
31+
export GOARCH=amd64
32+
export MC_CLI_TOOLS_ARCH=amd64
33+
;;
34+
*)
35+
echo "Unsupported macOS architecture: $MACOS_ARCH"
36+
exit 1
37+
;;
38+
esac
39+
fi
2940

3041
detect_qtdir() {
3142
if [ -n "${QTDIR:-}" ] && { [ -x "$QTDIR/bin/qmake" ] || [ -x "$QTDIR/bin/qmake6" ]; }; then
@@ -91,7 +102,15 @@ build_mc_cli_tools() {
91102
(
92103
cd "$tmp/$tool"
93104
git checkout --quiet "$ref"
94-
GOOS=darwin GOARCH="$GOARCH" go build -o "$dest/$tool" .
105+
if [ "$MACOS_UNIVERSAL" = "1" ]; then
106+
# Go cannot emit a universal binary directly: build each
107+
# slice, then merge them with lipo.
108+
GOOS=darwin GOARCH=amd64 go build -o "$tmp/$tool-amd64" .
109+
GOOS=darwin GOARCH=arm64 go build -o "$tmp/$tool-arm64" .
110+
lipo -create -output "$dest/$tool" "$tmp/$tool-amd64" "$tmp/$tool-arm64"
111+
else
112+
GOOS=darwin GOARCH="$GOARCH" go build -o "$dest/$tool" .
113+
fi
95114
) || return 1
96115
[ -f "$dest/$tool" ] || return 1
97116
chmod +x "$dest/$tool"

scripts/macos/build-local.sh

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ PACKAGE=0
1717
while [ $# -gt 0 ]; do
1818
case "$1" in
1919
--package) PACKAGE=1 ;;
20+
--universal) export MACOS_UNIVERSAL=1 ;;
2021
-h|--help)
21-
echo "Usage: $0 [--package]"
22+
echo "Usage: $0 [--package] [--universal]"
23+
echo " --universal build a universal x86_64+arm64 bundle"
24+
echo " (requires a universal Qt, i.e. the official"
25+
echo " Qt 6.2+ archives, not Homebrew's bottles)"
2226
exit 0
2327
;;
2428
*)
@@ -47,7 +51,16 @@ mkdir -p build
4751
cd build
4852

4953
export PATH="$QTDIR/bin:$PATH"
50-
"$QMAKE_BIN" ../Moolticute.pro
54+
if [ "$MACOS_UNIVERSAL" = "1" ]; then
55+
if ! lipo -archs "$QTDIR/lib/QtCore.framework/QtCore" 2>/dev/null | grep -q x86_64; then
56+
echo "Qt at $QTDIR is not universal - a universal build needs the"
57+
echo "official Qt archives (aqtinstall), not Homebrew's single-arch bottles."
58+
exit 1
59+
fi
60+
"$QMAKE_BIN" ../Moolticute.pro QMAKE_APPLE_DEVICE_ARCHS="x86_64 arm64"
61+
else
62+
"$QMAKE_BIN" ../Moolticute.pro
63+
fi
5164
make -j"$(sysctl -n hw.ncpu)"
5265

5366
if [ "$PACKAGE" -eq 0 ]; then

scripts/macos/package-release.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ set -eo pipefail
99
SCRIPTDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1010
REPO_ROOT="$(cd "$SCRIPTDIR/../.." && pwd)"
1111
APP=Moolticute
12-
ARCH="$(uname -m)"
12+
# env.sh resolves MACOS_ARCH, which is "universal" when MACOS_UNIVERSAL=1
13+
# is set in the environment, otherwise the host architecture.
14+
# shellcheck source=../ci/osx/env.sh
15+
source "$SCRIPTDIR/../ci/osx/env.sh"
16+
ARCH="$MACOS_ARCH"
1317

1418
if [ -n "${1:-}" ]; then
1519
VERSION="$1"

0 commit comments

Comments
 (0)