Skip to content

Commit c886b64

Browse files
committed
Add release workflow producing macOS DMGs for C++ and Python builds
A tag push matching `v*` (or a manual workflow_dispatch) triggers a release build on macos-latest. The job: - Builds the C++ targets with -DBUILD_TESTS=ON and runs mrtk_tests - Runs pytest on the Python regression suite - Assembles MRTKFeatureTool.app for the C++ GUI, using dylibbundler to embed Homebrew dylibs (wxWidgets, libarchive) so the .app runs on a clean Mac without brew - Bundles dylibs for mrtk_cli too - Builds a Python .app via PyInstaller with a CI-generated spec that finds customtkinter through importlib instead of a hardcoded conda path - Packages each into a UDZO DMG via hdiutil with an Applications symlink for drag-to-install - On tag pushes only, publishes a GitHub Release attaching both DMGs (manual runs only upload artifacts, no release) Also commits mrtk_icon.icns (previously gitignored) so the .app bundles can ship with the project icon.
1 parent ae4c6cc commit c886b64

3 files changed

Lines changed: 201 additions & 1 deletion

File tree

.github/workflows/release.yml

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
name: Release
2+
3+
# Triggers:
4+
# 1. Push a tag like `v1.0.0` — automatic release.
5+
# 2. Manual run via the "Run workflow" button (no release created).
6+
on:
7+
push:
8+
tags:
9+
- 'v*'
10+
workflow_dispatch:
11+
12+
jobs:
13+
release-macos:
14+
name: Build macOS DMGs and publish release
15+
runs-on: macos-latest
16+
permissions:
17+
contents: write # Required by softprops/action-gh-release to create the release.
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- name: Resolve release version
22+
id: ver
23+
run: |
24+
if [ "${{ github.event_name }}" = "push" ]; then
25+
echo "version=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
26+
else
27+
echo "version=dev-$(date +%Y%m%d-%H%M%S)" >> "$GITHUB_OUTPUT"
28+
fi
29+
30+
- name: Install Homebrew build dependencies
31+
run: |
32+
brew update
33+
brew install wxwidgets libarchive dylibbundler
34+
35+
- uses: actions/setup-python@v5
36+
with:
37+
python-version: '3.12'
38+
39+
- name: Install Python dependencies
40+
run: |
41+
python -m pip install --upgrade pip
42+
pip install pyinstaller customtkinter requests packaging pytest
43+
44+
# ---------------- C++ build + tests ----------------
45+
- name: Configure CMake
46+
run: cmake -S . -B build -DBUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Release
47+
48+
- name: Build C++ targets
49+
run: cmake --build build -j
50+
51+
- name: Run C++ regression tests
52+
run: ./build/mrtk_tests
53+
54+
- name: Run Python regression tests
55+
run: python -m pytest tests/test_core_logic.py -v
56+
57+
# ---------------- C++ .app bundle ----------------
58+
- name: Assemble C++ .app bundle
59+
run: |
60+
set -euo pipefail
61+
VERSION="${{ steps.ver.outputs.version }}"
62+
APP="dist/cpp/MRTKFeatureTool.app"
63+
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" "$APP/Contents/libs"
64+
cp build/mrtk_gui "$APP/Contents/MacOS/MRTKFeatureTool"
65+
cp build/mrtk_cli "dist/cpp/mrtk_cli"
66+
if [ -f mrtk_icon.icns ]; then
67+
cp mrtk_icon.icns "$APP/Contents/Resources/AppIcon.icns"
68+
fi
69+
cat > "$APP/Contents/Info.plist" <<PLIST
70+
<?xml version="1.0" encoding="UTF-8"?>
71+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
72+
<plist version="1.0">
73+
<dict>
74+
<key>CFBundleExecutable</key><string>MRTKFeatureTool</string>
75+
<key>CFBundleIdentifier</key><string>com.mrtk.featuretool.cpp</string>
76+
<key>CFBundleName</key><string>MRTKFeatureTool</string>
77+
<key>CFBundlePackageType</key><string>APPL</string>
78+
<key>CFBundleShortVersionString</key><string>${VERSION}</string>
79+
<key>CFBundleVersion</key><string>1</string>
80+
<key>CFBundleIconFile</key><string>AppIcon</string>
81+
<key>NSHighResolutionCapable</key><true/>
82+
</dict>
83+
</plist>
84+
PLIST
85+
# Bundle non-system dylibs (Homebrew wxWidgets, libarchive). dylibbundler
86+
# rewrites the install names to @executable_path-relative so the .app is
87+
# self-contained on a clean Mac.
88+
dylibbundler -od -b -of \
89+
-x "$APP/Contents/MacOS/MRTKFeatureTool" \
90+
-d "$APP/Contents/libs/" \
91+
-p "@executable_path/../libs/"
92+
# CLI uses libcurl/libarchive — fix it up too, dylibs in adjacent app/libs.
93+
mkdir -p dist/cpp/libs
94+
dylibbundler -od -b -of \
95+
-x dist/cpp/mrtk_cli \
96+
-d dist/cpp/libs/ \
97+
-p "@executable_path/libs/" || true
98+
99+
- name: Create C++ DMG
100+
run: |
101+
set -euo pipefail
102+
VERSION="${{ steps.ver.outputs.version }}"
103+
mkdir -p artifacts
104+
# Stage everything that should land in the DMG.
105+
STAGE=$(mktemp -d)
106+
cp -R dist/cpp/MRTKFeatureTool.app "$STAGE/"
107+
cp dist/cpp/mrtk_cli "$STAGE/"
108+
[ -d dist/cpp/libs ] && cp -R dist/cpp/libs "$STAGE/" || true
109+
ln -sf /Applications "$STAGE/Applications"
110+
hdiutil create -volname "MRTKFeatureTool-CPP" \
111+
-srcfolder "$STAGE" \
112+
-ov -format UDZO \
113+
"artifacts/MRTKFeatureTool-CPP-${VERSION}.dmg"
114+
115+
# ---------------- Python .app via PyInstaller ----------------
116+
- name: Write portable PyInstaller spec
117+
run: |
118+
cat > MRTKFeatureTool-CI.spec <<'SPEC'
119+
# -*- mode: python ; coding: utf-8 -*-
120+
# Auto-generated for CI. Finds customtkinter via importlib instead of
121+
# hardcoding a conda path so the build is portable across machines.
122+
import os
123+
import customtkinter
124+
125+
ctk_path = os.path.dirname(customtkinter.__file__)
126+
icon = 'mrtk_icon.icns' if os.path.exists('mrtk_icon.icns') else None
127+
128+
a = Analysis(
129+
['gui_tool.py'],
130+
pathex=[],
131+
binaries=[],
132+
datas=[(ctk_path, 'customtkinter/')],
133+
hiddenimports=[],
134+
hookspath=[],
135+
hooksconfig={},
136+
runtime_hooks=[],
137+
excludes=[],
138+
noarchive=False,
139+
optimize=0,
140+
)
141+
pyz = PYZ(a.pure)
142+
exe = EXE(
143+
pyz, a.scripts, a.binaries, a.datas, [],
144+
name='MRTKFeatureTool',
145+
debug=False,
146+
bootloader_ignore_signals=False,
147+
strip=False,
148+
upx=False,
149+
upx_exclude=[],
150+
runtime_tmpdir=None,
151+
console=False,
152+
disable_windowed_traceback=False,
153+
argv_emulation=False,
154+
target_arch=None,
155+
codesign_identity=None,
156+
entitlements_file=None,
157+
icon=icon,
158+
)
159+
app = BUNDLE(
160+
exe,
161+
name='MRTKFeatureTool.app',
162+
icon=icon,
163+
bundle_identifier='com.mrtk.featuretool.python',
164+
)
165+
SPEC
166+
167+
- name: Build Python .app
168+
run: |
169+
pyinstaller --clean --noconfirm MRTKFeatureTool-CI.spec
170+
ls -la dist/
171+
172+
- name: Create Python DMG
173+
run: |
174+
set -euo pipefail
175+
VERSION="${{ steps.ver.outputs.version }}"
176+
STAGE=$(mktemp -d)
177+
cp -R dist/MRTKFeatureTool.app "$STAGE/"
178+
ln -sf /Applications "$STAGE/Applications"
179+
hdiutil create -volname "MRTKFeatureTool-Python" \
180+
-srcfolder "$STAGE" \
181+
-ov -format UDZO \
182+
"artifacts/MRTKFeatureTool-Python-${VERSION}.dmg"
183+
184+
- name: Upload DMG artifacts (always)
185+
if: always()
186+
uses: actions/upload-artifact@v4
187+
with:
188+
name: mrtk-dmgs-${{ steps.ver.outputs.version }}
189+
path: artifacts/*.dmg
190+
retention-days: 30
191+
192+
- name: Publish GitHub Release
193+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
194+
uses: softprops/action-gh-release@v2
195+
with:
196+
files: artifacts/*.dmg
197+
name: ${{ steps.ver.outputs.version }}
198+
tag_name: ${{ github.ref_name }}
199+
generate_release_notes: true
200+
draft: false
201+
prerelease: false

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,4 @@ __pycache__/
4141
.pytest_cache/
4242
dist/*
4343
*.spec
44-
*.icns
4544
*.dmg

mrtk_icon.icns

465 KB
Binary file not shown.

0 commit comments

Comments
 (0)