Files
RobustToolbox/Tools/macos_make_appbundle.py
PJB3005 dd41a7ce44 Use hard links for macOS app bundles
Avoid needing to get executable path from MainModule, which broke the game when run with the dotnet command instead of the bin's apphost. Fixes tests.
2025-12-31 18:46:46 +01:00

119 lines
4.1 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import os
import re
import shutil
import plistlib
p = os.path.join
symlinkable_re = re.compile(r"(?:runtimes|.+\.(?:dll|pdb|json))$", re.IGNORECASE)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--webview", action="store_true")
parser.add_argument("--name", required=True)
parser.add_argument("--directory", required=True)
parser.add_argument("--apphost", required=True)
parser.add_argument("--identifier", required=True)
parser.add_argument("--icon")
args = parser.parse_args()
dir: str = args.directory
name: str = args.name
# Create base app directory structure.
os.makedirs(p(dir, f"{name}.app", "Contents", "MacOS"), exist_ok=True)
os.makedirs(p(dir, f"{name}.app", "Contents", "Resources"), exist_ok=True)
os.makedirs(p(dir, f"{name}.app", "Contents", "Frameworks"), exist_ok=True)
# Copy apphost
dest_apphost = p(dir, f"{name}.app", "Contents", "MacOS", name)
shutil.copy(p(dir, args.apphost), dest_apphost)
# Symlink most files in the bin dir.
symlink_files(args.directory, p(dir, f"{name}.app", "Contents", "MacOS"), "")
# Copy icon
if args.icon:
shutil.copy(args.icon, p(dir, f"{name}.app", "Contents", "Resources", "icon.icns"))
# Write plist
plist_dat = {
"CFBundleName": name,
"CFBundleDisplayName": name,
"CFBundleIdentifier": args.identifier,
"CFBundleIconFile": "icon",
"CFBundleExecutable": name,
"LSApplicationCategoryType": "public.app-category.games"
}
with open(p(dir, f"{name}.app", "Contents", "Info.plist"), "wb") as f:
plistlib.dump(plist_dat, f)
if args.webview:
chromium_framework_path = p(dir, f"{name}.app", "Contents", "Frameworks", "Chromium Embedded Framework.framework")
if not os.path.exists(chromium_framework_path):
os.symlink("../../../Chromium Embedded Framework.framework", chromium_framework_path)
create_webview_helper(dir, name, args.identifier, None, None)
create_webview_helper(dir, name, args.identifier, "GPU", "gpu")
create_webview_helper(dir, name, args.identifier, "Renderer", "renderer")
create_webview_helper(dir, name, args.identifier, "Alerts", "alerts")
def create_webview_helper(dir: str, name: str, identifier: str, suffix: str | None, identifier_suffix: str | None):
helper_name = f"{name} helper"
if suffix is not None:
helper_name += f" ({suffix})"
sub_app_path = p(dir, f"{name}.app", "Contents", "Frameworks", f"{helper_name}.app")
os.makedirs(p(sub_app_path, "Contents", "MacOS"), exist_ok=True)
os.makedirs(p(sub_app_path, "Contents", "Resources"), exist_ok=True)
# Copy apphost for Robust.Client.WebView
shutil.copy(p(dir, "Robust.Client.WebView"), p(sub_app_path, "Contents", "MacOS", helper_name))
# Symlink files
symlink_files(dir, p(sub_app_path, "Contents", "MacOS"), "../../../")
helper_identifier = f"{identifier}.cef.{identifier_suffix}"
if identifier_suffix is not None:
helper_identifier += "." + identifier_suffix
plist_dat = {
"CFBundleName": f"{name} helper",
"CFBundleDisplayName": f"{name} helper",
"CFBundleIdentifier": f"{identifier}.cef.{identifier_suffix}",
"CFBundleExecutable": helper_name
}
with open(p(sub_app_path, "Contents", "Info.plist"), "wb") as f:
plistlib.dump(plist_dat, f)
def symlink_files(src_dir: str, dest_dir: str, relative: str):
for file in os.listdir(src_dir):
if not symlinkable_re.match(file):
continue
src_path = p(src_dir, file)
dest_symlink = p(dest_dir, file)
if os.path.isdir(src_path):
# Symlink directories
if not os.path.islink(dest_symlink):
os.symlink(f"../../../{relative}{file}", dest_symlink)
else:
# Hardlink files
# (so that .NET doesn't report the real file path for assembly locations)
try:
os.remove(dest_symlink)
except FileNotFoundError:
pass # Fine
os.link(src_path, dest_symlink)
main()