diff --git a/packaging/macos/Eluant.dll.config b/packaging/macos/Eluant.dll.config
new file mode 100644
index 0000000000..43be429756
--- /dev/null
+++ b/packaging/macos/Eluant.dll.config
@@ -0,0 +1,3 @@
+
+
+
diff --git a/packaging/macos/Info.plist.in b/packaging/macos/Info.plist.in
new file mode 100644
index 0000000000..4990c6a69e
--- /dev/null
+++ b/packaging/macos/Info.plist.in
@@ -0,0 +1,50 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ English
+ CFBundleDisplayName
+ {MOD_NAME}
+ CFBundleExecutable
+ OpenRA
+ CFBundleIconFile
+ {MOD_ID}.icns
+ CFBundleIdentifier
+ net.openra.mod.{MOD_ID}
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ {MOD_NAME}
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ {DEV_VERSION}
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ {DEV_VERSION}
+ LSMinimumSystemVersion
+ {MINIMUM_SYSTEM_VERSION}
+ NSPrincipalClass
+ NSApplication
+ CFBundleURLTypes
+
+
+ CFBundleURLName
+ OpenRA Server
+ CFBundleURLSchemes
+
+ {JOIN_SERVER_URL_SCHEME}
+ {DISCORD_URL_SCHEME}
+
+
+
+ ModId
+ {MOD_ID}
+ FaqUrl
+ {FAQ_URL}
+ JoinServerUrlScheme
+ {JOIN_SERVER_URL_SCHEME}
+
+
diff --git a/packaging/macos/buildpackage.sh b/packaging/macos/buildpackage.sh
index 17b978d547..6ee4e851f6 100755
--- a/packaging/macos/buildpackage.sh
+++ b/packaging/macos/buildpackage.sh
@@ -13,7 +13,7 @@
# MACOS_DEVELOPER_PASSWORD: App-specific password for the developer account
#
-LAUNCHER_TAG="osx-launcher-20200525"
+MONO_TAG="osx-launcher-20200830"
if [ $# -ne "2" ]; then
echo "Usage: $(basename "$0") tag outputdir"
@@ -56,6 +56,7 @@ populate_bundle() {
MOD_ID=${2}
MOD_NAME=${3}
DISCORD_APPID=${4}
+
cp -r "${BUILTDIR}/OpenRA.app" "${TEMPLATE_DIR}"
# Assemble multi-resolution icon
@@ -76,7 +77,7 @@ populate_bundle() {
modify_plist "{MOD_ID}" "${MOD_ID}" "${TEMPLATE_DIR}/Contents/Info.plist"
modify_plist "{MOD_NAME}" "${MOD_NAME}" "${TEMPLATE_DIR}/Contents/Info.plist"
modify_plist "{JOIN_SERVER_URL_SCHEME}" "openra-${MOD_ID}-${TAG}" "${TEMPLATE_DIR}/Contents/Info.plist"
- modify_plist "{ADDITIONAL_URL_SCHEMES}" "discord-${DISCORD_APPID}" "${TEMPLATE_DIR}/Contents/Info.plist"
+ modify_plist "{DISCORD_URL_SCHEME}" "discord-${DISCORD_APPID}" "${TEMPLATE_DIR}/Contents/Info.plist"
}
# Deletes from the first argument's mod dirs all the later arguments
@@ -95,124 +96,145 @@ sign_bundle() {
fi
}
-echo "Building launchers"
-curl -s -L -O https://github.com/OpenRA/OpenRALauncherOSX/releases/download/${LAUNCHER_TAG}/launcher.zip || exit 3
-unzip -qq -d "${BUILTDIR}" launcher.zip
-rm launcher.zip
+build_platform() {
+ PLATFORM="${1}"
+ DMG_PATH="${2}"
+ echo "Building launchers (${PLATFORM})"
-modify_plist "{DEV_VERSION}" "${TAG}" "${BUILTDIR}/OpenRA.app/Contents/Info.plist"
-modify_plist "{FAQ_URL}" "http://wiki.openra.net/FAQ" "${BUILTDIR}/OpenRA.app/Contents/Info.plist"
-echo "Building core files"
+ mkdir -p "${BUILTDIR}/OpenRA.app/Contents/Resources"
+ mkdir -p "${BUILTDIR}/OpenRA.app/Contents/MacOS"
+ echo "APPL????" > "${BUILTDIR}/OpenRA.app/Contents/PkgInfo"
+ cp Eluant.dll.config "${BUILTDIR}/OpenRA.app/Contents/Resources"
+ cp Info.plist.in "${BUILTDIR}/OpenRA.app/Contents/Info.plist"
+ modify_plist "{DEV_VERSION}" "${TAG}" "${BUILTDIR}/OpenRA.app/Contents/Info.plist"
+ modify_plist "{FAQ_URL}" "http://wiki.openra.net/FAQ" "${BUILTDIR}/OpenRA.app/Contents/Info.plist"
-pushd "${SRCDIR}" > /dev/null || exit 1
-make clean
-make core TARGETPLATFORM=osx-x64
-make version VERSION="${TAG}"
-make install-core gameinstalldir="/Contents/Resources/" DESTDIR="${BUILTDIR}/OpenRA.app"
-make install-dependencies TARGETPLATFORM=osx-x64 gameinstalldir="/Contents/Resources/" DESTDIR="${BUILTDIR}/OpenRA.app"
-popd > /dev/null || exit 1
+ if [ "${PLATFORM}" = "compat" ]; then
+ modify_plist "{MINIMUM_SYSTEM_VERSION}" "10.9" "${BUILTDIR}/OpenRA.app/Contents/Info.plist"
+ clang -m64 launcher-mono.m -o "${BUILTDIR}/OpenRA.app/Contents/MacOS/OpenRA" -framework AppKit -mmacosx-version-min=10.9
+ else
+ modify_plist "{MINIMUM_SYSTEM_VERSION}" "10.13" "${BUILTDIR}/OpenRA.app/Contents/Info.plist"
+ clang -m64 launcher.m -o "${BUILTDIR}/OpenRA.app/Contents/MacOS/OpenRA" -framework AppKit -mmacosx-version-min=10.13
-populate_bundle "OpenRA - Red Alert.app" "ra" "Red Alert" "699222659766026240"
-delete_mods "OpenRA - Red Alert.app" "cnc" "d2k"
-sign_bundle "OpenRA - Red Alert.app"
+ curl -s -L -O https://github.com/OpenRA/OpenRALauncherOSX/releases/download/${MONO_TAG}/mono.zip || exit 3
+ unzip -qq -d "${BUILTDIR}/mono" mono.zip
+ mv "${BUILTDIR}/mono/mono" "${BUILTDIR}/OpenRA.app/Contents/MacOS/"
+ mv "${BUILTDIR}/mono/etc" "${BUILTDIR}/OpenRA.app/Contents/Resources"
+ mv "${BUILTDIR}/mono/lib" "${BUILTDIR}/OpenRA.app/Contents/Resources"
+ rm mono.zip
+ rmdir "${BUILTDIR}/mono"
+ fi
-populate_bundle "OpenRA - Tiberian Dawn.app" "cnc" "Tiberian Dawn" "699223250181292033"
-delete_mods "OpenRA - Tiberian Dawn.app" "ra" "d2k"
-sign_bundle "OpenRA - Tiberian Dawn.app"
+ echo "Building core files"
-populate_bundle "OpenRA - Dune 2000.app" "d2k" "Dune 2000" "712711732770111550"
-delete_mods "OpenRA - Dune 2000.app" "ra" "cnc"
-sign_bundle "OpenRA - Dune 2000.app"
+ pushd "${SRCDIR}" > /dev/null || exit 1
+ make clean
+ make core TARGETPLATFORM=osx-x64
+ make version VERSION="${TAG}"
+ make install-core gameinstalldir="/Contents/Resources/" DESTDIR="${BUILTDIR}/OpenRA.app"
+ make install-dependencies TARGETPLATFORM=osx-x64 gameinstalldir="/Contents/Resources/" DESTDIR="${BUILTDIR}/OpenRA.app"
+ popd > /dev/null || exit 1
-rm -rf "${BUILTDIR}/OpenRA.app"
+ populate_bundle "OpenRA - Red Alert.app" "ra" "Red Alert" "699222659766026240"
+ delete_mods "OpenRA - Red Alert.app" "cnc" "d2k"
+ sign_bundle "OpenRA - Red Alert.app"
-if [ -n "${MACOS_DEVELOPER_CERTIFICATE_BASE64}" ] && [ -n "${MACOS_DEVELOPER_CERTIFICATE_PASSWORD}" ] && [ -n "${MACOS_DEVELOPER_IDENTITY}" ]; then
- security delete-keychain build.keychain
-fi
+ populate_bundle "OpenRA - Tiberian Dawn.app" "cnc" "Tiberian Dawn" "699223250181292033"
+ delete_mods "OpenRA - Tiberian Dawn.app" "ra" "d2k"
+ sign_bundle "OpenRA - Tiberian Dawn.app"
-echo "Packaging disk image"
-hdiutil create build.dmg -format UDRW -volname "OpenRA" -fs HFS+ -srcfolder build
-DMG_DEVICE=$(hdiutil attach -readwrite -noverify -noautoopen "build.dmg" | egrep '^/dev/' | sed 1q | awk '{print $1}')
-sleep 2
+ populate_bundle "OpenRA - Dune 2000.app" "d2k" "Dune 2000" "712711732770111550"
+ delete_mods "OpenRA - Dune 2000.app" "ra" "cnc"
+ sign_bundle "OpenRA - Dune 2000.app"
-# Background image is created from source svg in artsrc repository
-mkdir "/Volumes/OpenRA/.background/"
-tiffutil -cathidpicheck "${ARTWORK_DIR}/macos-background.png" "${ARTWORK_DIR}/macos-background-2x.png" -out "/Volumes/OpenRA/.background/background.tiff"
+ rm -rf "${BUILTDIR}/OpenRA.app"
-cp "${BUILTDIR}/OpenRA - Red Alert.app/Contents/Resources/ra.icns" "/Volumes/OpenRA/.VolumeIcon.icns"
+ echo "Packaging disk image"
+ hdiutil create "${DMG_PATH}" -format UDRW -volname "OpenRA" -fs HFS+ -srcfolder build
+ DMG_DEVICE=$(hdiutil attach -readwrite -noverify -noautoopen "${DMG_PATH}" | egrep '^/dev/' | sed 1q | awk '{print $1}')
+ sleep 2
-echo '
- tell application "Finder"
- tell disk "'OpenRA'"
- open
- set current view of container window to icon view
- set toolbar visible of container window to false
- set statusbar visible of container window to false
- set the bounds of container window to {400, 100, 1040, 580}
- set theViewOptions to the icon view options of container window
- set arrangement of theViewOptions to not arranged
- set icon size of theViewOptions to 72
- set background picture of theViewOptions to file ".background:background.tiff"
- make new alias file at container window to POSIX file "/Applications" with properties {name:"Applications"}
- set position of item "'OpenRA - Tiberian Dawn.app'" of container window to {160, 106}
- set position of item "'OpenRA - Red Alert.app'" of container window to {320, 106}
- set position of item "'OpenRA - Dune 2000.app'" of container window to {480, 106}
- set position of item "Applications" of container window to {320, 298}
- set position of item ".background" of container window to {160, 298}
- set position of item ".fseventsd" of container window to {160, 298}
- set position of item ".VolumeIcon.icns" of container window to {160, 298}
- update without registering applications
- delay 5
- close
- end tell
- end tell
-' | osascript
+ # Background image is created from source svg in artsrc repository
+ mkdir "/Volumes/OpenRA/.background/"
+ tiffutil -cathidpicheck "${ARTWORK_DIR}/macos-background.png" "${ARTWORK_DIR}/macos-background-2x.png" -out "/Volumes/OpenRA/.background/background.tiff"
-# HACK: Copy the volume icon again - something in the previous step seems to delete it...?
-cp "${BUILTDIR}/OpenRA - Red Alert.app/Contents/Resources/ra.icns" "/Volumes/OpenRA/.VolumeIcon.icns"
-SetFile -c icnC "/Volumes/OpenRA/.VolumeIcon.icns"
-SetFile -a C "/Volumes/OpenRA"
+ cp "${BUILTDIR}/OpenRA - Red Alert.app/Contents/Resources/ra.icns" "/Volumes/OpenRA/.VolumeIcon.icns"
-chmod -Rf go-w /Volumes/OpenRA
-sync
-sync
+ echo '
+ tell application "Finder"
+ tell disk "'OpenRA'"
+ open
+ set current view of container window to icon view
+ set toolbar visible of container window to false
+ set statusbar visible of container window to false
+ set the bounds of container window to {400, 100, 1040, 580}
+ set theViewOptions to the icon view options of container window
+ set arrangement of theViewOptions to not arranged
+ set icon size of theViewOptions to 72
+ set background picture of theViewOptions to file ".background:background.tiff"
+ make new alias file at container window to POSIX file "/Applications" with properties {name:"Applications"}
+ set position of item "'OpenRA - Tiberian Dawn.app'" of container window to {160, 106}
+ set position of item "'OpenRA - Red Alert.app'" of container window to {320, 106}
+ set position of item "'OpenRA - Dune 2000.app'" of container window to {480, 106}
+ set position of item "Applications" of container window to {320, 298}
+ set position of item ".background" of container window to {160, 298}
+ set position of item ".fseventsd" of container window to {160, 298}
+ set position of item ".VolumeIcon.icns" of container window to {160, 298}
+ update without registering applications
+ delay 5
+ close
+ end tell
+ end tell
+ ' | osascript
-hdiutil detach "${DMG_DEVICE}"
+ # HACK: Copy the volume icon again - something in the previous step seems to delete it...?
+ cp "${BUILTDIR}/OpenRA - Red Alert.app/Contents/Resources/ra.icns" "/Volumes/OpenRA/.VolumeIcon.icns"
+ SetFile -c icnC "/Volumes/OpenRA/.VolumeIcon.icns"
+ SetFile -a C "/Volumes/OpenRA"
-# Submit for notarization
-if [ -n "${MACOS_DEVELOPER_USERNAME}" ] && [ -n "${MACOS_DEVELOPER_PASSWORD}" ]; then
- echo "Submitting disk image for notarization"
+ chmod -Rf go-w /Volumes/OpenRA
+ sync
+ sync
+
+ hdiutil detach "${DMG_DEVICE}"
+ rm -rf "${BUILTDIR}"
+}
+
+notarize_package() {
+ DMG_PATH="${1}"
+ NOTARIZE_DMG_PATH="${DMG_PATH%.*}"-notarization.dmg
+ echo "Submitting ${PACKAGE_NAME} for notarization"
# Reset xcode search path to fix xcrun not finding altool
sudo xcode-select -r
# Create a temporary read-only dmg for submission (notarization service rejects read/write images)
- hdiutil convert build.dmg -format UDZO -imagekey zlib-level=9 -ov -o notarization.dmg
+ hdiutil convert "${DMG_PATH}" -format UDZO -imagekey zlib-level=9 -ov -o "${NOTARIZE_DMG_PATH}"
- NOTARIZATION_UUID=$(xcrun altool --notarize-app --primary-bundle-id "net.openra.packaging" -u "${MACOS_DEVELOPER_USERNAME}" -p "${MACOS_DEVELOPER_PASSWORD}" --file notarization.dmg 2>&1 | awk -F' = ' '/RequestUUID/ { print $2; exit }')
+ NOTARIZATION_UUID=$(xcrun altool --notarize-app --primary-bundle-id "net.openra.packaging" -u "${MACOS_DEVELOPER_USERNAME}" -p "${MACOS_DEVELOPER_PASSWORD}" --file "${NOTARIZE_DMG_PATH}" 2>&1 | awk -F' = ' '/RequestUUID/ { print $2; exit }')
if [ -z "${NOTARIZATION_UUID}" ]; then
echo "Submission failed"
exit 1
fi
- echo "Submission UUID is ${NOTARIZATION_UUID}"
- rm notarization.dmg
+ echo "${DMG_PATH} submission UUID is ${NOTARIZATION_UUID}"
+ rm "${NOTARIZE_DMG_PATH}"
while :; do
sleep 30
NOTARIZATION_RESULT=$(xcrun altool --notarization-info "${NOTARIZATION_UUID}" -u "${MACOS_DEVELOPER_USERNAME}" -p "${MACOS_DEVELOPER_PASSWORD}" 2>&1 | awk -F': ' '/Status/ { print $2; exit }')
- echo "Submission status: ${NOTARIZATION_RESULT}"
+ echo "${DMG_PATH}: ${NOTARIZATION_RESULT}"
if [ "${NOTARIZATION_RESULT}" == "invalid" ]; then
NOTARIZATION_LOG_URL=$(xcrun altool --notarization-info "${NOTARIZATION_UUID}" -u "${MACOS_DEVELOPER_USERNAME}" -p "${MACOS_DEVELOPER_PASSWORD}" 2>&1 | awk -F': ' '/LogFileURL/ { print $2; exit }')
- echo "Notarization failed with error:"
+ echo "${NOTARIZATION_UUID} failed notarization with error:"
curl -s "${NOTARIZATION_LOG_URL}" -w "\n"
exit 1
fi
if [ "${NOTARIZATION_RESULT}" == "success" ]; then
- echo "Stapling notarization tickets"
- DMG_DEVICE=$(hdiutil attach -readwrite -noverify -noautoopen "build.dmg" | egrep '^/dev/' | sed 1q | awk '{print $1}')
+ echo "${DMG_PATH}: Stapling tickets"
+ DMG_DEVICE=$(hdiutil attach -readwrite -noverify -noautoopen "${DMG_PATH}" | egrep '^/dev/' | sed 1q | awk '{print $1}')
sleep 2
xcrun stapler staple "/Volumes/OpenRA/OpenRA - Red Alert.app"
@@ -226,9 +248,29 @@ if [ -n "${MACOS_DEVELOPER_USERNAME}" ] && [ -n "${MACOS_DEVELOPER_PASSWORD}" ];
break
fi
done
+}
+
+finalize_package() {
+ INPUT_PATH="${1}"
+ OUTPUT_PATH="${2}"
+
+ hdiutil convert "${INPUT_PATH}" -format UDZO -imagekey zlib-level=9 -ov -o "${OUTPUT_PATH}"
+ rm "${INPUT_PATH}"
+}
+
+build_platform "standard" "build.dmg"
+build_platform "compat" "build-compat.dmg"
+
+if [ -n "${MACOS_DEVELOPER_CERTIFICATE_BASE64}" ] && [ -n "${MACOS_DEVELOPER_CERTIFICATE_PASSWORD}" ] && [ -n "${MACOS_DEVELOPER_IDENTITY}" ]; then
+ security delete-keychain build.keychain
fi
-hdiutil convert build.dmg -format UDZO -imagekey zlib-level=9 -ov -o "${OUTPUTDIR}/OpenRA-${TAG}.dmg"
+if [ -n "${MACOS_DEVELOPER_USERNAME}" ] && [ -n "${MACOS_DEVELOPER_PASSWORD}" ]; then
+ # Parallelize processing
+ (notarize_package "build.dmg") &
+ (notarize_package "build-compat.dmg") &
+ wait
+fi
-# Clean up
-rm -rf "${BUILTDIR}" build.dmg
+finalize_package "build.dmg" "${OUTPUTDIR}/OpenRA-${TAG}.dmg"
+finalize_package "build-compat.dmg" "${OUTPUTDIR}/OpenRA-${TAG}-compat.dmg"
diff --git a/packaging/macos/launcher-mono.m b/packaging/macos/launcher-mono.m
new file mode 100644
index 0000000000..de3d7ab421
--- /dev/null
+++ b/packaging/macos/launcher-mono.m
@@ -0,0 +1,372 @@
+/*
+ * Copyright 2007-2020 The OpenRA Developers (see AUTHORS)
+ * This file is part of OpenRA, which is free software. It is made
+ * available to you under the terms of the GNU General Public License
+ * as published by the Free Software Foundation. For more information,
+ * see COPYING.
+ */
+
+#import
+#include
+
+#define SYSTEM_MONO_PATH @"/Library/Frameworks/Mono.framework/Versions/Current/"
+#define SYSTEM_MONO_MIN_VERSION @"6.4"
+
+typedef int (* mono_main)(int argc, char **argv);
+typedef void (* mono_free)(void *ptr);
+typedef char *(* mono_get_runtime_build_info)(void);
+
+@interface OpenRALauncher : NSObject
+- (void)launchGameWithArgs: (NSArray *)gameArgs;
+@end
+
+@implementation OpenRALauncher
+
+BOOL launched = NO;
+NSTask *gameTask;
+
+static int check_mono_version(const char *version, const char *req_version)
+{
+ char *req_end, *end;
+ long req_val, val;
+
+ while (*req_version)
+ {
+ req_val = strtol(req_version, &req_end, 10);
+ if (req_version == req_end || (*req_end && *req_end != '.'))
+ {
+ fprintf(stderr, "Bad version requirement string '%s'\n", req_end);
+ return FALSE;
+ }
+
+ req_version = req_end;
+ if (*req_version)
+ req_version++;
+
+ val = strtol (version, &end, 10);
+ if (version == end || val < req_val)
+ return FALSE;
+
+ if (val > req_val)
+ return TRUE;
+
+ if (*req_version == '.' && *end != '.')
+ return FALSE;
+
+ version = end + 1;
+ }
+
+ return TRUE;
+}
+
+- (int)hasValidMono
+{
+ void *libmono = dlopen([[SYSTEM_MONO_PATH stringByAppendingPathComponent: @"/lib/libmonosgen-2.0.dylib"] UTF8String], RTLD_LAZY);
+
+ if (libmono == NULL)
+ {
+ fprintf (stderr, "Failed to load libmonosgen-2.0.dylib: %s\n", dlerror());
+ return FALSE;
+ }
+
+ mono_main _mono_main = (mono_main)dlsym(libmono, "mono_main");
+ if (!_mono_main)
+ {
+ fprintf(stderr, "Could not load mono_main(): %s\n", dlerror());
+ return FALSE;
+ }
+
+ mono_free _mono_free = (mono_free)dlsym(libmono, "mono_free");
+ if (!_mono_free)
+ {
+ fprintf(stderr, "Could not load mono_free(): %s\n", dlerror());
+ return FALSE;
+ }
+
+ mono_get_runtime_build_info _mono_get_runtime_build_info = (mono_get_runtime_build_info)dlsym(libmono, "mono_get_runtime_build_info");
+ if (!_mono_get_runtime_build_info)
+ {
+ fprintf(stderr, "Could not load mono_get_runtime_build_info(): %s\n", dlerror());
+ return FALSE;
+ }
+
+ char *mono_version = _mono_get_runtime_build_info();
+ return check_mono_version(mono_version, [SYSTEM_MONO_MIN_VERSION UTF8String]);
+}
+
+- (NSString *)modName
+{
+ NSDictionary *plist = [[NSBundle mainBundle] infoDictionary];
+ if (plist)
+ {
+ NSString *title = [plist objectForKey:@"CFBundleDisplayName"];
+ if (title && [title length] > 0)
+ return title;
+ }
+
+ return @"OpenRA";
+}
+
+- (void)exitWithMonoPrompt
+{
+ [NSApp setActivationPolicy: NSApplicationActivationPolicyRegular];
+ [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
+
+ NSString *modName = [self modName];
+ NSString *title = [NSString stringWithFormat: @"Cannot launch %@", modName];
+ NSString *message = [NSString stringWithFormat: @"%@ requires Mono %@ or later. Please install Mono and try again.", modName, SYSTEM_MONO_MIN_VERSION];
+
+ NSAlert *alert = [[NSAlert alloc] init];
+ [alert setMessageText:title];
+ [alert setInformativeText:message];
+ [alert addButtonWithTitle:@"Download Mono"];
+ [alert addButtonWithTitle:@"Quit"];
+ NSInteger answer = [alert runModal];
+ [alert release];
+
+ if (answer == NSAlertFirstButtonReturn)
+ [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:@"https://www.mono-project.com/download/"]];
+
+ exit(1);
+}
+
+- (void)exitWithCrashPrompt
+{
+ [NSApp setActivationPolicy: NSApplicationActivationPolicyRegular];
+ [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
+
+ NSString *modName = [self modName];
+ NSString *message = [NSString stringWithFormat: @"%@ has encountered a fatal error and must close.\nPlease refer to the crash logs and FAQ for more information.", modName];
+
+ NSAlert *alert = [[NSAlert alloc] init];
+ [alert setMessageText:@"Fatal Error"];
+ [alert setInformativeText:message];
+ [alert addButtonWithTitle:@"View Logs"];
+ [alert addButtonWithTitle:@"View FAQ"];
+ [alert addButtonWithTitle:@"Quit"];
+
+ NSInteger answer = [alert runModal];
+ [alert release];
+
+ if (answer == NSAlertFirstButtonReturn)
+ {
+ NSString *logDir = [@"~/Library/Application Support/OpenRA/Logs/" stringByExpandingTildeInPath];
+ [[NSWorkspace sharedWorkspace] openFile: logDir withApplication:@"Finder"];
+ }
+ else if (answer == NSAlertSecondButtonReturn)
+ {
+ NSDictionary *plist = [[NSBundle mainBundle] infoDictionary];
+ if (plist)
+ {
+ NSString *faqUrl = [plist objectForKey:@"FaqUrl"];
+ if (faqUrl && [faqUrl length] > 0)
+ [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:faqUrl]];
+ }
+ }
+
+ exit(1);
+}
+
+// Application was launched via a URL handler
+- (void)getUrl:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent
+{
+ NSMutableArray *gameArgs = [[[NSProcessInfo processInfo] arguments] mutableCopy];
+ NSDictionary *plist = [[NSBundle mainBundle] infoDictionary];
+ NSString *url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
+
+ if (plist)
+ {
+ NSString *joinServerUrl = [plist objectForKey:@"JoinServerUrlScheme"];
+ if (joinServerUrl && [joinServerUrl length] > 0)
+ {
+ NSString *prefix = [joinServerUrl stringByAppendingString: @"://"];
+ if ([url hasPrefix: prefix])
+ {
+ NSString *trimmed = [url substringFromIndex:[prefix length]];
+ NSArray *parts = [trimmed componentsSeparatedByString:@":"];
+ NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
+
+ if ([parts count] == 2 && [formatter numberFromString: [parts objectAtIndex:1]] != nil)
+ [gameArgs addObject: [NSString stringWithFormat: @"Launch.Connect=%@", trimmed]];
+
+ [formatter release];
+ }
+ }
+ }
+
+ [self launchGameWithArgs: gameArgs];
+ [gameArgs release];
+}
+
+- (void)applicationWillFinishLaunching:(NSNotification *)aNotification
+{
+ // Register for url events
+ NSDictionary *plist = [[NSBundle mainBundle] infoDictionary];
+ if (plist)
+ {
+ NSString *joinServerUrl = [plist objectForKey:@"JoinServerUrlScheme"];
+ NSString *bundleIdentifier = [plist objectForKey:@"CFBundleIdentifier"];
+ if (joinServerUrl && [joinServerUrl length] > 0 && bundleIdentifier)
+ {
+ LSSetDefaultHandlerForURLScheme((CFStringRef)joinServerUrl, (CFStringRef)bundleIdentifier);
+ [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self andSelector:@selector(getUrl:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL];
+ }
+ }
+}
+
+- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
+{
+ [self launchGameWithArgs: [[NSProcessInfo processInfo] arguments]];
+}
+
+- (BOOL)applicationShouldTerminateAfterLastWindowClosed: (NSApplication *)theApplication
+{
+ return YES;
+}
+
+- (void)launchGameWithArgs: (NSArray *)gameArgs
+{
+ if (launched)
+ {
+ NSLog(@"launchgame is already running... ignoring request.");
+ return;
+ }
+
+ launched = YES;
+
+ if (![self hasValidMono])
+ [self exitWithMonoPrompt];
+
+ // Default values - can be overriden by setting certain keys Info.plist
+ NSString *gameName = @"OpenRA.Game.exe";
+ NSString *modId = nil;
+
+ NSDictionary *plist = [[NSBundle mainBundle] infoDictionary];
+ if (plist)
+ {
+ NSString *exeValue = [plist objectForKey:@"MonoGameExe"];
+ if (exeValue && [exeValue length] > 0)
+ gameName = exeValue;
+
+ NSString *modIdValue = [plist objectForKey:@"ModId"];
+ if (modIdValue && [modIdValue length] > 0)
+ modId = modIdValue;
+ }
+
+ NSString *exePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/MacOS/"];
+ NSString *gamePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/Resources/"];
+
+ NSString *launchPath = [SYSTEM_MONO_PATH stringByAppendingPathComponent: @"Commands/mono"];
+ NSString *appPath = [exePath stringByAppendingPathComponent: @"OpenRA"];
+ NSString *engineLaunchPath = [self resolveTranslocatedPath: appPath];
+
+ NSMutableArray *launchArgs = [NSMutableArray arrayWithCapacity: [gameArgs count] + 2];
+ [launchArgs addObject: @"--debug"];
+ [launchArgs addObject: [gamePath stringByAppendingPathComponent: gameName]];
+ [launchArgs addObject: [NSString stringWithFormat:@"Engine.LaunchPath=\"%@\"", engineLaunchPath]];
+
+ if (modId)
+ [launchArgs addObject: [NSString stringWithFormat:@"Game.Mod=%@", modId]];
+
+ [launchArgs addObjectsFromArray: gameArgs];
+
+ NSLog(@"Running launchgame with arguments:");
+ for (size_t i = 0; i < [launchArgs count]; i++)
+ NSLog(@"%@", [launchArgs objectAtIndex: i]);
+
+ gameTask = [[NSTask alloc] init];
+ [gameTask setCurrentDirectoryPath: gamePath];
+ [gameTask setLaunchPath: launchPath];
+ [gameTask setArguments: launchArgs];
+
+ [[NSNotificationCenter defaultCenter]
+ addObserver: self
+ selector: @selector(taskExited:)
+ name: NSTaskDidTerminateNotification
+ object: gameTask
+ ];
+
+ [gameTask launch];
+}
+
+- (NSString *)resolveTranslocatedPath: (NSString *)path
+{
+ // macOS 10.12 introduced the "App Translocation" feature, which runs quarantined applications
+ // from a transient read-only disk image. The read-only image isn't a problem, but the transient
+ // path breaks the mod registration/switching feature.
+ // This resolves the original path which can then be written into the mod metadata for future
+ // launches (which will then be re-translocated)
+
+ // Running on macOS < 10.12
+ if (floor(NSAppKitVersionNumber) <= 1404)
+ return path;
+
+ void *handle = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY);
+
+ // Failed to load security framework
+ if (handle == NULL)
+ return path;
+
+ Boolean (*mySecTranslocateIsTranslocatedURL)(CFURLRef path, bool *isTranslocated, CFErrorRef * __nullable error);
+ mySecTranslocateIsTranslocatedURL = dlsym(handle, "SecTranslocateIsTranslocatedURL");
+
+ CFURLRef __nullable (*mySecTranslocateCreateOriginalPathForURL)(CFURLRef translocatedPath, CFErrorRef * __nullable error);
+ mySecTranslocateCreateOriginalPathForURL = dlsym(handle, "SecTranslocateCreateOriginalPathForURL");
+
+ // Failed to resolve required functions
+ if (mySecTranslocateIsTranslocatedURL == NULL || mySecTranslocateCreateOriginalPathForURL == NULL)
+ return path;
+
+ bool isTranslocated = false;
+ CFURLRef pathURLRef = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (__bridge CFStringRef)path, kCFURLPOSIXPathStyle, false);
+
+ if (mySecTranslocateIsTranslocatedURL(pathURLRef, &isTranslocated, NULL))
+ {
+ if (isTranslocated)
+ {
+ CFURLRef resolvedURL = mySecTranslocateCreateOriginalPathForURL(pathURLRef, NULL);
+ path = [(NSURL *)(resolvedURL) path];
+ }
+ }
+
+ CFRelease(pathURLRef);
+ return path;
+}
+
+- (void)taskExited:(NSNotification *)note
+{
+ [[NSNotificationCenter defaultCenter]
+ removeObserver:self
+ name:NSTaskDidTerminateNotification
+ object:gameTask
+ ];
+
+ int ret = [gameTask terminationStatus];
+ NSLog(@"launchgame exited with code %d", ret);
+ [gameTask release];
+ gameTask = nil;
+
+ // We're done here
+ if (ret != 0)
+ [self exitWithCrashPrompt];
+
+ exit(0);
+}
+
+@end
+
+int main(int argc, char **argv)
+{
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ NSApplication *application = [NSApplication sharedApplication];
+ OpenRALauncher *launcher = [[OpenRALauncher alloc] init];
+ [NSApp setActivationPolicy: NSApplicationActivationPolicyProhibited];
+
+ [application setDelegate:launcher];
+ [application run];
+
+ [launcher release];
+ [pool drain];
+
+ return EXIT_SUCCESS;
+}
diff --git a/packaging/macos/launcher.m b/packaging/macos/launcher.m
new file mode 100644
index 0000000000..47f89e9416
--- /dev/null
+++ b/packaging/macos/launcher.m
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2007-2020 The OpenRA Developers (see AUTHORS)
+ * This file is part of OpenRA, which is free software. It is made
+ * available to you under the terms of the GNU General Public License
+ * as published by the Free Software Foundation. For more information,
+ * see COPYING.
+ */
+
+#import
+#include
+
+@interface OpenRALauncher : NSObject
+- (void)launchGameWithArgs: (NSArray *)gameArgs;
+@end
+
+@implementation OpenRALauncher
+
+BOOL launched = NO;
+NSTask *gameTask;
+
+- (NSString *)modName
+{
+ NSDictionary *plist = [[NSBundle mainBundle] infoDictionary];
+ if (plist)
+ {
+ NSString *title = [plist objectForKey:@"CFBundleDisplayName"];
+ if (title && [title length] > 0)
+ return title;
+ }
+
+ return @"OpenRA";
+}
+
+- (void)showCrashPrompt
+{
+ NSString *modName = [self modName];
+ NSString *message = [NSString stringWithFormat: @"%@ has encountered a fatal error and must close.\nPlease refer to the crash logs and FAQ for more information.", modName];
+
+ NSAlert *alert = [[NSAlert alloc] init];
+ [alert setMessageText:@"Fatal Error"];
+ [alert setInformativeText:message];
+ [alert addButtonWithTitle:@"View Logs"];
+ [alert addButtonWithTitle:@"View FAQ"];
+ [alert addButtonWithTitle:@"Quit"];
+
+ NSInteger answer = [alert runModal];
+ [alert release];
+
+ if (answer == NSAlertFirstButtonReturn)
+ {
+ NSString *logDir = [@"~/Library/Application Support/OpenRA/Logs/" stringByExpandingTildeInPath];
+ [[NSWorkspace sharedWorkspace] openFile: logDir withApplication:@"Finder"];
+ }
+ else if (answer == NSAlertSecondButtonReturn)
+ {
+ NSDictionary *plist = [[NSBundle mainBundle] infoDictionary];
+ if (plist)
+ {
+ NSString *faqUrl = [plist objectForKey:@"FaqUrl"];
+ if (faqUrl && [faqUrl length] > 0)
+ [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:faqUrl]];
+ }
+ }
+}
+
+// Application was launched via a URL handler
+- (void)getUrl:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent
+{
+ NSMutableArray *gameArgs = [[[NSProcessInfo processInfo] arguments] mutableCopy];
+ NSDictionary *plist = [[NSBundle mainBundle] infoDictionary];
+ NSString *url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
+
+ if (plist)
+ {
+ NSString *joinServerUrl = [plist objectForKey:@"JoinServerUrlScheme"];
+ if (joinServerUrl && [joinServerUrl length] > 0)
+ {
+ NSString *prefix = [joinServerUrl stringByAppendingString: @"://"];
+ if ([url hasPrefix: prefix])
+ {
+ NSString *trimmed = [url substringFromIndex:[prefix length]];
+ NSArray *parts = [trimmed componentsSeparatedByString:@":"];
+ NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
+
+ if ([parts count] == 2 && [formatter numberFromString: [parts objectAtIndex:1]] != nil)
+ [gameArgs addObject: [NSString stringWithFormat: @"Launch.Connect=%@", trimmed]];
+
+ [formatter release];
+ }
+ }
+ }
+
+ [self launchGameWithArgs: gameArgs];
+ [gameArgs release];
+}
+
+- (void)applicationWillFinishLaunching:(NSNotification *)aNotification
+{
+ // Register for url events
+ NSDictionary *plist = [[NSBundle mainBundle] infoDictionary];
+ if (plist)
+ {
+ NSString *joinServerUrl = [plist objectForKey:@"JoinServerUrlScheme"];
+ NSString *bundleIdentifier = [plist objectForKey:@"CFBundleIdentifier"];
+ if (joinServerUrl && [joinServerUrl length] > 0 && bundleIdentifier)
+ {
+ LSSetDefaultHandlerForURLScheme((CFStringRef)joinServerUrl, (CFStringRef)bundleIdentifier);
+ [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self andSelector:@selector(getUrl:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL];
+ }
+ }
+}
+
+- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
+{
+ [self launchGameWithArgs: [[NSProcessInfo processInfo] arguments]];
+}
+
+- (BOOL)applicationShouldTerminateAfterLastWindowClosed: (NSApplication *)theApplication
+{
+ return YES;
+}
+
+- (void)launchGameWithArgs: (NSArray *)gameArgs
+{
+ if (launched)
+ {
+ NSLog(@"launchgame is already running... ignoring request.");
+ return;
+ }
+
+ launched = YES;
+
+ // Default values - can be overriden by setting certain keys Info.plist
+ NSString *gameName = @"OpenRA.Game.exe";
+ NSString *modId = nil;
+
+ NSDictionary *plist = [[NSBundle mainBundle] infoDictionary];
+ if (plist)
+ {
+ NSString *exeValue = [plist objectForKey:@"MonoGameExe"];
+ if (exeValue && [exeValue length] > 0)
+ gameName = exeValue;
+
+ NSString *modIdValue = [plist objectForKey:@"ModId"];
+ if (modIdValue && [modIdValue length] > 0)
+ modId = modIdValue;
+ }
+
+ NSString *exePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/MacOS/"];
+ NSString *gamePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/Resources/"];
+
+ NSString *launchPath = [exePath stringByAppendingPathComponent: @"mono"];
+ NSString *appPath = [exePath stringByAppendingPathComponent: @"OpenRA"];
+ NSString *engineLaunchPath = [self resolveTranslocatedPath: appPath];
+
+ NSMutableArray *launchArgs = [NSMutableArray arrayWithCapacity: [gameArgs count] + 2];
+ [launchArgs addObject: @"--debug"];
+ [launchArgs addObject: [gamePath stringByAppendingPathComponent: gameName]];
+ [launchArgs addObject: [NSString stringWithFormat:@"Engine.LaunchPath=\"%@\"", engineLaunchPath]];
+
+ if (modId)
+ [launchArgs addObject: [NSString stringWithFormat:@"Game.Mod=%@", modId]];
+
+ [launchArgs addObjectsFromArray: gameArgs];
+
+ NSLog(@"Running mono with arguments:");
+ for (size_t i = 0; i < [launchArgs count]; i++)
+ NSLog(@"%@", [launchArgs objectAtIndex: i]);
+
+ gameTask = [[NSTask alloc] init];
+ [gameTask setCurrentDirectoryPath: gamePath];
+ [gameTask setLaunchPath: launchPath];
+ [gameTask setArguments: launchArgs];
+
+ NSMutableDictionary *environment = [NSMutableDictionary dictionaryWithDictionary: [[NSProcessInfo processInfo] environment]];
+ [environment setObject: [gamePath stringByAppendingPathComponent: @"lib/mono/4.5"] forKey: @"MONO_PATH"];
+ [environment setObject: [gamePath stringByAppendingPathComponent: @"etc"] forKey: @"MONO_CFG_DIR"];
+ [environment setObject: [gamePath stringByAppendingPathComponent: @"etc/mono/config"] forKey: @"MONO_CONFIG"];
+ [gameTask setEnvironment: environment];
+
+ [[NSNotificationCenter defaultCenter]
+ addObserver: self
+ selector: @selector(taskExited:)
+ name: NSTaskDidTerminateNotification
+ object: gameTask
+ ];
+
+ [gameTask launch];
+}
+
+- (NSString *)resolveTranslocatedPath: (NSString *)path
+{
+ // macOS 10.12 introduced the "App Translocation" feature, which runs quarantined applications
+ // from a transient read-only disk image. The read-only image isn't a problem, but the transient
+ // path breaks the mod registration/switching feature.
+ // This resolves the original path which can then be written into the mod metadata for future
+ // launches (which will then be re-translocated)
+
+ // Running on macOS < 10.12
+ if (floor(NSAppKitVersionNumber) <= 1404)
+ return path;
+
+ void *handle = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY);
+
+ // Failed to load security framework
+ if (handle == NULL)
+ return path;
+
+ Boolean (*mySecTranslocateIsTranslocatedURL)(CFURLRef path, bool *isTranslocated, CFErrorRef * __nullable error);
+ mySecTranslocateIsTranslocatedURL = dlsym(handle, "SecTranslocateIsTranslocatedURL");
+
+ CFURLRef __nullable (*mySecTranslocateCreateOriginalPathForURL)(CFURLRef translocatedPath, CFErrorRef * __nullable error);
+ mySecTranslocateCreateOriginalPathForURL = dlsym(handle, "SecTranslocateCreateOriginalPathForURL");
+
+ // Failed to resolve required functions
+ if (mySecTranslocateIsTranslocatedURL == NULL || mySecTranslocateCreateOriginalPathForURL == NULL)
+ return path;
+
+ bool isTranslocated = false;
+ CFURLRef pathURLRef = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (__bridge CFStringRef)path, kCFURLPOSIXPathStyle, false);
+
+ if (mySecTranslocateIsTranslocatedURL(pathURLRef, &isTranslocated, NULL))
+ {
+ if (isTranslocated)
+ {
+ CFURLRef resolvedURL = mySecTranslocateCreateOriginalPathForURL(pathURLRef, NULL);
+ path = [(NSURL *)(resolvedURL) path];
+ }
+ }
+
+ CFRelease(pathURLRef);
+ return path;
+}
+
+- (void)taskExited:(NSNotification *)note
+{
+ [[NSNotificationCenter defaultCenter]
+ removeObserver:self
+ name:NSTaskDidTerminateNotification
+ object:gameTask
+ ];
+
+ int ret = [gameTask terminationStatus];
+
+ NSLog(@"launchgame exited with code %d", ret);
+ [gameTask release];
+ gameTask = nil;
+
+ // We're done here
+ if (ret == 0)
+ exit(0);
+
+ // Make the error dialog visible
+ [NSApp setActivationPolicy: NSApplicationActivationPolicyRegular];
+ [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
+ [self showCrashPrompt];
+
+ exit(1);
+}
+
+@end
+
+int main(int argc, char **argv)
+{
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ NSApplication *application = [NSApplication sharedApplication];
+ OpenRALauncher *launcher = [[OpenRALauncher alloc] init];
+ [NSApp setActivationPolicy: NSApplicationActivationPolicyProhibited];
+
+ [application setDelegate:launcher];
+ [application run];
+
+ [launcher release];
+ [pool drain];
+
+ return EXIT_SUCCESS;
+}