Skip to content

Commit c0ead20

Browse files
andyclaude
andcommitted
Merge wip/133-macos-package: macOS .app/.dmg for Apple silicon (macOS 11+) and Intel (macOS 10.15+), dependencies built from source for the minimum, minos check per arch (#133)
The Organizer message-pane height change is kept to macOS; Linux is unchanged. Catalina VM: all 34 executables of the Intel app bind with DYLD_BIND_AT_LAUNCH=1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PFPK14wBcLD1s4dYUowpP4
2 parents 3519a6e + c8d69d2 commit c0ead20

13 files changed

Lines changed: 561 additions & 17 deletions

File tree

‎.github/workflows/build.yml‎

Lines changed: 117 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,21 @@ jobs:
266266

267267
macos:
268268
if: ${{ github.event_name != 'workflow_dispatch' || inputs.only == '' || inputs.only == 'macos' }}
269-
name: macOS (Homebrew)
270-
runs-on: macos-latest
269+
name: macOS (${{ matrix.arch }})
270+
runs-on: ${{ matrix.os }}
271+
strategy:
272+
fail-fast: false
273+
matrix:
274+
include:
275+
- { os: macos-latest, arch: arm64, target: "11.0", experimental: false }
276+
# Catalina: the oldest macOS many 2012-2014 Intel Macs can run.
277+
- { os: macos-15-intel, arch: x86_64, target: "10.15", experimental: true }
278+
env:
279+
# Oldest macOS the app is built for (#133). Our code and every
280+
# bundled library are built to this; the minimum-macOS step fails
281+
# the job on any Mach-O above it.
282+
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.target }}
283+
ECCE_DEPS: /Users/runner/ecce-deps
271284
# Bounded so a hung job cannot hold a runner slot for GitHub's
272285
# 6-hour default; see the concurrency note above.
273286
timeout-minutes: 90
@@ -278,22 +291,50 @@ jobs:
278291
# here to track the gap with a real, current compile log instead of
279292
# nothing -- it's expected to fail until #3 gets real porting work,
280293
# not a sign the workflow itself is broken.
281-
continue-on-error: true
294+
# x86_64 may fail until it is proven; arm64 must be green.
295+
continue-on-error: ${{ matrix.experimental }}
282296
steps:
283297
- uses: actions/checkout@v4
284298

285-
- name: Install build dependencies
286-
run: |
287-
brew install cmake ninja wxwidgets xerces-c mosquitto gcc libssh pkgconf
288-
# The vendored Inventor core needs GLX and pre-C++17 code that
289-
# Apple's compiler rejects; the Coin build (#166) skips it.
290-
brew install coin3d || { echo "::error::Homebrew has no coin3d formula"; exit 1; }
299+
- name: Install build tools
300+
# Only tools: the libraries come from build-deps.sh, because
301+
# Homebrew bottles are built for the runner's own macOS.
302+
run: brew install cmake ninja pkgconf gcc bash
303+
304+
- name: Cache libraries built for macOS ${{ env.MACOSX_DEPLOYMENT_TARGET }}
305+
id: deps
306+
uses: actions/cache/restore@v4
307+
with:
308+
path: ${{ env.ECCE_DEPS }}
309+
key: macos-deps-${{ matrix.arch }}-${{ env.MACOSX_DEPLOYMENT_TARGET }}-${{ hashFiles('packaging/macos/build-deps.sh') }}
310+
311+
- name: Build libraries (wxWidgets, Coin, Xerces, Mosquitto, libssh, OpenSSL)
312+
if: steps.deps.outputs.cache-hit != 'true'
313+
run: packaging/macos/build-deps.sh "$ECCE_DEPS"
314+
315+
- name: Save the libraries
316+
if: steps.deps.outputs.cache-hit != 'true'
317+
uses: actions/cache/save@v4
318+
with:
319+
path: ${{ env.ECCE_DEPS }}
320+
key: ${{ steps.deps.outputs.cache-primary-key }}
291321

292322
- name: Configure
293323
# ECCE_HOME_DIR/ECCE_WRAPPER_DESTINATION point the install at a
294324
# staging tree inside the workspace for the run step below.
325+
# Homebrew's prefixes are ignored so nothing is picked up from
326+
# them by accident.
295327
run: |
296-
cmake -G Ninja -B build-cmake -DECCE_USE_COIN=ON -DCMAKE_PREFIX_PATH="$(brew --prefix)" \
328+
export PATH="$ECCE_DEPS/bin:$PATH"
329+
export PKG_CONFIG_LIBDIR="$ECCE_DEPS/lib/pkgconfig"
330+
cmake -G Ninja -B build-cmake -DECCE_USE_COIN=ON \
331+
-DCMAKE_OSX_DEPLOYMENT_TARGET="$MACOSX_DEPLOYMENT_TARGET" \
332+
-DCMAKE_PREFIX_PATH="$ECCE_DEPS" \
333+
-DCMAKE_IGNORE_PREFIX_PATH="/opt/homebrew;/usr/local" \
334+
-DCMAKE_MAKE_PROGRAM="$(command -v ninja)" -DPKG_CONFIG_EXECUTABLE="$(command -v pkg-config)" \
335+
-DCMAKE_Fortran_COMPILER="$(ls $(brew --prefix)/bin/gfortran-* | head -1)" \
336+
-DFREETYPE_UNQUALIFIED_INCLUDE_DIR="$ECCE_DEPS/include/freetype2/freetype" \
337+
-DFREETYPE_ROOT_INCLUDE_DIR="$ECCE_DEPS/include/freetype2" \
297338
-DECCE_HOME_DIR="$PWD/stage/ecce" -DECCE_WRAPPER_DESTINATION="$PWD/stage/bin" \
298339
-DCMAKE_INSTALL_PREFIX="$PWD/stage/ecce"
299340
@@ -308,14 +349,78 @@ jobs:
308349
continue-on-error: true
309350
timeout-minutes: 25
310351
run: |
311-
brew install bash || true
352+
export PATH="$ECCE_DEPS/sbin:$ECCE_DEPS/bin:$PATH"
312353
cmake --install build-cmake
313354
tests/macos/run.sh stage macos-run
314355
cat macos-run/summary.txt >> "$GITHUB_STEP_SUMMARY"
315356
316357
- uses: actions/upload-artifact@v4
317358
if: always()
318359
with:
319-
name: macos-run
360+
name: macos-run-${{ matrix.arch }}
320361
path: macos-run/
321362
if-no-files-found: ignore
363+
364+
# ECCE.app in a .dmg (#133), then the start test again on a copy of
365+
# the app taken out of the mounted image, away from the build tree.
366+
# The two steps below fail the job when they fail. The job itself
367+
# is continue-on-error (the native build is still experimental), so
368+
# the workflow stays green but the job and these steps show red and
369+
# the summary says FAILED.
370+
- name: Package ECCE.app
371+
run: |
372+
export PATH="$ECCE_DEPS/bin:$PATH"
373+
packaging/macos/make-app.sh stage dist
374+
375+
# Oldest macOS the bundle can run on (#133): the step fails when any
376+
# Mach-O needs more than the deployment target or is not this runner's arch.
377+
- name: Minimum macOS of every Mach-O in the app
378+
run: |
379+
mkdir -p minos
380+
app=$(mktemp -d)
381+
mnt=$(mktemp -d)
382+
hdiutil attach dist/ECCE-*.dmg -mountpoint "$mnt" -nobrowse -quiet
383+
cp -R "$mnt/ECCE.app" "$app/"
384+
hdiutil detach "$mnt" -quiet
385+
python3 packaging/macos/minos.py "$app/ECCE.app" minos/minos.txt \
386+
--floor "$MACOSX_DEPLOYMENT_TARGET" --arch "${{ matrix.arch }}" > /dev/null || rc=$?
387+
echo "runner: $(sw_vers -productVersion) $(uname -m)" >> minos/minos.txt
388+
{ echo '### Minimum macOS (ECCE.app)'; echo '```';
389+
sed -n '/^Mach-O files/,$p' minos/minos.txt; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
390+
exit ${rc:-0}
391+
392+
- name: Run the packaged app
393+
if: always() && hashFiles('dist/*.dmg') != ''
394+
timeout-minutes: 25
395+
run: |
396+
pkill -f "$PWD/stage" || true
397+
mnt=$(mktemp -d)
398+
hdiutil attach dist/ECCE-*.dmg -mountpoint "$mnt" -nobrowse -quiet
399+
appdir=$(mktemp -d)
400+
cp -R "$mnt/ECCE.app" "$appdir/"
401+
hdiutil detach "$mnt" -quiet
402+
# The build tree and the staging prefix must not be needed.
403+
mv build-cmake build-cmake.gone
404+
mv stage stage.gone
405+
ECCE_APP="$appdir/ECCE.app" tests/macos/run.sh "$appdir/ECCE.app/Contents/Resources" macos-app-run
406+
cat macos-app-run/summary.txt >> "$GITHUB_STEP_SUMMARY"
407+
408+
- name: Summary on failure
409+
if: failure()
410+
run: echo '### macOS packaging FAILED (see the failed step above)' >> "$GITHUB_STEP_SUMMARY"
411+
412+
- uses: actions/upload-artifact@v4
413+
if: always()
414+
with:
415+
name: ECCE-macos-dmg-${{ matrix.arch }}
416+
path: |
417+
dist/*.dmg
418+
minos/minos.txt
419+
if-no-files-found: ignore
420+
421+
- uses: actions/upload-artifact@v4
422+
if: always()
423+
with:
424+
name: macos-app-run-${{ matrix.arch }}
425+
path: macos-app-run/
426+
if-no-files-found: ignore

‎CMakeLists.txt‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,9 +338,12 @@ target_link_libraries(eccewxgui PRIVATE ${wxWidgets_LIBRARIES})
338338
# ewxApp.C installs a g_log_set_writer_func() filter for a known-benign
339339
# wxWidgets/GTK3 backend warning (see the comment there) -- needs GLib's
340340
# own headers directly, which wxWidgets_INCLUDE_DIRS doesn't expose.
341-
pkg_check_modules(GLIB2 REQUIRED glib-2.0)
342-
target_include_directories(eccewxgui PRIVATE ${GLIB2_INCLUDE_DIRS})
343-
target_link_libraries(eccewxgui PRIVATE ${GLIB2_LIBRARIES})
341+
# GTK only: wx's macOS and Windows ports do not use GLib.
342+
if(NOT APPLE)
343+
pkg_check_modules(GLIB2 REQUIRED glib-2.0)
344+
target_include_directories(eccewxgui PRIVATE ${GLIB2_INCLUDE_DIRS})
345+
target_link_libraries(eccewxgui PRIVATE ${GLIB2_LIBRARIES})
346+
endif()
344347
345348
# ewxRaiseWindow() (ewxWindowUtils.C) calls gtk_window_present*() and, on an
346349
# X11 display only, gdk_x11_get_server_time() (#120); needs gtk+-3.0 headers
@@ -535,6 +538,11 @@ function(ecce_fortran_app name)
535538
# doesn't know that and refuses to build with a shared output).
536539
set_target_properties(${name} PROPERTIES
537540
Fortran_MODULE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${name}.mod)
541+
# The Fortran runtime comes with the compiler, not the OS: link it in
542+
# so the app bundles no libgfortran and its minimum macOS stays ours.
543+
if(APPLE)
544+
target_link_options(${name} PRIVATE -static-libgfortran -static-libgcc -static-libquadmath)
545+
endif()
538546
endfunction()
539547
540548
set(_sym_dir ${CMAKE_SOURCE_DIR}/src/apps/symmetry)

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ your `PATH`.
132132
goal ([#133](https://github.com/FriendsofECCE/ECCE/issues/133),
133133
[#232](https://github.com/FriendsofECCE/ECCE/issues/232)); the state of
134134
the macOS build is tracked in #133. Until then, ECCE runs on Linux only.
135+
CI builds an experimental `ECCE.app` in a `.dmg` (workflow artifact
136+
`ECCE-macos-dmg`). It is signed ad hoc only, so macOS refuses it at
137+
first: right-click the app and choose Open, or run
138+
`xattr -dr com.apple.quarantine /Applications/ECCE.app`.
135139

136140
### 2. Create your account
137141

‎packaging/macos/Info.plist.in‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleName</key><string>ECCE</string>
6+
<key>CFBundleDisplayName</key><string>ECCE</string>
7+
<key>CFBundleIdentifier</key><string>org.friendsofecce.ecce</string>
8+
<key>CFBundleExecutable</key><string>ecce</string>
9+
<key>CFBundleIconFile</key><string>ecce</string>
10+
<key>CFBundlePackageType</key><string>APPL</string>
11+
<key>CFBundleShortVersionString</key><string>@VERSION@</string>
12+
<key>CFBundleVersion</key><string>@VERSION@</string>
13+
<key>LSMinimumSystemVersion</key><string>12.0</string>
14+
<key>NSHighResolutionCapable</key><true/>
15+
<key>LSApplicationCategoryType</key><string>public.app-category.education</string>
16+
</dict>
17+
</plist>

‎packaging/macos/build-deps.sh‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/bin/bash
2+
# build-deps.sh <prefix>: the libraries ECCE.app bundles, built from source
3+
# for MACOSX_DEPLOYMENT_TARGET (default 11.0; CI uses 10.15 on Intel) and the machine's own
4+
# architecture. Homebrew bottles carry the runner's macOS version as their
5+
# minimum, so they cannot give an app that runs on older systems (#133).
6+
# All libraries are shared and install with absolute install names, which
7+
# bundle_libs.py then rewrites. CI caches <prefix> keyed on this file.
8+
set -euo pipefail
9+
P=$1
10+
mkdir -p "$P"
11+
P=$(cd "$P" && pwd)
12+
export MACOSX_DEPLOYMENT_TARGET=${MACOSX_DEPLOYMENT_TARGET:-11.0}
13+
JOBS=$(sysctl -n hw.ncpu)
14+
ARCH=$(uname -m)
15+
W=$(mktemp -d)
16+
trap 'rm -rf "$W"' EXIT
17+
cd "$W"
18+
19+
fetch() { # url dir
20+
echo "== fetch $1"
21+
curl -fsSL --retry 3 "$1" -o src.tar
22+
mkdir -p "$2"
23+
tar -xf src.tar -C "$2" --strip-components=1
24+
rm -f src.tar
25+
}
26+
cm() { # srcdir extra cmake args...
27+
local s=$1; shift
28+
cmake -G Ninja -S "$s" -B "$s/build" -DCMAKE_BUILD_TYPE=Release \
29+
-DCMAKE_INSTALL_PREFIX="$P" -DCMAKE_PREFIX_PATH="$P" \
30+
-DCMAKE_OSX_DEPLOYMENT_TARGET="$MACOSX_DEPLOYMENT_TARGET" \
31+
-DCMAKE_INSTALL_NAME_DIR="$P/lib" -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
32+
-DBUILD_SHARED_LIBS=ON "$@"
33+
cmake --build "$s/build" -j "$JOBS"
34+
cmake --install "$s/build"
35+
}
36+
37+
# openssl: libssh and the broker
38+
fetch https://github.com/openssl/openssl/releases/download/openssl-3.5.4/openssl-3.5.4.tar.gz openssl
39+
( cd openssl
40+
./Configure "darwin64-$( [ "$ARCH" = arm64 ] && echo arm64 || echo x86_64 )-cc" shared \
41+
--prefix="$P" --openssldir="$P/ssl" --libdir=lib no-tests no-docs \
42+
"-mmacosx-version-min=$MACOSX_DEPLOYMENT_TARGET"
43+
make -j "$JOBS" build_libs && make install_dev install_runtime_libs ) > openssl.log 2>&1 \
44+
|| { tail -50 openssl.log; exit 1; }
45+
46+
fetch https://github.com/DaveGamble/cJSON/archive/refs/tags/v1.7.19.tar.gz cjson
47+
cm cjson -DENABLE_CJSON_TEST=OFF -DENABLE_CJSON_UNINSTALL=OFF
48+
49+
fetch https://mosquitto.org/files/source/mosquitto-2.1.2.tar.gz mosquitto
50+
cm mosquitto -DOPENSSL_ROOT_DIR="$P" -DWITH_TESTS=OFF -DWITH_WEBSOCKETS=OFF \
51+
-DWITH_CLIENTS=OFF -DWITH_PLUGINS=OFF -DWITH_DOCS=OFF -DWITH_LTO=OFF \
52+
-DWITH_CTRL_SHELL=OFF -DWITH_SRV=OFF \
53+
-DCMAKE_DISABLE_FIND_PACKAGE_argon2=ON
54+
55+
fetch https://www.libssh.org/files/0.11/libssh-0.11.3.tar.xz libssh
56+
cm libssh -DOPENSSL_ROOT_DIR="$P" -DWITH_EXAMPLES=OFF \
57+
-DUNIT_TESTING=OFF -DCLIENT_TESTING=OFF -DWITH_GSSAPI=OFF
58+
59+
fetch https://archive.apache.org/dist/xerces/c/3/sources/xerces-c-3.3.0.tar.xz xerces
60+
cm xerces -Dnetwork=OFF
61+
62+
fetch https://github.com/freetype/freetype/archive/refs/tags/VER-2-13-3.tar.gz freetype
63+
cm freetype -DFT_DISABLE_BZIP2=ON -DFT_DISABLE_PNG=ON -DFT_DISABLE_HARFBUZZ=ON \
64+
-DFT_DISABLE_BROTLI=ON
65+
66+
# ECCE links -ljpeg itself (texture images in the viewer)
67+
fetch https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/3.1.2/libjpeg-turbo-3.1.2.tar.gz jpeg
68+
cm jpeg -DENABLE_STATIC=OFF -DWITH_TURBOJPEG=OFF -DWITH_SIMD=OFF
69+
70+
fetch https://github.com/coin3d/coin/releases/download/v4.0.10/coin-4.0.10-src.tar.gz coin
71+
cm coin -DCOIN_BUILD_SHARED_LIBS=ON -DCOIN_BUILD_TESTS=OFF -DHAVE_SOUND=OFF \
72+
-DCOIN_HAVE_JAVASCRIPT=OFF
73+
74+
# wxWidgets 3.3: 3.2 still links the AGL framework, gone from the macOS 26
75+
# SDK. The image libraries are wx's own copies.
76+
fetch https://github.com/wxWidgets/wxWidgets/releases/download/v3.3.3/wxWidgets-3.3.3.tar.bz2 wx
77+
( cd wx
78+
./configure --prefix="$P" --enable-shared --with-cocoa \
79+
--with-opengl --with-macosx-version-min="$MACOSX_DEPLOYMENT_TARGET" \
80+
--with-libpng=builtin --with-libjpeg=builtin --with-libtiff=builtin \
81+
--with-zlib=builtin --with-expat=builtin --with-regex=builtin \
82+
--disable-webview --disable-mediactrl --without-libcurl --without-libwebp
83+
make -j "$JOBS" && make install ) > wx.log 2>&1 \
84+
|| { tail -60 wx.log; exit 1; }
85+
echo "deps installed in $P"

0 commit comments

Comments
 (0)