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
13 changes: 11 additions & 2 deletions .github/workflows/build-nuitka.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,25 @@ jobs:
if: runner.os == 'macOS'
run: |
brew install create-dmg
SUFFIX=$(python3 -c "from build_conf import get_platform_suffix; print(get_platform_suffix())")
for app in dist_nuitka/*.app; do
[ -d "$app" ] || continue
name=$(basename "$app" .app)
dmg_name="${name}-${SUFFIX}"
# create-dmg uses source folder contents as DMG root,
# so stage the .app inside a temporary directory
STAGING=$(mktemp -d)
cp -R "$app" "$STAGING/"
create-dmg \
--volname "$name" \
--app-drop-link 600 185 \
--sandbox-safe \
"dist_nuitka/${name}.dmg" \
"$app"
"dist_nuitka/${dmg_name}.dmg" \
"$STAGING"
rm -rf "$STAGING"
done
# Clean up temporary read-write DMG files left by create-dmg
rm -f dist_nuitka/rw.*.dmg

- name: Upload Artifacts (Windows)
if: runner.os == 'Windows'
Expand Down
13 changes: 11 additions & 2 deletions .github/workflows/build-pyinstaller.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,25 @@ jobs:
if: runner.os == 'macOS'
run: |
brew install create-dmg
SUFFIX=$(python3 -c "from build_conf import get_platform_suffix; print(get_platform_suffix())")
for app in dist_pyinstaller/*.app; do
[ -d "$app" ] || continue
name=$(basename "$app" .app)
dmg_name="${name}-${SUFFIX}"
# create-dmg uses source folder contents as DMG root,
# so stage the .app inside a temporary directory
STAGING=$(mktemp -d)
cp -R "$app" "$STAGING/"
create-dmg \
--volname "$name" \
--app-drop-link 600 185 \
--sandbox-safe \
"dist_pyinstaller/${name}.dmg" \
"$app"
"dist_pyinstaller/${dmg_name}.dmg" \
"$STAGING"
rm -rf "$STAGING"
done
# Clean up temporary read-write DMG files left by create-dmg
rm -f dist_pyinstaller/rw.*.dmg

- name: Upload Artifacts (Windows)
if: runner.os == 'Windows'
Expand Down
3 changes: 3 additions & 0 deletions build_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
DIST_PYINSTALLER = "dist_pyinstaller"
DIST_NUITKA = "dist_nuitka"

# macOS app bundle display name (CFBundleName / .app directory name)
MACOS_APP_NAME = "SessionPrep"

# Platform Logic
_PLATFORM_SUFFIXES = {
"Windows": "win",
Expand Down
16 changes: 12 additions & 4 deletions build_nuitka.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import shutil
import subprocess
import argparse
from build_conf import TARGETS, BASE_DIR, DIST_NUITKA
from build_conf import TARGETS, BASE_DIR, DIST_NUITKA, MACOS_APP_NAME

def _check_dependencies(target_key):
"""Ensure required packages for the target are installed."""
Expand Down Expand Up @@ -61,6 +61,7 @@ def run_nuitka(target_key, clean=False):
# GUI on macOS: produce a proper .app bundle instead of a bare onefile binary
cmd.remove("--onefile")
cmd.append("--macos-create-app-bundle")
cmd.append(f"--macos-app-name={MACOS_APP_NAME}")
icon_path = target.get("icon")
if icon_path and os.path.isfile(icon_path):
cmd.append(f"--macos-app-icon={icon_path}")
Expand Down Expand Up @@ -103,9 +104,16 @@ def run_nuitka(target_key, clean=False):
print(f" Renaming {bin_path} -> {output_exe}")
os.rename(bin_path, output_exe)

# On macOS GUI, output is a .app bundle (directory), not a single file
app_bundle = os.path.join(dist_dir, f"{os.path.splitext(target['name'])[0]}.app")
if sys.platform == "darwin" and not target["console"] and os.path.isdir(app_bundle):
# On macOS GUI, output is a .app bundle (directory), not a single file.
# Nuitka names the bundle from the script name, not --output-filename.
# Rename it to MACOS_APP_NAME for a clean user-facing name.
script_stem = os.path.splitext(os.path.basename(target["script"]))[0]
nuitka_bundle = os.path.join(dist_dir, f"{script_stem}.app")
app_bundle = os.path.join(dist_dir, f"{MACOS_APP_NAME}.app")
if sys.platform == "darwin" and not target["console"] and os.path.isdir(nuitka_bundle):
if os.path.exists(app_bundle):
shutil.rmtree(app_bundle)
os.rename(nuitka_bundle, app_bundle)
print(f"[SUCCESS] Built {app_bundle}")
elif os.path.isfile(output_exe):
print(f"[SUCCESS] Built {output_exe}")
Expand Down
41 changes: 30 additions & 11 deletions build_pyinstaller.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import sys
import platform

from build_conf import TARGETS, BASE_DIR, DIST_PYINSTALLER
from build_conf import TARGETS, BASE_DIR, DIST_PYINSTALLER, MACOS_APP_NAME

DIST_DIR = os.path.join(BASE_DIR, DIST_PYINSTALLER)

Expand Down Expand Up @@ -91,17 +91,24 @@ def build(target_key: str, onefile: bool = False):

is_macos = platform.system() == "Darwin"
windowed = target.get("pyinstaller_windowed", False)
macos_app = is_macos and windowed

# On macOS GUI, override --name so the .app bundle and CFBundleName
# use the display name (e.g. "SessionPrep") instead of the platform-
# suffixed executable name.
if macos_app:
cmd[cmd.index(app_name)] = MACOS_APP_NAME

if windowed:
cmd.append("--windowed")
else:
cmd.append("--console")

# macOS: --onefile + --windowed is deprecated.
if onefile and not (is_macos and windowed):
if onefile and not macos_app:
cmd.append("--onefile")
else:
if onefile and is_macos and windowed:
if onefile and macos_app:
print("Note: macOS GUI always builds as onedir (.app bundle — DMG created by workflow)")
cmd.append("--onedir")

Expand All @@ -116,18 +123,30 @@ def build(target_key: str, onefile: bool = False):
return False

# Check for output
if onefile:
if macos_app:
# PyInstaller creates MACOS_APP_NAME.app inside DIST_DIR
bundle_path = os.path.join(DIST_DIR, f"{MACOS_APP_NAME}.app")
if os.path.isdir(bundle_path):
print(f"\nBuild successful: {bundle_path}")
else:
print(f"\nBuild completed but .app bundle not found at: {bundle_path}")
elif onefile:
exe_path = os.path.join(DIST_DIR, app_name_ext)
if os.path.isfile(exe_path):
size_mb = os.path.getsize(exe_path) / (1024 * 1024)
print(f"\nBuild successful: {exe_path}")
print(f"Size: {size_mb:.1f} MB")
else:
print(f"\nBuild completed but executable not found at expected path: {exe_path}")
else:
# Onedir puts it in dist/APP_NAME/APP_NAME_EXT
exe_path = os.path.join(DIST_DIR, app_name, app_name_ext)

if os.path.isfile(exe_path):
size_mb = os.path.getsize(exe_path) / (1024 * 1024)
print(f"\nBuild successful: {exe_path}")
print(f"Size: {size_mb:.1f} MB")
else:
print(f"\nBuild completed but executable not found at expected path: {exe_path}")
if os.path.isfile(exe_path):
size_mb = os.path.getsize(exe_path) / (1024 * 1024)
print(f"\nBuild successful: {exe_path}")
print(f"Size: {size_mb:.1f} MB")
else:
print(f"\nBuild completed but executable not found at expected path: {exe_path}")

return True

Expand Down
Loading