diff --git a/.github/workflows/build-nuitka.yml b/.github/workflows/build-nuitka.yml index a1a71f5..d6464fd 100644 --- a/.github/workflows/build-nuitka.yml +++ b/.github/workflows/build-nuitka.yml @@ -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' diff --git a/.github/workflows/build-pyinstaller.yml b/.github/workflows/build-pyinstaller.yml index aa64a23..0527204 100644 --- a/.github/workflows/build-pyinstaller.yml +++ b/.github/workflows/build-pyinstaller.yml @@ -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' diff --git a/build_conf.py b/build_conf.py index 7530388..5c97276 100644 --- a/build_conf.py +++ b/build_conf.py @@ -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", diff --git a/build_nuitka.py b/build_nuitka.py index a1770d5..5cc1e1c 100644 --- a/build_nuitka.py +++ b/build_nuitka.py @@ -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.""" @@ -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}") @@ -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}") diff --git a/build_pyinstaller.py b/build_pyinstaller.py index 4e95592..d6a28bc 100644 --- a/build_pyinstaller.py +++ b/build_pyinstaller.py @@ -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) @@ -91,6 +91,13 @@ 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") @@ -98,10 +105,10 @@ def build(target_key: str, onefile: bool = False): 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") @@ -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