Files
2026-07-13 13:05:14 +08:00

106 lines
3.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Assemble a macOS `.app` bundle around the rerun-cli binary.
Produces `<output>/Rerun.app/` with:
Contents/
Info.plist (with __VERSION__ substituted)
MacOS/rerun (the binary, executable bit set)
Resources/Rerun.icns (multi-resolution icns derived from the PNG)
The bundle is what gives macOS the right dock label ("Rerun" instead of "rerun"),
proper "About Rerun" menu, and a hook for future file associations.
"""
from __future__ import annotations
import argparse
import re
import shutil
import subprocess
import sys
from pathlib import Path
# Apple requires CFBundleShortVersionString to be three integers separated by periods.
# Map e.g. "0.33.0-alpha.1+dev" → "0.33.0".
_VERSION_PREFIX = re.compile(r"^(\d+\.\d+\.\d+)")
ICNS_SIZES = [
(16, "icon_16x16.png"),
(32, "icon_16x16@2x.png"),
(32, "icon_32x32.png"),
(64, "icon_32x32@2x.png"),
(128, "icon_128x128.png"),
(256, "icon_128x128@2x.png"),
(256, "icon_256x256.png"),
(512, "icon_256x256@2x.png"),
(512, "icon_512x512.png"),
(1024, "icon_512x512@2x.png"),
]
def run(args: list[str]) -> None:
print(f"> {' '.join(args)}", flush=True)
subprocess.run(args, check=True)
def build_icns(png: Path, out: Path) -> None:
"""Build a multi-resolution .icns from a square source PNG using macOS native tools."""
iconset = out.parent / f"{out.stem}.iconset"
if iconset.exists():
shutil.rmtree(iconset)
iconset.mkdir(parents=True)
for size, name in ICNS_SIZES:
run(["sips", "-z", str(size), str(size), str(png), "--out", str(iconset / name)])
run(["iconutil", "--convert", "icns", "--output", str(out), str(iconset)])
shutil.rmtree(iconset)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--binary", required=True, type=Path, help="Path to the rerun-cli binary")
parser.add_argument("--icon", required=True, type=Path, help="Path to the source square PNG icon")
parser.add_argument("--info-plist", required=True, type=Path, help="Path to the Info.plist template")
parser.add_argument("--version", required=True, help="Version string (e.g. 0.21.0)")
parser.add_argument("--output-dir", required=True, type=Path, help="Directory to write Rerun.app into")
args = parser.parse_args()
if sys.platform != "darwin":
print("error: bundle_macos_app.py must run on macOS (uses sips and iconutil)", file=sys.stderr)
return 1
for path in [args.binary, args.icon, args.info_plist]:
if not path.exists():
print(f"error: {path} does not exist", file=sys.stderr)
return 1
app = args.output_dir / "Rerun.app"
if app.exists():
shutil.rmtree(app)
macos_dir = app / "Contents" / "MacOS"
resources_dir = app / "Contents" / "Resources"
macos_dir.mkdir(parents=True)
resources_dir.mkdir(parents=True)
# Binary — named with a capital R inside the bundle so macOS's
# NSProcessInfo.processName resolves to "Rerun", which winit then uses to
# build the app menu items ("About Rerun", "Hide Rerun", "Quit Rerun").
binary_dst = macos_dir / "Rerun"
shutil.copy2(args.binary, binary_dst)
binary_dst.chmod(0o755)
# Info.plist with version substituted (sanitized to Apple's x.y.z form)
match = _VERSION_PREFIX.match(args.version)
short_version = match.group(1) if match else "0.0.0"
plist_text = args.info_plist.read_text(encoding="utf-8").replace("__VERSION__", short_version)
(app / "Contents" / "Info.plist").write_text(plist_text, encoding="utf-8")
# Icon
build_icns(args.icon, resources_dir / "Rerun.icns")
print(f"Wrote {app}")
return 0
if __name__ == "__main__":
sys.exit(main())