Skip to content

Commit 899322c

Browse files
committed
refactor: update
1 parent 50bfe31 commit 899322c

File tree

2 files changed

+180
-223
lines changed

2 files changed

+180
-223
lines changed

build_desktop.py

Lines changed: 180 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import os
22
import platform
3+
import plistlib
34
import shutil
5+
import subprocess
46
from pathlib import Path
57
from subprocess import run
68

7-
# Import macOS app builder if on macOS
8-
if platform.system() == "Darwin":
9-
from build_macos_app import create_app_bundle
9+
import tomllib
1010

1111

1212
def get_executable_extension() -> str:
@@ -15,15 +15,177 @@ def get_executable_extension() -> str:
1515

1616
def load_toml(file_path: Path) -> dict:
1717
"""Load TOML file, handling different Python versions."""
18-
try:
19-
import tomllib
20-
except ImportError:
21-
import tomli as tomllib
22-
2318
with open(file_path, "rb") as f:
2419
return tomllib.load(f)
2520

2621

22+
def create_icns_from_png(png_path: Path, output_icns: Path) -> bool:
23+
"""
24+
Convert PNG to ICNS format using sips (macOS built-in tool).
25+
Returns True if successful, False otherwise.
26+
"""
27+
try:
28+
if platform.system() != "Darwin":
29+
print("Warning: ICNS conversion is only supported on macOS")
30+
return False
31+
32+
# create iconset directory
33+
iconset_dir = output_icns.parent / f"{output_icns.stem}.iconset"
34+
iconset_dir.mkdir(exist_ok=True)
35+
36+
# generate different sizes required for ICNS
37+
sizes = [16, 32, 64, 128, 256, 512, 1024]
38+
for size in sizes:
39+
output_file = iconset_dir / f"icon_{size}x{size}.png"
40+
subprocess.run(
41+
[
42+
"sips",
43+
"-z",
44+
str(size),
45+
str(size),
46+
str(png_path),
47+
"--out",
48+
str(output_file),
49+
],
50+
check=True,
51+
capture_output=True,
52+
)
53+
54+
# create @2x versions for retina displays (except for largest size)
55+
if size <= 512:
56+
retina_size = size * 2
57+
output_file_2x = iconset_dir / f"icon_{size}x{size}@2x.png"
58+
subprocess.run(
59+
[
60+
"sips",
61+
"-z",
62+
str(retina_size),
63+
str(retina_size),
64+
str(png_path),
65+
"--out",
66+
str(output_file_2x),
67+
],
68+
check=True,
69+
capture_output=True,
70+
)
71+
72+
# convert iconset to icns
73+
subprocess.run(
74+
["iconutil", "-c", "icns", str(iconset_dir), "-o", str(output_icns)],
75+
check=True,
76+
capture_output=True,
77+
)
78+
79+
# clean up iconset directory
80+
shutil.rmtree(iconset_dir)
81+
print(f"Created ICNS icon: {output_icns}")
82+
return True
83+
84+
except Exception as e:
85+
print(f"Failed to create ICNS: {e}")
86+
return False
87+
88+
89+
def create_info_plist(app_name: str, version: str, bundle_identifier: str) -> dict:
90+
"""Create the Info.plist dictionary for the macOS app."""
91+
return {
92+
"CFBundleDevelopmentRegion": "en",
93+
"CFBundleDisplayName": app_name,
94+
"CFBundleExecutable": app_name,
95+
"CFBundleIconFile": "AppIcon.icns",
96+
"CFBundleIdentifier": bundle_identifier,
97+
"CFBundleInfoDictionaryVersion": "6.0",
98+
"CFBundleName": app_name,
99+
"CFBundlePackageType": "APPL",
100+
"CFBundleShortVersionString": version,
101+
"CFBundleVersion": version,
102+
"LSMinimumSystemVersion": "10.13",
103+
"NSHighResolutionCapable": True,
104+
"NSPrincipalClass": "NSApplication",
105+
"NSRequiresAquaSystemAppearance": False,
106+
}
107+
108+
109+
def create_app_bundle(
110+
pyinstaller_output_dir: Path,
111+
app_name: str,
112+
version: str,
113+
bundle_identifier: str,
114+
icon_path: Path | None = None,
115+
) -> Path:
116+
"""
117+
Create a macOS .app bundle from PyInstaller output.
118+
119+
Args:
120+
pyinstaller_output_dir: Path to PyInstaller's dist output directory
121+
app_name: Name of the application
122+
version: Version string
123+
bundle_identifier: Bundle identifier (e.g., com.example.app)
124+
icon_path: Path to PNG icon file (will be converted to ICNS)
125+
126+
Returns:
127+
Path to the created .app bundle
128+
"""
129+
# define .app structure paths
130+
app_bundle = pyinstaller_output_dir.parent / f"{app_name}.app"
131+
contents_dir = app_bundle / "Contents"
132+
macos_dir = contents_dir / "MacOS"
133+
resources_dir = contents_dir / "Resources"
134+
135+
# clean up if it already exists
136+
if app_bundle.exists():
137+
shutil.rmtree(app_bundle)
138+
139+
# create directory structure
140+
macos_dir.mkdir(parents=True, exist_ok=True)
141+
resources_dir.mkdir(parents=True, exist_ok=True)
142+
143+
print(f"Creating app bundle: {app_bundle}")
144+
145+
# move PyInstaller contents to MacOS directory
146+
# PyInstaller creates a folder with the app name containing the executable and resources
147+
pyinstaller_app_dir = pyinstaller_output_dir / app_name
148+
if pyinstaller_app_dir.exists():
149+
# move everything from the PyInstaller output to MacOS
150+
for item in pyinstaller_app_dir.iterdir():
151+
dest = macos_dir / item.name
152+
if item.is_dir():
153+
shutil.copytree(item, dest)
154+
else:
155+
shutil.copy2(item, dest)
156+
print(f"Copied PyInstaller output to MacOS directory")
157+
else:
158+
raise FileNotFoundError(f"PyInstaller output not found: {pyinstaller_app_dir}")
159+
160+
# handle icon
161+
icns_path = resources_dir / "AppIcon.icns"
162+
if icon_path and icon_path.exists():
163+
if icon_path.suffix.lower() == ".png":
164+
create_icns_from_png(icon_path, icns_path)
165+
elif icon_path.suffix.lower() == ".icns":
166+
shutil.copy2(icon_path, icns_path)
167+
else:
168+
print(f"Warning: Unsupported icon format: {icon_path.suffix}")
169+
else:
170+
print("Warning: No icon provided")
171+
172+
# create Info.plist
173+
plist_data = create_info_plist(app_name, version, bundle_identifier)
174+
plist_path = contents_dir / "Info.plist"
175+
with open(plist_path, "wb") as f:
176+
plistlib.dump(plist_data, f)
177+
print(f"Created Info.plist")
178+
179+
# make the executable actually executable
180+
executable = macos_dir / app_name
181+
if executable.exists():
182+
os.chmod(executable, 0o755)
183+
print(f"Set executable permissions on {app_name}")
184+
185+
print(f"Successfully created app bundle: {app_bundle}")
186+
return app_bundle
187+
188+
27189
# def get_site_packages() -> Path:
28190
# output = run(
29191
# ("uv", "pip", "show", "PySide6"),
@@ -89,13 +251,13 @@ def build_app():
89251
"bundle",
90252
"--windowed",
91253
]
92-
93-
# Only add icon on Windows/Linux; macOS uses .icns which is added during .app bundle creation
254+
255+
# only add icon on Windows/Linux; macOS uses .icns which is added during .app bundle creation
94256
if platform.system() != "Darwin":
95257
build_args.append(f"--icon={str(icon_path)}")
96-
258+
97259
build_args.extend(["-y", str(desktop_script)])
98-
260+
99261
build_job_onedir = run(build_args)
100262

101263
# cleanse included runtime folder of unneeded files
@@ -126,30 +288,30 @@ def build_app():
126288
# else:
127289
# success_msgs.append("Onefile build did not complete successfully")
128290

129-
# Check onedir (bundle) build
291+
# check onedir (bundle) build
130292
onedir_path = Path("bundled_mode") / "Mp4Forge" / f"Mp4Forge{exe_str}"
131293
build_succeeded = onedir_path.is_file() and str(build_job_onedir.returncode) == "0"
132-
294+
133295
if build_succeeded:
134296
success_msgs.append(f"Bundle build success! Path: {Path.cwd() / onedir_path}")
135297
else:
136298
success_msgs.append("Bundle build did not complete successfully")
137299

138-
# Store absolute path before changing directory
300+
# store absolute path before changing directory
139301
pyinstaller_output = pyinstaller_folder / "bundled_mode"
140302

141303
# change directory back to original directory
142304
os.chdir(desktop_script.parent)
143305

144-
# On macOS, create a proper .app bundle
306+
# on macOS, create a proper .app bundle
145307
if platform.system() == "Darwin" and build_succeeded:
146308
try:
147-
# Get version from pyproject.toml
309+
# get version from pyproject.toml
148310
pyproject_path = project_root / "pyproject.toml"
149311
pyproject = load_toml(pyproject_path)
150312
version = pyproject["project"]["version"]
151313

152-
# Create .app bundle (pyinstaller_output already defined above)
314+
# create .app bundle (pyinstaller_output already defined above)
153315
icon_png = project_root / "runtime" / "images" / "mp4.png"
154316

155317
app_bundle = create_app_bundle(

0 commit comments

Comments
 (0)