Split a separate "compat" macOS package that uses the system mono.

This commit is contained in:
Paul Chote
2020-08-30 23:06:14 +01:00
committed by teinarss
parent 9b90e4f25a
commit 8a9b5e7e01
5 changed files with 829 additions and 85 deletions

View File

@@ -0,0 +1,3 @@
<configuration>
<dllmap os="osx" dll="lua51.dll" target="liblua.5.1.dylib" />
</configuration>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleDisplayName</key>
<string>{MOD_NAME}</string>
<key>CFBundleExecutable</key>
<string>OpenRA</string>
<key>CFBundleIconFile</key>
<string>{MOD_ID}.icns</string>
<key>CFBundleIdentifier</key>
<string>net.openra.mod.{MOD_ID}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>{MOD_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>{DEV_VERSION}</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>{DEV_VERSION}</string>
<key>LSMinimumSystemVersion</key>
<string>{MINIMUM_SYSTEM_VERSION}</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>OpenRA Server</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{JOIN_SERVER_URL_SCHEME}</string>
<string>{DISCORD_URL_SCHEME}</string>
</array>
</dict>
</array>
<key>ModId</key>
<string>{MOD_ID}</string>
<key>FaqUrl</key>
<string>{FAQ_URL}</string>
<key>JoinServerUrlScheme</key>
<string>{JOIN_SERVER_URL_SCHEME}</string>
</dict>
</plist>

View File

@@ -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}" "<string>discord-${DISCORD_APPID}</string>" "${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,13 +96,35 @@ 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})"
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"
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
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
echo "Building core files"
pushd "${SRCDIR}" > /dev/null || exit 1
@@ -126,13 +149,9 @@ sign_bundle "OpenRA - Dune 2000.app"
rm -rf "${BUILTDIR}/OpenRA.app"
if [ -n "${MACOS_DEVELOPER_CERTIFICATE_BASE64}" ] && [ -n "${MACOS_DEVELOPER_CERTIFICATE_PASSWORD}" ] && [ -n "${MACOS_DEVELOPER_IDENTITY}" ]; then
security delete-keychain build.keychain
fi
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}')
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
# Background image is created from source svg in artsrc repository
@@ -178,41 +197,44 @@ sync
sync
hdiutil detach "${DMG_DEVICE}"
rm -rf "${BUILTDIR}"
}
# Submit for notarization
if [ -n "${MACOS_DEVELOPER_USERNAME}" ] && [ -n "${MACOS_DEVELOPER_PASSWORD}" ]; then
echo "Submitting disk image for notarization"
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"

View File

@@ -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 <Cocoa/Cocoa.h>
#include <dlfcn.h>
#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 <NSApplicationDelegate>
- (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;
}

277
packaging/macos/launcher.m Normal file
View File

@@ -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 <Cocoa/Cocoa.h>
#include <dlfcn.h>
@interface OpenRALauncher : NSObject <NSApplicationDelegate>
- (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;
}